ADR-0015: TUI-native permission prompts via blocking goroutine + channel¶
Date: 2026-05-14
Status¶
Accepted
Context¶
ADR-0013 wired permission prompts through an inputReader interface backed by stdinInputReader — a bufio.Scanner reading from os.Stdin. This worked in the original readline REPL because the TUI was suspended during agent execution (tea.Exec), so stdin was available.
With the full TUI (ADR-0014), tea.Exec is gone and the alt-screen is always live. The agent runs in a goroutine. Reading from os.Stdin inside that goroutine deadlocks: bubbletea owns the terminal and is reading from stdin itself; the agent goroutine never receives input.
Decision¶
Replace stdinInputReader in interactive mode with tuiInputReader, which routes permission prompts through the bubbletea message queue:
Mechanism¶
- The agent goroutine calls
tuiInputReader.readLine(prompt). readLinecreates achan stringand callsp.Send(permRequestMsg{prompt, respCh}), then blocks on<-respCh.UpdatehandlespermRequestMsg: stores it asmodel.pendingPerm, appends the prompt text to the transcript, and resets the textarea.- While
pendingPerm != nil, key events are routed tohandlePermKeyinstead of the normal handler. Enter sendsm.ta.Value()torespChand clearspendingPerm. Ctrl-C sends"n". - The agent goroutine unblocks, receives the answer, and continues.
Why blocking the goroutine is acceptable¶
The agent goroutine is already blocking on I/O (Claude subprocess or llama.cpp HTTP). Blocking it additionally on a channel does not consume a thread — Go's goroutine scheduler parks it. The TUI remains responsive throughout.
Why not a second message type for the response¶
Sending a permResponseMsg from Update back to the goroutine would require a shared channel stored on the model, which is copied on every Update call. A channel on a pointer field of the model survives copies; the permRequestMsg itself carries the response channel, so no model field is needed.
Consequences¶
- Permission prompts appear inline in the transcript, consistently with all other output.
- The TUI remains live and responsive during permission prompts — the spinner continues, the viewport is scrollable.
- The agent goroutine is parked (not spinning) while waiting for user input.
stdinInputReaderis still used for single-shot (non-REPL) mode where the TUI is not running.- If the user force-quits (Ctrl-C twice) while a prompt is pending, the goroutine leaks briefly until the context is cancelled by the program exit. This is acceptable.