diff --git a/cmd/Assist/main.go b/cmd/Assist/main.go index 0c6c4a7..d303323 100644 --- a/cmd/Assist/main.go +++ b/cmd/Assist/main.go @@ -820,7 +820,7 @@ func openPromptWindow(sess *SessionInfo) (*acme.Win, error) { return nil, err } w.Name(name) - w.Write("tag", []byte("Send ")) + w.Write("tag", []byte("Send Compact Clear ")) w.Ctl("clean") // Track window ID client-side @@ -842,19 +842,32 @@ func handlePromptWindow(w *acme.Win, sess *SessionInfo) { for e := range w.EventChan() { cmd := string(e.Text) - if (e.C2 == 'x' || e.C2 == 'X') && cmd == "Send" { - body, err := w.ReadAll("body") - if err != nil { - continue - } - prompt := strings.TrimSpace(string(body)) - if prompt != "" { - if err := sendPrompt(sess.ID, prompt); err != nil { - fmt.Fprintf(os.Stderr, "Failed to send: %v\n", err) + if e.C2 == 'x' || e.C2 == 'X' { + switch cmd { + case "Send": + body, err := w.ReadAll("body") + if err != nil { continue } - w.Ctl("delete") - return + prompt := strings.TrimSpace(string(body)) + if prompt != "" { + if err := sendPrompt(sess.ID, prompt); err != nil { + fmt.Fprintf(os.Stderr, "Failed to send: %v\n", err) + continue + } + w.Ctl("delete") + return + } + case "Compact": + if err := controlSession(sess.ID, "compact"); err != nil { + fmt.Fprintf(os.Stderr, "Failed to compact: %v\n", err) + } + case "Clear": + if err := controlSession(sess.ID, "clear"); err != nil { + fmt.Fprintf(os.Stderr, "Failed to clear: %v\n", err) + } + default: + w.WriteEvent(e) } } else { w.WriteEvent(e) diff --git a/internal/backend/tmux/helpers.go b/internal/backend/tmux/helpers.go index b500027..669b978 100644 --- a/internal/backend/tmux/helpers.go +++ b/internal/backend/tmux/helpers.go @@ -63,6 +63,11 @@ func sendKeys(target string, keys ...string) error { return err } +// SendKeysTo is an exported wrapper for sendKeys, for use by backend-specific handlers. +func SendKeysTo(target string, keys ...string) error { + return sendKeys(target, keys...) +} + // sendLiteral sends literal text to a tmux target (doesn't interpret special chars) func sendLiteral(target, text string) error { _, err := tmuxCmd("send-keys", "-t", target, "-l", text) diff --git a/internal/backend/tmux/session.go b/internal/backend/tmux/session.go index c038fbf..c84afe7 100644 --- a/internal/backend/tmux/session.go +++ b/internal/backend/tmux/session.go @@ -41,6 +41,7 @@ type Session struct { model string // Active model override (empty = backend default) modelResolver ModelResolver // Optional: modifies command to include model selection + clearHandler ClearHandler // Optional: backend-specific /clear handling commands backend.CommandHandler startupHandler StartupHandler @@ -487,6 +488,29 @@ func (s *Session) SendStream(ctx context.Context, prompt string) (io.ReadCloser, return io.NopCloser(strings.NewReader(response)), nil } +// SendRaw sends literal text followed by Enter to the tmux session. +// Used for slash commands like /clear and /compact. +func (s *Session) SendRaw(text string) error { + s.mu.Lock() + target := s.target() + s.mu.Unlock() + return sendKeys(target, text, "C-m") +} + +// Clear sends the /clear command using backend-specific handling if available. +func (s *Session) Clear() error { + s.mu.Lock() + target := s.target() + handler := s.clearHandler + s.mu.Unlock() + + if handler != nil { + return handler(target) + } + // Default: just send /clear + Enter + return sendKeys(target, "/clear", "C-m") +} + // stopInternal performs the actual stop operation. // Caller must hold s.mu and have set s.transitioning = true. // This method will unlock s.mu while performing stop operations. diff --git a/internal/backend/tmux/tmux.go b/internal/backend/tmux/tmux.go index 13a1593..bc1d03f 100644 --- a/internal/backend/tmux/tmux.go +++ b/internal/backend/tmux/tmux.go @@ -44,6 +44,11 @@ type StateInspector interface { // For positional CLIs: replace the relevant positional argument type ModelResolver func(cmd []string, model string) []string +// ClearHandler handles the /clear command for a specific backend. +// It receives the tmux target and should send the appropriate keys. +// Returns nil on success. +type ClearHandler func(target string) error + // Config holds tmux backend configuration type Config struct { Name string @@ -56,6 +61,7 @@ type Config struct { StateInspector StateInspector // Optional: for process tree inspection NsSuffix string // Optional: namespace suffix (e.g., "0" for :0) ModelResolver ModelResolver // Optional: modifies command to include model selection + ClearHandler ClearHandler // Optional: backend-specific /clear handling } // Backend implements backend.Backend for tmux-based CLI tools @@ -351,6 +357,7 @@ func (b *Backend) CreateSession(ctx context.Context, opts backend.SessionOptions sandbox: sbx, model: opts.Model, modelResolver: b.cfg.ModelResolver, + clearHandler: b.cfg.ClearHandler, pid: pid, state: "starting", createdAt: time.Now(), diff --git a/internal/backends/kirocli.go b/internal/backends/kirocli.go index 75408c1..48e369f 100644 --- a/internal/backends/kirocli.go +++ b/internal/backends/kirocli.go @@ -33,10 +33,20 @@ func NewKiroCLI(nsSuffix string) backend.Backend { }, StartupTime: 30 * time.Second, StateInspector: &kiroStateInspector{}, + ClearHandler: kiroClearHandler, NsSuffix: nsSuffix, }) } +// kiroClearHandler sends /clear and confirms the "y" prompt for kiro-cli +func kiroClearHandler(target string) error { + if err := tmux.SendKeysTo(target, "/clear", "C-m"); err != nil { + return err + } + time.Sleep(250 * time.Millisecond) + return tmux.SendKeysTo(target, "y", "C-m") +} + // kiroStateInspector implements StateInspector for kiro-cli type kiroStateInspector struct{} diff --git a/internal/p9/server.go b/internal/p9/server.go index a3b82c2..d54eb8c 100644 --- a/internal/p9/server.go +++ b/internal/p9/server.go @@ -845,6 +845,18 @@ func (s *Server) write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { if err := sess.Refresh(ctx); err != nil { return errFcall(fc, err.Error()) } + case "clear": + if tmuxSess, ok := sess.(*tmux.Session); ok { + if err := tmuxSess.Clear(); err != nil { + return errFcall(fc, err.Error()) + } + } + case "compact": + if tmuxSess, ok := sess.(*tmux.Session); ok { + if err := tmuxSess.SendRaw("/compact"); err != nil { + return errFcall(fc, err.Error()) + } + } case "complete": if len(args) < 2 { return errFcall(fc, "usage: complete ")