tmux backend and p9 server improvements

This commit is contained in:
Levi Neely 2026-03-05 09:32:07 +01:00
parent 948e23156a
commit e3a63dedd8
6 changed files with 83 additions and 12 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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.

View File

@ -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(),

View File

@ -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{}

View File

@ -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 <msg-id>")