Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions cmd/bytemind/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ type slashCommand struct {

var slashCommands = []slashCommand{
{Name: "/help", Usage: "/help", Description: "Show available commands"},
{Name: "/session", Usage: "/session", Description: "Show the current session"},
{Name: "/session", Usage: "/session", Description: "Show the current session (CLI)"},
{Name: "/sessions", Usage: "/sessions [limit]", Description: "List recent sessions"},
{Name: "/resume", Usage: "/resume <id>", Description: "Resume a recent session by id or prefix"},
{Name: "/resume", Usage: "/resume <id>", Description: "Resume a recent session by id or prefix (CLI only; TUI uses /session then Enter)"},
{Name: "/new", Usage: "/new", Description: "Start a new session in the current workspace"},
{Name: "/quit", Usage: "/quit", Description: "Exit the CLI"},
}
Expand Down Expand Up @@ -237,6 +237,11 @@ func handleSlashCommand(stdout io.Writer, store *session.Store, current *session
fmt.Fprintln(stdout, "usage: /resume <id>")
return current, false, true, nil
}
if store != nil {
if err := purgeZeroMessageSessionsForWorkspace(stdout, store, current.Workspace, current.ID); err != nil {
return current, false, true, err
}
}
id, err := resolveSessionID(store, fields[1])
if err != nil {
return current, false, true, err
Expand All @@ -248,14 +253,29 @@ func handleSlashCommand(stdout io.Writer, store *session.Store, current *session
if !sameWorkspace(current.Workspace, next.Workspace) {
return current, false, true, fmt.Errorf("session %s belongs to workspace %s, current workspace is %s", next.ID, next.Workspace, current.Workspace)
}
if store != nil {
if err := purgeZeroMessageSessionsForWorkspace(stdout, store, current.Workspace, next.ID); err != nil {
return current, false, true, err
}
}
fmt.Fprintf(stdout, "%sresumed%s %s\n", ansiDim, ansiReset, next.ID)
printCurrentSession(stdout, next)
return next, false, true, nil
case "/new":
if store != nil {
if err := purgeZeroMessageSessionsForWorkspace(stdout, store, current.Workspace, current.ID); err != nil {
return current, false, true, err
}
}
next := session.New(current.Workspace)
if err := store.Save(next); err != nil {
return current, false, true, err
}
if store != nil {
if err := purgeZeroMessageSessionsForWorkspace(stdout, store, current.Workspace, next.ID); err != nil {
return current, false, true, err
}
}
fmt.Fprintf(stdout, "%snew session%s %s\n", ansiDim, ansiReset, next.ID)
printCurrentSession(stdout, next)
return next, false, true, nil
Expand Down Expand Up @@ -335,9 +355,12 @@ func printSessions(w io.Writer, store *session.Store, currentID string, limit in
if item.ID == currentID {
marker = "*"
}
preview := item.LastUserMessage
preview := strings.TrimSpace(item.Title)
if preview == "" {
preview = item.LastUserMessage
}
if preview == "" {
preview = "(no user prompt yet)"
preview = "(untitled session)"
}
fmt.Fprintf(w, "%s %s %s %2d msgs %s\n", marker, item.ID, item.UpdatedAt.Local().Format("2006-01-02 15:04"), item.MessageCount, preview)
fmt.Fprintf(w, "%s %s%s\n", ansiGray, item.Workspace, ansiReset)
Expand Down Expand Up @@ -392,6 +415,20 @@ func parseOptionalLimit(fields []string) (int, error) {
return limit, nil
}

func purgeZeroMessageSessionsForWorkspace(stdout io.Writer, store *session.Store, workspace, excludeID string) error {
deleted, warnings, err := store.PurgeZeroMessageSessions(workspace, excludeID)
if err != nil {
return err
}
for _, warning := range warnings {
fmt.Fprintf(stdout, "%swarning%s %s\n", ansiDim, ansiReset, warning)
}
if deleted > 0 {
fmt.Fprintf(stdout, "%sinfo%s auto-cleaned %d zero-msg session(s)\n", ansiDim, ansiReset, deleted)
}
return nil
}

func sameWorkspace(a, b string) bool {
left, err := filepath.Abs(a)
if err != nil {
Expand Down
120 changes: 120 additions & 0 deletions cmd/bytemind/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"testing"

"bytemind/internal/config"
"bytemind/internal/llm"
"bytemind/internal/provider"
"bytemind/internal/session"
)
Expand All @@ -34,6 +35,19 @@ func TestPrintUsageIncludesInstallCommand(t *testing.T) {
}
}

func TestPrintHelpClarifiesResumePathsForCLIAndTUI(t *testing.T) {
var out bytes.Buffer
printHelp(&out)
text := out.String()

if !strings.Contains(text, "/resume <id>") {
t.Fatalf("expected help to include /resume usage, got %q", text)
}
if !strings.Contains(text, "CLI only") || !strings.Contains(text, "/session then Enter") {
t.Fatalf("expected help to clarify CLI/TUI resume semantics, got %q", text)
}
}

func TestCompleteSlashCommandReturnsSuggestionsForAmbiguousPrefix(t *testing.T) {
completed, suggestions := completeSlashCommand("/sess")
if completed != "/sess" {
Expand Down Expand Up @@ -120,6 +134,7 @@ func TestHandleSlashCommandResumesSessionWithinWorkspace(t *testing.T) {

resumed := session.New(filepath.Join(workspace, "."))
resumed.ID = "resume-me"
resumed.Messages = append(resumed.Messages, llm.NewUserTextMessage("resume payload"))
if err := store.Save(resumed); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -249,6 +264,85 @@ func TestHandleSlashCommandCreatesNewSession(t *testing.T) {
}
}

func TestHandleSlashCommandNewPurgesZeroMsgSessions(t *testing.T) {
store, err := session.NewStore(t.TempDir())
if err != nil {
t.Fatal(err)
}

workspace := t.TempDir()
current := session.New(workspace)
current.ID = "current"
current.Messages = append(current.Messages, llm.NewUserTextMessage("keep current"))
if err := store.Save(current); err != nil {
t.Fatal(err)
}

zero := session.New(workspace)
zero.ID = "zero"
if err := store.Save(zero); err != nil {
t.Fatal(err)
}

var out bytes.Buffer
next, shouldExit, handled, err := handleSlashCommand(&out, store, current, "/new")
if err != nil {
t.Fatal(err)
}
if shouldExit || !handled {
t.Fatalf("expected handled new command without exit, got handled=%v shouldExit=%v", handled, shouldExit)
}
if next.ID == current.ID {
t.Fatalf("expected a new session, got %#v", next)
}
if _, err := store.Load(zero.ID); !os.IsNotExist(err) {
t.Fatalf("expected stale zero-msg session to be purged, got %v", err)
}
}

func TestHandleSlashCommandResumePurgesZeroMsgSessions(t *testing.T) {
store, err := session.NewStore(t.TempDir())
if err != nil {
t.Fatal(err)
}

workspace := t.TempDir()
current := session.New(workspace)
current.ID = "current"
current.Messages = append(current.Messages, llm.NewUserTextMessage("current message"))
if err := store.Save(current); err != nil {
t.Fatal(err)
}

resumed := session.New(workspace)
resumed.ID = "resume-target"
resumed.Messages = append(resumed.Messages, llm.NewUserTextMessage("resume message"))
if err := store.Save(resumed); err != nil {
t.Fatal(err)
}

zero := session.New(workspace)
zero.ID = "zero"
if err := store.Save(zero); err != nil {
t.Fatal(err)
}

var out bytes.Buffer
next, shouldExit, handled, err := handleSlashCommand(&out, store, current, "/resume resume-target")
if err != nil {
t.Fatal(err)
}
if shouldExit || !handled {
t.Fatalf("expected handled resume command without exit, got handled=%v shouldExit=%v", handled, shouldExit)
}
if next.ID != resumed.ID {
t.Fatalf("expected resumed session %q, got %#v", resumed.ID, next)
}
if _, err := store.Load(zero.ID); !os.IsNotExist(err) {
t.Fatalf("expected stale zero-msg session to be purged, got %v", err)
}
}

func TestPrintSessionsShowsFullSessionID(t *testing.T) {
store, err := session.NewStore(t.TempDir())
if err != nil {
Expand All @@ -270,6 +364,32 @@ func TestPrintSessionsShowsFullSessionID(t *testing.T) {
}
}

func TestPrintSessionsPrefersTitleOverPreview(t *testing.T) {
store, err := session.NewStore(t.TempDir())
if err != nil {
t.Fatal(err)
}
sess := session.New(`E:\\repo`)
sess.ID = "title-first"
sess.Title = "Session Summary Title"
sess.Messages = append(sess.Messages, llm.NewUserTextMessage("preview that should not be preferred"))
if err := store.Save(sess); err != nil {
t.Fatal(err)
}

var out bytes.Buffer
if err := printSessions(&out, store, sess.ID, 8); err != nil {
t.Fatal(err)
}
output := out.String()
if !strings.Contains(output, "Session Summary Title") {
t.Fatalf("expected title to be rendered in sessions list, got %q", output)
}
if strings.Contains(output, "preview that should not be preferred") {
t.Fatalf("expected title to override preview in sessions list, got %q", output)
}
}

func TestHandleSlashCommandReportsUnknownCommandSuggestions(t *testing.T) {
store, err := session.NewStore(t.TempDir())
if err != nil {
Expand Down
Loading
Loading