[BUG] Desktop app workflow panel: per-agent tokens/tools/time stay empty during long parallel runs — progress-only batches omit workflowProgress
What's Wrong?
In the Claude Desktop (macOS) app, the Workflow progress panel shows the header aggregates updating live (e.g. 4 agents · 657.0k tokens, elapsed 10m 42s), but the per-agent table below it — the tokens / tools / time columns — stays completely empty for the entire time the agents are running. Agent labels render fine; the metric cells never populate until an agent finishes.
This reliably happens with long-running parallel fan-outs: a Workflow run that spawns N agents in pure parallel() where each agent runs for many minutes and the script calls no log()/phase() in between. In my case: 4 agents, all running 10+ minutes → all 4 rows empty for 10+ minutes while the header token total kept climbing.
The terminal TUI (/workflows in CLI) is not affected — same run shows live per-agent tokens/tools there.
What Should Happen?
Per-agent tokens/toolCalls (and elapsed time) should update live in the desktop app's workflow panel, matching what the terminal TUI shows for the same run.
Steps to Reproduce
- In Claude Code inside the Claude Desktop macOS app, run a Workflow that spawns several long-running agents concurrently, e.g.:
``js`
const results = await parallel(PROMPTS.map(p => () => agent(p, {schema: S})))
log()` calls while they run.
with prompts heavy enough that each agent runs for several minutes, and no
- Open the workflow panel in the app.
- Observe: header shows
N agents · X tokensupdating live, but every agent row's tokens/tools/time cells are empty until an agent reachesdone/error.
Root Cause Analysis
Recovered from the shipped CLI binary (bun-compiled; identifiers below are minified and vary per build). Verified identical logic in 2.1.205 (bundled in the desktop app) and 2.1.206 (standalone, latest as of 2026-07-10).
The workflow runner tracks per-agent metrics correctly — each agent's stream loop emits progress ticks with live counters, and durationMs only on completion:
Ie("progress", { tokens: sr + Tn, toolCalls: ht + hn })
// on completion:
Ie("done", { tokens: sr + Tn, toolCalls: ht + hn, durationMs: wt + si, resultPreview: ... })
// on start (no tokens unless journal-resume respawn):
Ie("start", sr || ht ? { tokens: sr, toolCalls: ht } : void 0)
The in-process reducer merges these into workflowProgress[] and sums totalTokens/totalToolCalls. The bug is in the bridge that forwards state to the host app (onSdkEmit):
onSdkEmit: (q) => {
let G = q.filter(obd);
if (G.length === 0) return;
let U = S.getAppState()?.tasks?.[t];
if (U?.type !== "local_workflow" || U.status !== "running") return;
let $ = G.findLast((W) => W.type === "workflow_agent"),
j = G.every((W) => W.type === "workflow_agent" && W.state === "progress"); // ←
XKt({
taskId: t, toolUseId: u, description: ..., startTime: _.startTime,
totalTokens: U.totalTokens, toolUses: U.totalToolCalls,
lastToolName: $?.label, summary: m,
workflowProgress: j ? void 0 : U.workflowProgress.filter(obd) // ← elided
})
}
If the emitted batch consists solely of workflow_agent events with state === "progress", the per-agent list (workflowProgress) is set to undefined and only the header aggregates go out. The row list is only re-sent when a batch contains a start/done/error, workflow_log, or workflow_phase event.
Consequences:
- The
state:"start"snapshot (the last full list the app receives) carries notokens/toolCallsyet, anddurationMsonly ever appears ondone. - So in any window where all agents are mid-flight and nothing starts/finishes and no
log()fires, every batch is progress-only → the host UI never receives a row list with populated metrics → the table stays frozen at the empty start snapshot. Header totals keep updating because they're always included. This matches the observed screenshot exactly. - It also explains the intermittent nature: in
pipeline()runs or staggered fan-outs, each start/done event flushes a full list (which includes the other running agents' current counters), so rows appear to update "sometimes". - Terminal TUI is unaffected because it renders the in-process
workflowProgressstate directly, not the elided bridge payload.
The data layer is intact — after completion, <session>/workflows/wf_*.json contains correct per-agent metrics:
{ "type": "workflow_agent", "label": "verify:plan", "state": "done",
"tokens": 178902, "toolCalls": 51, "durationMs": 769722, ... }
Suggested Fix
The elision is presumably a bandwidth optimization (don't re-serialize the whole list every tick). Keeping that intent, any of:
- Include
workflowProgressin progress-only batches on a throttle (e.g. at most once every 2–5 s), or - Send row-level diffs — only the
workflow_agententries whosetokens/toolCallschanged since the last emit, or - At minimum, include the full list whenever some row's counters changed materially since the last emitted list.
Since the omission is CLI-side, fixing it there fixes every SDK-stream host (desktop app, and any other embedder) at once.
Environment
- Claude Desktop (macOS): 1.20186.0, bundled Claude Code CLI 2.1.205
- Same logic verified in standalone CLI 2.1.206 (latest at time of filing)
- macOS 26.4.1 (arm64)
- Workflow: dynamic script, 4 agents via
parallel(), each running 10+ min, nolog()during the fan-out
Error Messages/Logs
N/A — no errors; this is a display/data-propagation gap. Header aggregates prove the event stream itself is healthy.