Wake agents when the human runs a command they left in the terminal
This commit is contained in:
@@ -421,6 +421,8 @@ Each active task can have one terminal: a login shell (`$SHELL -l`, from byre's
|
||||
|
||||
A task's agents use the same terminal through two MCP tools (`internal/agents/terminal.go`). `terminal_read` returns the backlog as plain text; it starts no shell, and says so when none is running. `terminal_type` writes `text` to the PTY as keystrokes, starting the shell at 120×40 if none is running and leaving a running one's size alone (refused once the task is closed). Within a shell's first 10s, it first waits for the prompt: until the shell has printed something and then gone 500ms without output (`Terminal.Activity` gives the start and last output times), or until 10s after the start. Typing before that would reach the PTY while it still echoes in cooked mode, before zsh's line editor takes over, so the text would show twice. The startup output, prompt included, is part of the reply. It then returns, as plain text, the output that followed, once none has arrived for 500ms or after 10s, whichever comes first, holding at most the last 512 KiB of raw output while it waits. It types on its own goroutine one byte per write and stops between bytes once the call returns. A program in raw mode that stops reading blocks a PTY write, and the PTY's file doesn't support deadlines, so typing one byte at a time keeps the call within its limits while leaving at most one byte to arrive late. The reply then says how many bytes were typed. Typing from an agent doesn't count as the human acting (`last_action_at`). The plain text comes from `terminal.Text` (`internal/terminal/text.go`). It replays the output one line at a time, keeping track of the cursor column, so a carriage return overwrites the line (progress bars, zsh's `\r\r\n` after a command) instead of duplicating it, and a backspace moves the cursor back. It applies erase-to-end-of-line (`CSI K`) and drops every other escape sequence and control character. It keeps the last 64 KiB, cut at a line start. Full-screen programs come out garbled, since it isn't a screen emulator.
|
||||
|
||||
A `terminal_type` whose text doesn't end in a newline (`\n` or `\r`) leaves a line for the human to run, and wakes its agent when the shell next returns to its prompt. A zsh terminal gets shell integration for this: `terminal.New` writes a `.zshenv` (the `zshenv` constant in `internal/terminal/terminal.go`) into `<data>/zsh`, and the shell starts with `ZDOTDIR` pointing there and the user's own `ZDOTDIR` (empty when unset) in `BYRE_ZDOTDIR`. That `.zshenv` puts `ZDOTDIR` back (unsetting it when it was empty), sources the user's `.zshenv`, and in an interactive shell appends a `precmd` hook that prints `ESC ] 133 ; D ; <exit status> BEL` (the OSC 133 "command finished" mark) before every prompt; the rest of the user's startup files then load from their own `ZDOTDIR` as usual. Terminals ignore OSC sequences they don't know, and `terminal.Text` drops them, so nothing shows. Other shells get no integration (`Terminal.Prompts` is false), and nothing wakes their agents. The reply to such a `terminal_type` ends with a line saying which: byre will message the agent, or, in another shell, the agent must ask the human to say when they have run it. Once all of the text is typed, `Terminal.WakeOnPrompt` arms a one-shot wake that collects the output from then on and fires at the first mark; every `terminal_type` first cancels any armed wake (`CancelWake`), so the agent pressing Enter itself, or typing more, doesn't wake it. A zsh line editor runs `precmd` after Enter on an empty line and after Ctrl-C too (exit status 130), so either wakes the agent. On the mark, byre sends that agent's session a `byre` message (`Manager.wakeOnPrompt`, through `Manager.Message`, so it starts a turn when idle, reaches it mid-turn, and is held while the human has interrupted the agent) with the exit status and the output as plain text. The log records `woke agent on terminal prompt` with the session, task and exit status.
|
||||
|
||||
For commands that don't need the human to see them, agents have the `bash` MCP tool (`internal/agents/bash.go`), for agents whose own shell tool runs in a sandbox that gets in the way. byre runs `$SHELL -l -c <command>` itself, as the service user and outside the agent's sandbox, in the task's worktree with byre's environment plus the task's `PORT`, stdin empty, stdout and stderr into one buffer, in its own process group. The group gets `SIGKILL` after `timeout_ms` (default 120000, at most 600000) or when the call is cancelled (in a Claude session, an MCP call's context ends when Claude sends `control_cancel_request` for it, as on an interrupt, or when the session's output ends), and the reply says `killed after <timeout>`; otherwise it starts with the exit status (`exit status 1`, `signal: killed`). The output is the last 64 KiB, as raw text. Background children holding the output open are waited for at most 5s after the command exits, and the reply then says the output was cut off.
|
||||
|
||||
The **Terminal** tab (`mac/Sources/Byre/Terminal.swift`) draws it with SwiftTerm's `TerminalView`. The terminal uses the UI font and is coloured from the palette (background, text, accent cursor, accent-tint selection); ANSI colours are SwiftTerm's defaults. The colours are set once per light/dark scheme (`TerminalSession.scheme`), never on other SwiftUI updates, since each colour set makes SwiftTerm redraw the whole screen and the tab re-renders on every task change in the store. It fits the pane and resizes with it. Opening the tab, or selecting another task while it is open, opens one socket for the selected task; switching tab or task closes it, and the shell keeps running, so coming back replays its recent output. A socket that drops for any other reason reconnects after 2s while the task is active; after a byre restart that starts a new shell. When the shell exits the header shows `shell ended: <status>` and a **New shell** button; when it can't be started (the socket closes with 1011, the error in byre's log as `start terminal`), the header says so, with the same button to try again. Ctrl key combinations typed in the terminal go to the shell, not to byre's Ctrl-K, Ctrl-P, Ctrl-L and Ctrl-S shortcuts; ⌘K, ⌘P, ⌘L and ⌘S still work.
|
||||
|
||||
Reference in New Issue
Block a user