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:
โฎ [claude-sonnet-4-6] [14s] [โโโโโโโโโโโโโโโโโโโโ] 2/4 agents (50%)
โ [file] Read all .go files in pkg/coder/engine/ โ completed (2.1s)
โฏ [search] Find references to handleRead and... โ running...
โ [shell] Run lint on modified files โ failed (1.3s) exit code 1
โ [coder] Refactor read/write separation โ pending
Display Elements
| Element | Description |
|---|
Spinner (โฎ โฏ โฐ โฑ) | Rotating animation confirming the process is active |
[model] | LLM provider/model used by workers |
[time] | Total elapsed time since dispatch started |
| Progress bar | Visual representation of completion percentage |
N/M agents (X%) | Count and percentage of finished agents |
Per-Agent Status Icons
| Icon | Status | Description |
|---|
โ | Pending | Agent waiting for semaphore slot (max workers reached) |
โฏ | Running | Agent actively executing (animated spinner) |
โ | Completed | Agent finished successfully (shows duration) |
โ | Failed | Agent finished with error (shows duration + message) |
Internal Architecture
The live progress system uses 3 independent goroutines communicating via shared state protected by a mutex:
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Worker Goroutines (Nร) โ Dispatcher goroutines
โ executeAgent() โ Each agent runs isolated
โ โ
โ On start/finish โโโโโ sends AgentEvent โโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Consumer Goroutine โ
โ for evt := range โ
โ progressCh โ
โ โ
โ MarkStarted() โ
โ MarkCompleted() โโโโโ writes โโโโโโ
โ MarkFailed() โ (with mutex) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ AgentProgressState โ
โ Timer Goroutine โ โ (thread-safe) โ
โ Ticker every 100ms โโโโโ reads state โโโโโโโโโโโโโโโโโโโโโโโบโ โ
โ โ and renders โ Agents[0]: Running โ
โ FormatDispatchProgress โโโโโโ snapshot with mutex โโโโโโโโโโโโโโโ Agents[1]: Complete โ
โ () โ โ Agents[2]: Failed โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ โ Agents[3]: Pending โ
โโโโโโโโโโโโโโโโโโโโโโโโ
Detailed Flow
Initialization
agent_mode.go creates an AgentProgressState with N slots (one per agent) and a buffered progressCh channel with capacity Nร2.
Consumer Goroutine
A dedicated goroutine consumes events from progressCh and updates the shared state via Mark*() methods protected by a mutex.
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
Dispatch with Events
DispatchWithProgress() executes agents and sends AgentEvent on the channel as each agent starts and finishes.
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:
type AgentEventType int
const (
AgentEventStarted AgentEventType = iota // agent started executing
AgentEventCompleted // agent finished successfully
AgentEventFailed // agent finished with error
)
Each event carries full context:
type AgentEvent struct {
Type AgentEventType
CallID string // unique agent call ID (ac-1, ac-2...)
Agent AgentType // agent type (file, coder, shell...)
Task string // task description
Duration time.Duration // execution time (only on Completed/Failed)
Error error // error (only on Failed)
Index int // position in batch (0-based)
Total int // total agents in batch
}
Thread Safety
AgentProgressState is accessed concurrently by two goroutines:
- Consumer goroutine โ writes (via
MarkStarted, MarkCompleted, MarkFailed)
- Timer goroutine โ reads (via
FormatDispatchProgress)
Synchronization is handled by an internal sync.Mutex:
type AgentProgressState struct {
mu sync.Mutex
Total int
Agents []AgentSlot
StartTime time.Time
}
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:
| Operation | Escape Sequence | Description |
|---|
| Clear line | \r\033[K | Return to start and erase line |
| Move up N lines | \033[A\033[K (รN) | Move cursor up and clear each line |
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
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:
- 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
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:
| Plugin | Before | After |
|---|
@read | RUNNING: @read read | Reading: main.go |
@search | RUNNING: @search search | Searching: golang errgroup |
@webfetch | RUNNING: @webfetch fetch | Fetching: https://api.example.com/... |
@coder exec | RUNNING: @coder exec | Executing: go test ./... |
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.
โ claude-opus [1m12s] | Processing... (1 queued)
Works in both /agent and /coder (previously only /agent).
Full Lifecycle
[Before dispatch]
โญโโ ๐ MULTI-AGENT DISPATCH
โ Dispatching 4 agents
โฐโโโโโโโโโโโโโโโโโโโโโโโโ
โญโโ ๐ค [file] #1
โ Read all .go files in pkg/coder/engine/
โฐโโโโโโโโโโโโโโโโโโโโโโโโ
โญโโ ๐ค [search] #2
โ Find references to handleRead and handleWrite
โฐโโโโโโโโโโโโโโโโโโโโโโโโ
[During dispatch โ updated every 100ms]
โฎ [claude-sonnet-4-6] [3s] [โโโโโโโโโโโโโโโโโโโโ] 0/4 agents (0%)
โฏ [file] Read all .go files in pkg/coder/... โ running...
โฏ [search] Find references to handleRead... โ running...
โ [shell] Run lint on modified files โ pending
โ [coder] Refactor read/write separation โ pending
... 5 seconds later ...
โฐ [claude-sonnet-4-6] [8s] [โโโโโโโโโโโโโโโโโโโโ] 2/4 agents (50%)
โ [file] Read all .go files... โ completed (2.1s)
โ [search] Find references... โ completed (4.8s)
โฏ [shell] Run lint... โ running...
โฏ [coder] Refactor read/write... โ running...
... finished ...
[After dispatch โ result cards]
โญโโ โ
[file] OK (2.1s, 3 tool calls)
โ Files read: engine.go, reader.go, writer.go
โฐโโโโโโโโโโโโโโโโโโโโโโโโ
โญโโ โ
[search] OK (4.8s, 5 tool calls)
โ Found 12 references across 4 files
โฐโโโโโโโโโโโโโโโโโโโโโโโโ
โญโโ โ [shell] FAILED
โ exit code 1: unused variable 'tmp' in engine.go:42
โฐโโโโโโโโโโโโโโโโโโโโโโโโ
โญโโ โ
[coder] OK (12.3s, 8 tool calls, 3 parallel)
โ Refactored read/write into separate files
โฐโโโโโโโโโโโโโโโโโโโโโโโโ
โญโโ ๐ SUMMARY
โ 3/4 agents completed | 16 tool calls executed | 3 parallel goroutines | 14.1s total
โฐโโโโโโโโโโโโโโโโโโโโโโโโ
Code Components
| Component | File | Responsibility |
|---|
AgentEvent / AgentEventType | cli/agent/workers/types.go | Progress event types |
DispatchWithProgress() | cli/agent/workers/dispatcher.go | Dispatcher that emits events via channel |
AgentProgressState | cli/metrics/display.go | Thread-safe state for each agent |
FormatDispatchProgress() | cli/metrics/display.go | Multi-line display rendering |
ClearLines() | cli/metrics/display.go | Terminal line clearing |
| Integration | cli/agent_mode.go | Consumer + timer + dispatch orchestration |
Comparison: Before vs After
Before (Static Spinner)
After (Live Progress)
โฎ [claude-sonnet-4-6] [32s] Aguardando 4 agents...
(No information about:
- Which agent is running
- Which already finished
- Whether there was an error
- How much is left)
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.โฐ [claude-sonnet-4-6] [14s] [โโโโโโโโโโโโโโโโโโโโ] 2/4 agents (50%)
โ [file] Read all .go files... โ completed (2.1s)
โ [search] Find references... โ completed (4.8s)
โฏ [shell] Run lint... โ running...
โ [coder] Refactor read/write... โ pending
The user sees in real time:
- Progress bar with percentage
- Individual status of each agent
- Duration of each completed agent
- Immediate errors with message
- Total elapsed time