From 101dace80efc67a6a074ffbf0d2b63a747bb463b Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Thu, 22 Oct 2026 20:40:42 -0700 Subject: [PATCH] Notify the agent when any command it runs in a task terminal finishes --- docs/operations.md | 2 +- internal/agents/terminal.go | 8 ++++---- internal/terminal/terminal.go | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/operations.md b/docs/operations.md index 245da78..1c07ce9 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -130,4 +130,4 @@ byre never signs with a key on the machine it runs on: every git command it runs - `terminal_type` takes named keys because Claude Code passes tool arguments through as the text the model wrote, so an escape like `\u0003` in `text` reaches the shell as six characters. - In a new shell it waits for the prompt first: typing earlier would reach the PTY while it still echoes in cooked mode, before zsh's line editor takes over, so the text would show twice. A terminal started from a saved command waits the same way before its command is typed. - Saved commands belong to a user on one server, because what they do is usually tied to that Mac: signing in again to the credential an `apiKeyHelper` uses (AppleConnect's `getToken -n cli` on a work Mac) is the case they were made for. Its terminal stays open after the command, so the result stays readable. -- zsh gets shell integration through a `.zshenv` in `/zsh` (via `ZDOTDIR`) that reports the prompt (OSC 133) and the line being edited, so byre can wake the agent when a line it left is run. It turns off `PROMPT_SP`, which would print a `%` padded to the width the shell started at, so an agent-opened shell (120 columns) replayed in a narrower tab wraps each one onto an extra line. A user `.zshrc` that sets `zle-line-pre-redraw` with `zle -N` rather than `add-zle-hook-widget` replaces the hook, and byre then sees the line as empty. Other shells get no integration and nothing wakes their agents. +- zsh gets shell integration through a `.zshenv` in `/zsh` (via `ZDOTDIR`) that reports the prompt (OSC 133) and the line being edited, so byre can wake the agent when a command it ran or a line it left finishes. It turns off `PROMPT_SP`, which would print a `%` padded to the width the shell started at, so an agent-opened shell (120 columns) replayed in a narrower tab wraps each one onto an extra line. A user `.zshrc` that sets `zle-line-pre-redraw` with `zle -N` rather than `add-zle-hook-widget` replaces the hook, and byre then sees the line as empty. Other shells get no integration and nothing wakes their agents. diff --git a/internal/agents/terminal.go b/internal/agents/terminal.go index 533a2b7..8f9c8cd 100644 --- a/internal/agents/terminal.go +++ b/internal/agents/terminal.go @@ -110,7 +110,8 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { "When the input line still holds unsent text after keys, text is refused, so that it isn't joined onto someone else's command: clear it with ctrl-u first, or set append to add to it on purpose. " + "Replies from this tool and terminal_read end with the shell's current input line when zsh is at its prompt. " + "The human sees everything you type and its output. " + - "To hand the human a command to run (one needing their password, say), type it without the newline and tell them to press Enter in the Terminal tab. In a zsh terminal, the reply then says byre will message you when the shell next returns to its prompt, with the exit status and output, starting a turn if you are idle, so end your turn rather than asking whether they ran it. " + + "In a zsh terminal, whenever a call leaves a command running or a line unsent, the reply says byre will message you when the shell next returns to its prompt, with the exit status and the output since the reply, starting a turn if you are idle: so after starting a long command, end your turn rather than polling it. " + + "To hand the human a command to run (one needing their password, say), type it without the newline and tell them to press Enter in the Terminal tab, then end your turn rather than asking whether they ran it. " + "Returns the output that followed, as terminal_read does, once the terminal has been quiet for half a second or after 10 seconds; read the rest of a longer command's output with terminal_read.", InputSchema: json.RawMessage(`{ "type": "object", @@ -156,7 +157,6 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { keys += b } term.CancelWake() - startLine, _ := term.InputLine() sent := keys out := "" if keys != "" || args.Text == "" { @@ -188,7 +188,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { } return out, nil } - if term.WakeOnPrompt(m.wakeOnPrompt(l), !submitted && (startLine != "" || args.Text != "")) { + if term.WakeOnPrompt(m.wakeOnPrompt(l)) { out += "\n[byre messages you when the shell is next back at its prompt]" } return out, nil @@ -198,7 +198,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { func (m *Manager) wakeOnPrompt(l *live) func(string, []byte) { return func(exit string, out []byte) { - text := fmt.Sprintf("The line you left unsent in the terminal was run, and the shell is back at its prompt with exit status %s (130 means it was canceled with Ctrl-C). The terminal's output since you typed it:\n\n%s", exit, terminal.Text(out)) + text := fmt.Sprintf("The terminal you last typed into is back at its prompt: the command there finished with exit status %s (130 means it was canceled with Ctrl-C). The terminal's output since your last terminal_type reply:\n\n%s", exit, terminal.Text(out)) _, _, err := m.Message(context.Background(), l.id, "byre", text, "", "", "") if err != nil { slog.Error("wake agent on terminal prompt", "session", l.id, "task", l.taskID, "err", err) diff --git a/internal/terminal/terminal.go b/internal/terminal/terminal.go index 905b056..9340d0d 100644 --- a/internal/terminal/terminal.go +++ b/internal/terminal/terminal.go @@ -366,10 +366,10 @@ func (term *Terminal) InputLine() (string, bool) { return term.line, term.atPrompt } -func (term *Terminal) WakeOnPrompt(fn func(exit string, out []byte), mayRun bool) bool { +func (term *Terminal) WakeOnPrompt(fn func(exit string, out []byte)) bool { term.mu.Lock() defer term.mu.Unlock() - if term.atPrompt && term.line == "" || !term.atPrompt && !mayRun { + if term.atPrompt && term.line == "" { return false } term.wake = &wake{fn: fn}