This repository was archived by the owner on Sep 4, 2026. It is now read-only.
feat(chat): let the working row say what is running and for how long - #392
Merged
Conversation
The turn-level half of #365. Mid-turn the chat showed a bare `WORKING` dot and nothing else — during a four-minute `lanes_wait` there was no way to tell thinking from blocked on a tool from dead. **Two message kinds were arriving and being dropped.** The bridge forwards every `SDKMessage` verbatim and `claude_bridge.rs` re-serialises it untouched, so nothing was lost before the parser; the dispatch in `claude_stream.rs` simply handled only `system`/`stream_event`/`assistant`/`user`/`result`: - `tool_progress` — `{tool_use_id, elapsed_time_seconds}`, emitted as a heartbeat *while a tool executes*. This is an authoritative clock from the source. There was no timing field anywhere in the pipeline, so the alternative was a client-side timer that drifts and lies across a slow frame. - `system` / `subtype: "status"` — the SDK's own `compacting` / `requesting`. The status branch is checked BEFORE `handle_system`'s init guard, which returns early once the session has started — otherwise every status after init is silently dropped. That has its own test. Both are **live-only**: `run_agent_event` records them alongside `ProviderPayload` as never-persisted. They describe a turn while it runs and are meaningless replayed from history, where the tool has finished and its real duration is whatever the completion recorded. The store tracks the running tool by matching the heartbeat's `itemId` against the timeline row that started it, so the row names itself — a tool by name, an MCP call as `server/tool`, a command by its command line. An unmatched id is ignored rather than shown as a nameless clock, since other providers never emit this at all. Activity clears on every turn boundary, including `turnFailed`. `WorkingRow` renders the name and elapsed time on the row that already exists rather than adding a badge, and the accessible name carries both. Behind the default-off `turnActivity` flag. Refs #365
12 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The turn-level half of #365 — its headline complaint, which #387 did not address.
Mid-turn the chat showed a bare
WORKINGdot and nothing else. During a four-minutelanes_waitthere was no way to tell thinking from blocked on a tool from dead.Two message kinds were arriving and being dropped
The bridge is a pure pass-through —
scripts/claude-bridge.tsforwards everySDKMessageverbatim andclaude_bridge.rsre-serialises it untouched. Nothing was lost before the parser. The dispatch inclaude_stream.rssimply handled onlysystem/stream_event/assistant/user/result:tool_progress—{tool_use_id, elapsed_time_seconds}, emitted as a heartbeat while a tool executes. This is an authoritative clock from the source. There was no timing field anywhere in the pipeline, so the only alternative was a client-side timer, which drifts and lies across a slow frame or a reconnect.system/subtype: "status"— the SDK's owncompacting/requesting.The status branch is checked BEFORE
handle_system's init guard, which returns early once the session has started. Put it after and every status following init is silently dropped — that has its own test, because it is exactly the bug this change could have shipped.Live-only, deliberately
run_agent_eventrecords both alongsideProviderPayloadas never persisted. They describe a turn while it runs; replayed from history they are meaningless — the tool has finished, and its real duration is whatever the completion recorded. The exhaustive match inmanager.rsforced that decision rather than letting it default.Naming the row
The store matches the heartbeat's
itemIdagainst the timeline row that started it, so the row names itself:toolUsemcpToolCallserver/toolcommandAn unmatched id is ignored, not shown as a nameless clock — other providers never emit this at all. Activity clears on every turn boundary including
turnFailed; a test pins that, and it caught a real gap where my first pass missed the inline turn-close paths.WorkingRowrenders name and elapsed on the row that already exists rather than adding a badge (the standing readouts-need-an-edge rule), and the accessible name carries both. Elapsed formats as12s/2m 05s— it only formats, it never counts.Behind the default-off
turnActivityflag;flags.test.tsenumerates the registry so it is asserted, not silently added.Tests
Rust (5) and store (6), all verified load-bearing:
tool_progress→ elapsed; missing id or clock ignoredsystem/status→ activity; a status frame after init still surfaces; empty status ignoredRemoving the reducer case fails 3 store tests; the Rust suite is 32 passing in
claude_stream.Validation
cargo test -p pickforge-core claude_stream— 32 passedcargo clippy -p pickforge-core --all-targets -- -D warnings— cleanbun run test:unit— 1616 passedbunx tsc --noEmitclean;bun run lintcleanWhat still remains on #365
tool_progress, so the working row has unit coverage but no snapshot. The mock emits a completed history, not a mid-flight turn — adding that is its own fixture work.input_json_delta) and subagent attribution (parent_tool_use_id) remain deferred, as the issue says.Refs #365