Decisions › D24
Every real action runs on a live terminal you can read, type into and stop
Context
While calibrating a moteus board, Zach wanted to see the terminal output in case there is debugging information, and to enter user input if a tool asks. Round 3 had asked the opposite for the default view: no streaming wall, a quiet line under the button. Both stand: the line stays quiet, and the output is one click away.
The decision
Every real action (never a dry run) is a run: the plugin's own command on a pseudo-terminal, with live output, typed input and Stop. Stop sends SIGINT to the plugin's process group, then SIGTERM and SIGKILL after 3 seconds each. The plugin's records leave the terminal on a second file descriptor named in an environment variable, so a wrapped vendor tool inherits the terminal, prints progressively and can prompt, while the structured result still reaches the host.
Output is held in a 2 MiB ring and written as it happens to an asciinema recording, with a 64 MiB total budget. The page streams it with Server-Sent Events as raw bytes, drawn by xterm.js. Each run adds a RunStarted and a RunEnded event to the activity log.
Why
“it would be cool though if I could perhaps see the terminal output or something, in case there is debugging information”
Zach Yamaoka, 24 Oct 2026
“the Claude model is good: you run this and then there is all the terminal's processes you can switch between.”
Zach Yamaoka, 24 Oct 2026
“the tulities are not the main thing and shouldn't be drawing attention”
Zach Yamaoka, notes, 23 Oct 2026
Options considered
Five page proposals draw against the same seam. Two are built; the default is the first.
| Proposal | State |
|---|---|
| r1 Terminal panel with tabs | Built, default |
| r2 Output in place | Built |
| r3 Runs as rows | Not built yet |
| r4 Runs in the Activity log | Not built yet |
| r5 A tmux session | Not built yet |
Consequences
- An old plugin that ignores the variable still works: the host reads the terminal's lines that parse as records.
- Finished runs persist, so a run's output can be replayed with any asciinema player.
- A dry run asked while a run holds the same exclusive group is refused at once, because a run can sit at a prompt for minutes.
Evidence
- Runs and Identify: captures from the appReport, 2026-10-24. Evidence for this decision.
- Runs v1: a live terminal for every utilityContract doc, 2026-10-24. The seam for runs: a real action on a pseudo-terminal, where records go, the output ring and recordings, Stop and the plugin's own stop.
- IO diagnostics v1: build planContract doc, 2026-10-22. The decisions, the plugin boundary (a subprocess speaking JSON lines), the lanes, and the interfaces frozen between them.
Commits in the app's repository (private):
bcbc8a92026-10-24 Runs v1 contract693cb802026-10-24 Runs, host sidef2c84c22026-10-24 Runs, page side: terminal panel with tabs8ed65042026-10-24 Runs: hero video and GIF recorded from the real page