What You See
During parallel execution, the terminal renders a multi-line display updated every 100ms:Display Elements
Per-Agent Status Icons
Internal Architecture
The live progress system uses 3 independent goroutines communicating via shared state protected by a mutex:Detailed Flow
1
Initialization
agent_mode.go creates an AgentProgressState with N slots (one per agent) and a buffered progressCh channel with capacity Nร2.2
Consumer Goroutine
A dedicated goroutine consumes events from
progressCh and updates the shared state via Mark*() methods protected by a mutex.3
Timer Display
The
turnTimer starts with a callback that, every 100ms:- Clears previous terminal lines (
ClearLines) - Reads the
AgentProgressState(acquiring the mutex) - Renders the updated multi-line display
4
Dispatch with Events
DispatchWithProgress() executes agents and sends AgentEvent on the channel as each agent starts and finishes.5
Finalization
After all agents complete, the timer stops, the display is cleared, and final results are rendered as timeline cards.
Event Types
The dispatcher emits three event types via the progress channel:Thread Safety
AgentProgressState is accessed concurrently by two goroutines:
- Consumer goroutine โ writes (via
MarkStarted,MarkCompleted,MarkFailed) - Timer goroutine โ reads (via
FormatDispatchProgress)
sync.Mutex:
FormatDispatchProgress also acquires the mutex and takes a complete state snapshot before formatting the output string.
Terminal Rendering
The multi-line display uses ANSI escape sequences to update the terminal in-place:
On each timer tick (100ms), the callback:
- Emits
ClearLines(prevLines)to erase the previous display - Calls
FormatDispatchProgress()which returns the updated multi-line string - Prints the new string
- Updates
prevLinesfor the next cycle
The maximum latency between a state change event and its display in the terminal is 100ms โ imperceptible to the user.
Interaction with Policy Prompts
When a worker needs security approval (policy โaskโ), the system:- Pauses the timer โ the progress display stops updating
- Shows the security prompt โ with agent context
- After response โ resumes the timer and the display continues updating
Per-call spinner labels (DescribeCall)
The spinner line uses each pluginโsDescribeCall(args) method when available, surfacing the concrete target of the operation instead of a generic label. Comparison:
Legacy plugins that donโt implement
DescriberWithInput keep showing the fallback RUNNING: <tool> <subcmd> (locale-resolved, becomes EXECUTANDO: in pt-BR).
The action/multimodal tools (@send, @moa, @osv, @session, @speak, @image, @skill) implement DescribeCall, so the box shows a concise label (e.g. ๐จ Generating image: a watercolor fox) instead of the toolโs long description.
Animated spinner during execution
Tools that wait on network I/O (@moa, @image, @speak, @webfetch, @websearch, @osv, @sendโฆ) used to show a static box โ it felt frozen. The agent loop now runs the animated braille spinner (โ โ โ นโฆ) while the tool executes, labeled with its DescribeCall text. Behavior:
- Blocking tool (network): the spinner animates until it returns.
- Streaming tool (e.g.
@coder exec): the spinner stops on the first output line and the stream flows normally (no clash with the carriage-return repaint). - Outside a TTY (gateway/daemon) the spinner is suppressed automatically.
Mid-turn message queue indicator
During an agent turn the spinner displays(N queued) when the user has enqueued messages. The counter sums:
- Completed lines (Enter pressed) still pending drain into
messageQueue - Lines already in
messageQueuewaiting for the next turn
/agent and /coder (previously only /agent).
Full Lifecycle
Code Components
Comparison: Before vs After
- Before (Static Spinner)
- After (Live Progress)