package session import ( "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "ollie/agent" ) func (a *Session) handleCommand(ctx context.Context, input string) bool { if !strings.HasPrefix(input, "/") { return false } parts := strings.Fields(input) if len(parts) == 0 { return false } cmd := parts[0] args := parts[1:] // Try agent-level commands first (they access agent internals directly). if a.r.HandleCommand(ctx, input) { return true } listFromHandler := func(name string) { if h := a.listHandlers[name]; h != nil { for _, item := range h() { a.emit(agent.Event{Role: "info", Content: " " + item + "\n"}) } } } type cmdFn func([]string) cmds := map[string]cmdFn{ "/sessions": func(args []string) { allFlag := len(args) > 0 && args[0] == "-a" type sessionFile struct { id string path string } var files []sessionFile appendSessionFiles := func(dir string) { entries, err := os.ReadDir(dir) if err != nil { return } for i := len(entries) - 1; i >= 0; i-- { e := entries[i] if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { continue } files = append(files, sessionFile{ id: strings.TrimSuffix(e.Name(), ".json"), path: filepath.Join(dir, e.Name()), }) } } appendSessionFiles(a.sessionsDir) appendSessionFiles(filepath.Join(a.sessionsDir, "active")) cwd := a.CWD() found := false for _, file := range files { data, readErr := os.ReadFile(file.path) if readErr != nil { continue } var ps agent.PersistedAgent if json.Unmarshal(data, &ps) != nil { continue } if !allFlag && ps.CWD != "" && ps.CWD != cwd { continue } marker := " " if file.id == a.id { marker = "* " } goal := "" for _, msg := range ps.Messages { if msg.Role == "user" { goal = msg.Content break } } if len(goal) > 60 { goal = goal[:60] + "..." } a.emit(agent.Event{Role: "info", Content: marker + fmt.Sprintf("%-24s [%s] %q", file.id, ps.Agent, goal) + "\n"}) found = true } if !found { a.emit(agent.Event{Role: "info", Content: "no sessions for " + cwd + "\n"}) } }, "/save": func(args []string) { if !a.r.HasHistory() { a.emit(agent.Event{Role: "info", Content: "error: no active session\n"}) return } if len(args) == 0 { a.emit(agent.Event{Role: "info", Content: "error: /save requires a name\n"}) return } name := args[0] path := a.sessionsDir + "/" + name + ".json" if err := a.r.SaveTo(path, name, a.CWD()); err != nil { a.emit(agent.Event{Role: "info", Content: "error: " + err.Error() + "\n"}) return } a.emit(agent.Event{Role: "info", Content: "saved: " + path + "\n"}) }, "/resume": func(args []string) { if len(args) == 0 { a.emit(agent.Event{Role: "info", Content: "error: /resume requires a session id or name\n"}) return } if a.IsRunning() { a.emit(agent.Event{Role: "info", Content: "error: cannot resume while agent is running\n"}) return } name := args[0] path := a.sessionsDir + "/" + name + ".json" data, err := os.ReadFile(path) if err != nil { a.emit(agent.Event{Role: "info", Content: fmt.Sprintf("error: %v\n", err)}) return } var ps agent.PersistedAgent if err := json.Unmarshal(data, &ps); err != nil { a.emit(agent.Event{Role: "info", Content: fmt.Sprintf("error: %v\n", err)}) return } a.r.Restore(&ps) a.emit(agent.Event{Role: "info", Content: fmt.Sprintf("resumed session %s (%d messages)\n", name, len(ps.Messages))}) }, "/cwd": func(args []string) { if len(args) == 0 { a.emit(agent.Event{Role: "info", Content: "cwd: " + a.CWD() + "\n"}) return } dir := strings.Join(args, " ") if err := a.SetCWD(dir); err != nil { a.emit(agent.Event{Role: "info", Content: "error: " + err.Error() + "\n"}) return } a.emit(agent.Event{Role: "info", Content: "cwd: " + dir + "\n"}) }, "/skills": func(args []string) { listFromHandler("skills") }, "/tools": func(args []string) { listFromHandler("tools") }, "/help": func(args []string) { lines := []string{ "Available commands:", " /agents - list available agent configs", " /sessions [-a] - list saved sessions (current cwd; -a for all)", " /save - save session as a named checkpoint", " /resume - resume a saved session", " /agent [name] - show or switch active agent", " /backend [type] - show current backend, or switch to ", " /model [name] - show current model, or switch to ", " /models - list available models", " /maxsteps [n] - show or set max tool-call steps (0 = unlimited)", " /skills - list available skills", " /tools - list available tools", " /cwd [path] - show or change working directory", " /i - inject prompt into the running turn", " /irw - rewrite the pending inject", " /compact - summarize conversation and compact context", " /context - show context size and message breakdown", " /cost - show last turn and session cost", " /usage - show token usage and context percentage", " /history - dump bounded message history", " /clear - clear session", " /sp - show rendered system prompt", " /help - show this help", } for _, l := range lines { a.emit(agent.Event{Role: "info", Content: l + "\n"}) } }, } fn, ok := cmds[cmd] if !ok { return false } a.emit(agent.Event{Role: "info", Content: "\n"}) fn(args) return true }