rethink save/load

This commit is contained in:
Levi Neely 2026-02-11 11:35:08 +01:00
parent 3f570e993e
commit f104c6aa5e
4 changed files with 30 additions and 98 deletions

View File

@ -4,7 +4,6 @@ package backends
import (
"anvillm/internal/backend"
"anvillm/internal/backend/tmux"
"anvillm/internal/debug"
"context"
"fmt"
"os"
@ -301,7 +300,7 @@ func stripANSI(s string) string {
// SessionsDir returns the path to the sessions storage directory
func SessionsDir() string {
home, _ := os.UserHomeDir()
dir := filepath.Join(home, ".config", "acme-q", "sessions")
dir := filepath.Join(home, ".config", "anvillm", "sessions")
os.MkdirAll(dir, 0755)
return dir
}
@ -395,48 +394,8 @@ func contains(slice []string, item string) bool {
return false
}
// SavePath returns the save file path for a session ID
func SavePath(id string) string {
return filepath.Join(SessionsDir(), id+".json")
}
// SendCtl is a helper to send a control command to kiro-cli
func SendCtl(sess backend.Session, cmd string) error {
_, err := sess.Send(context.Background(), cmd)
return err
}
// Save saves a session using kiro-cli's /chat save command
func Save(sess backend.Session, id string) error {
return SendCtl(sess, "/chat save "+SavePath(id))
}
// Load loads a session using kiro-cli's /chat load command
func Load(sess backend.Session, path string) error {
return SendCtl(sess, "/chat load "+path)
}
// SaveWithSmartFilename saves with a generated filename
func SaveWithSmartFilename(sess backend.Session, id string, bodyText string) (string, error) {
tempPath := SavePath(id)
if err := SendCtl(sess, "/chat save "+tempPath); err != nil {
return "", err
}
smartPath, err := GenerateSmartFilename(tempPath, bodyText)
if err != nil {
debug.Log("[session %s] Smart filename failed: %v, keeping %s", id, err, tempPath)
return tempPath, nil
}
if smartPath != tempPath {
if err := os.Rename(tempPath, smartPath); err == nil {
return smartPath, nil
} else {
debug.Log("[session %s] Rename failed: %v", id, err)
return tempPath, nil
}
}
return smartPath, nil
}

View File

@ -4,7 +4,6 @@ package p9
import (
"anvillm/internal/backend"
"anvillm/internal/backend/tmux"
"anvillm/internal/backends"
"anvillm/internal/session"
"context"
"errors"
@ -29,7 +28,7 @@ agent/
ctl (write) "new <backend> <cwd>" creates session, returns id
list (read) list sessions: "id state pid cwd"
{session-id}/
ctl (write) "save [path]", "load path", "kill"
ctl (write) "stop", "kill"
in (write) send prompt, blocks until response
out (read) response from last prompt
err (read) stderr/debug output
@ -322,7 +321,7 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
}
args := strings.Fields(input)
if len(args) == 0 {
return errFcall(fc, "usage: stop | save [path] | load path | kill")
return errFcall(fc, "usage: stop | kill")
}
switch args[0] {
case "stop":
@ -330,21 +329,6 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
if err := sess.Stop(ctx); err != nil {
return errFcall(fc, err.Error())
}
case "save":
path := backends.SavePath(sess.ID())
if len(args) > 1 {
path = args[1]
}
if err := backends.SendCtl(sess, "/chat save "+path); err != nil {
return errFcall(fc, err.Error())
}
case "load":
if len(args) < 2 {
return errFcall(fc, "load requires path")
}
if err := backends.Load(sess, args[1]); err != nil {
return errFcall(fc, err.Error())
}
case "kill":
sess.Close()
s.mgr.Remove(sess.ID())

View File

@ -3,9 +3,16 @@ package sandbox
import (
"fmt"
"os"
"sort"
"strings"
)
// pathEntry holds a path with its permission type
type pathEntry struct {
path string
flag string // --ro, --rox, --rw, --rwx
}
// WrapCommand wraps a command with landrun based on the configuration.
// Returns the wrapped command args, or the original command if landrun is unavailable.
func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string {
@ -44,38 +51,46 @@ func WrapCommand(cfg *Config, originalCmd []string, cwd string) []string {
args = append(args, "--add-exec")
}
// Filesystem permissions - RO (read-only, no execute)
// Collect all filesystem paths
var entries []pathEntry
for _, path := range cfg.Filesystem.RO {
expanded := expandPath(path, cwd, home, tmpdir)
if pathExists(expanded) {
args = append(args, "--ro", expanded)
entries = append(entries, pathEntry{expanded, "--ro"})
}
}
// Filesystem permissions - ROX (read-only with execute)
for _, path := range cfg.Filesystem.ROX {
expanded := expandPath(path, cwd, home, tmpdir)
if pathExists(expanded) {
args = append(args, "--rox", expanded)
entries = append(entries, pathEntry{expanded, "--rox"})
}
}
// Filesystem permissions - RW (read-write, no execute)
for _, path := range cfg.Filesystem.RW {
expanded := expandPath(path, cwd, home, tmpdir)
if pathExists(expanded) {
args = append(args, "--rw", expanded)
entries = append(entries, pathEntry{expanded, "--rw"})
}
}
// Filesystem permissions - RWX (read-write with execute)
for _, path := range cfg.Filesystem.RWX {
expanded := expandPath(path, cwd, home, tmpdir)
if pathExists(expanded) {
args = append(args, "--rwx", expanded)
entries = append(entries, pathEntry{expanded, "--rwx"})
}
}
// Sort paths so parents come before children (shorter paths first, then alphabetically)
sort.Slice(entries, func(i, j int) bool {
if len(entries[i].path) != len(entries[j].path) {
return len(entries[i].path) < len(entries[j].path)
}
return entries[i].path < entries[j].path
})
// Add sorted paths to args
for _, e := range entries {
args = append(args, e.flag, e.path)
}
// Network permissions
if cfg.Network.Unrestricted {
args = append(args, "--unrestricted-network")

28
main.go
View File

@ -297,7 +297,7 @@ func openChatWindow(sess backend.Session) (*acme.Win, error) {
return nil, err
}
w.Name(name)
w.Write("tag", []byte("Send Stop Attach Alias Save Sessions "))
w.Write("tag", []byte("Send Stop Attach Alias Sessions "))
w.Fprintf("body", "# Session %s\n# cwd: %s\n\nUSER:\n", sess.ID(), meta.Cwd)
w.Ctl("clean")
@ -372,32 +372,6 @@ func handleChatWindow(w *acme.Win, sess backend.Session) {
newName := filepath.Join(meta.Cwd, fmt.Sprintf("+Chat.%s", arg))
w.Name(newName)
fmt.Printf("Alias set to %s\n", arg)
case "Save":
// Grab first part of window content for filename hints
var bodyText string
if body, err := w.ReadAll("body"); err == nil && len(body) > 0 {
if len(body) > 500 {
body = body[:500]
}
bodyText = string(body)
}
go func() {
var savePath string
var err error
if arg != "" {
// Save to explicit path
savePath = arg
err = backends.SendCtl(sess, "/chat save "+savePath)
} else {
// Save with smart filename
savePath, err = backends.SaveWithSmartFilename(sess, sess.ID(), bodyText)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error saving: %v\n", err)
return
}
fmt.Printf("Saved to %s\n", savePath)
}()
case "Sessions":
openSessionsWindow(w, sess)
default: