This repository has been archived on 2026-08-16. You can view files and clone it, but cannot push or open issues or pull requests.
ollie-core/session/commands.go

202 lines
5.7 KiB
Go

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 <name> - save session as a named checkpoint",
" /resume <id> - resume a saved session",
" /agent [name] - show or switch active agent",
" /backend [type] - show current backend, or switch to <type>",
" /model [name] - show current model, or switch to <name>",
" /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 <prompt> - inject prompt into the running turn",
" /irw <prompt> - 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
}