* feat(web-shell): per-task token & time detail on completed todos
Expanding a completed task in the todo list now reveals when it ran (start /
end / duration) and what it spent: input / output / cached tokens, API time,
and tool time.
The agent stamps a cumulative-usage snapshot onto each todo (plan) update via
`_meta.stats`; the SDK normalizer carries it into the TodoWrite tool call's
rawOutput, and the web-shell diffs consecutive snapshots for tokens and API
time while summing transcript tool durations for tool time.
Works live (no polling race) and on /resume: tokens and tool time are
reconstructed from persisted usage metadata, while API time is live-only since
per-turn durations are not replayed. Sessions whose agent never stamped a
snapshot degrade gracefully to start/end + tool time.
* refactor(web-shell): show task duration inline on the end-time row
Trail the elapsed duration after the end time as a dimmed parenthetical
("12:34:15 (4m 14s)") instead of a separate row, since it's derived from the
start/end pair.
* fix(web-shell): correct per-task detail for reused todo ids; review follow-ups
- computeTodoDetails: when a completed id+content key restarts as in_progress (positional plan-N ids repeat across plans), reset the window so the new task diffs its own start instead of the prior task's far-earlier boundary — which rendered a cross-plan window with wildly inflated token/time numbers. Correct the todoStateKey JSDoc accordingly.
- Tool time: sort spans once and binary-search the task window instead of an O(todos x spans) scan per completed task.
- Tests: add the reuse-reset case, a windowed tool-time case, an SDK-normalizer -> extractTodoStats contract test (locks the stats passthrough so a field rename fails loudly), and a stopPropagation test (expander click must not bubble to the tool-row header).
* fix(web-shell): reset todo detail window on reopen via pending, not just direct
Track keys that have ever reached 'completed' instead of checking prev === 'completed': a reopened task can pass through 'pending' (completed → pending → in_progress), where prev at the re-activation is 'pending' and the direct check missed it, leaving a stale baseline that diffs across both runs. A pause/resume that never completed (in_progress → pending → in_progress) still keeps its first baseline, so its diff captures the whole task.
* fix(web-shell,cli): harden todo stats against NaN poisoning and partial snapshots
- MessageEmitter: only fold finite usage/duration values into the cumulative accumulator. A NaN/Infinity (incl. a NaN that survives `?? 0`) would poison the running total forever, making every later snapshot fail extractTodoStats and silently show 'not captured' for the rest of the session.
- extractTodoStats: require the token fields but default the live-only apiTimeMs to 0 when absent/non-finite, so a snapshot that omits it keeps its valid token counts instead of being dropped whole.
- computeTodoDetails: gate the start baseline on the stored value, not Map.has — a stats-less start (e.g. a plain plan message) recorded undefined, which Map.has treated as already-set, blocking a later stats-bearing snapshot from upgrading the baseline.
- Document the MessageEmitter-before-PlanEmitter ordering invariant in both emitters.
* feat(web-shell): collapsible TodoWrite history with status diff
Inline todo_write updates rendered as a generic, non-collapsible tool row
crammed in with surrounding tool calls — the todo-specific renderer was
effectively dead code because the detector matched the literal "todowrite"
while the wire name is "todo_write" (kind "think").
- Detect the todo tool by name (todo_write / todowrite) instead of relying
on the unrelated tool kind, reviving the rich rendering and also
populating the floating todo panel that was empty on the daemon path.
- Render each update as its own standalone group, collapsed by default to
the per-snapshot diff (just-completed / just-started items) or the current
step, expanding to the full checklist; the header shows completed/total.
- Use consistent status glyphs (●/◐/○) in both collapsed and expanded views
via a shared TodoView component used by ToolGroup and PlanMessage.
* fix(web-shell): scope todo diff per task identity; address review
- [Critical] computeTodoTimeline keyed its running state on todo.id alone, but
ids aren't globally unique (ACP assigns positional ids, models renumber per
plan), so a later plan diffed against a previous plan's stale terminal status
and silently dropped events in the collapsed view. Key on id+content so
distinct tasks stay separate; add an id-reuse regression test.
- Stabilize the TodoTimelineContext value with a signature-cached Map so
streaming ticks that don't touch a todo snapshot no longer re-render every
todo/plan row.
- Drop the unused completed/total from TodoSnapshotDiff (consumers compute the
count locally — single source of truth).
- Fall back to the raw result summary when a todo_write payload is unparseable.
- Add PlanMessage rendering tests and extractTodosFromToolCall coverage; remove
stale "step time" comments left after dropping per-step timing.
* test(web-shell): document todo-diff keying limits; cover signature
Follow-up to review on the id+content keying in computeTodoTimeline:
- Document the two rare trade-offs in the todoStateKey doc (a mid-task reword on
a stable id, and unrelated plans reusing both id and content), both degrading
to "the collapsed diff omits one event" while the expanded list stays correct.
- Pin behavior with tests: an item carried over and completed in a later turn
(which id+content handles but a user-turn reset would drop), plus the two
documented gaps.
- Add todoTimelineSignature tests: stable across non-todo edits; changes on any
id/status/content change.
* refactor(web-shell): isolate plan context read; expand todo test coverage
Address follow-up review:
- Extract PlanEventSummary as the sole TodoTimelineContext consumer, mirroring
ToolGroup's TodoToolBody so the memo-shielded PlanMessage stays stable when
the timeline Map reference changes.
- Note the spurious-`started` axis of the reword trade-off in the todoStateKey
doc (a reword can drop a completion or emit a stray start).
- Add direct isTodoWriteToolName tests (incl. the `todowrite` ACP variant) and
a todoTimelineSignature empty-transcript test.