Skip to main content
When the dispatcher executes multiple agents in parallel, the terminal displays a real-time progress panel showing the state of each individual agent. This eliminates the uncertainty of โ€œis it still running?โ€ without having to check logs.

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:
  1. Clears previous terminal lines (ClearLines)
  2. Reads the AgentProgressState (acquiring the mutex)
  3. 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:
Each event carries full context:

Thread Safety

AgentProgressState is accessed concurrently by two goroutines:
  1. Consumer goroutine โ€” writes (via MarkStarted, MarkCompleted, MarkFailed)
  2. Timer goroutine โ€” reads (via FormatDispatchProgress)
Synchronization is handled by an internal sync.Mutex:
Every public method acquires the mutex before reading or writing. FormatDispatchProgress also acquires the mutex and takes a complete state snapshot before formatting the output string.
The progressCh channel uses a buffer of Nร—2 (where N = number of agents) to prevent workers from blocking when sending events if the consumer is momentarily busy.

Terminal Rendering

The multi-line display uses ANSI escape sequences to update the terminal in-place: On each timer tick (100ms), the callback:
  1. Emits ClearLines(prevLines) to erase the previous display
  2. Calls FormatDispatchProgress() which returns the updated multi-line string
  3. Prints the new string
  4. Updates prevLines for 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:
  1. Pauses the timer โ€” the progress display stops updating
  2. Shows the security prompt โ€” with agent context
  3. After response โ€” resumes the timer and the display continues updating
This prevents the spinner and security prompt from overlapping in the terminal.

Per-call spinner labels (DescribeCall)

The spinner line uses each pluginโ€™s DescribeCall(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 messageQueue waiting for the next turn
Result: pressing Enter while the LLM streams updates the indicator immediately instead of waiting for the turn to close.
Works in both /agent and /coder (previously only /agent).

Full Lifecycle


Code Components


Comparison: Before vs After

The user only knew something was happening because the spinner was rotating. To confirm agents were actually running, they had to open ChatCLI logs in another terminal.