feat(work-status): add opt-in turn statistics (#3177)

Add optional completed-turn statistics without changing the existing panel layout. Separate final text delivery speed from whole-turn throughput, preserve scope and opt-in settings, and explain each metric with localized delayed tooltips.

Validated focused telemetry, lifecycle, sync and persistence tests, all-workspace type-check and lint, web builds, the 12-locale narrow layout, and full GitHub CI.
This commit is contained in:
alvins82
2026-09-06 22:56:27 +03:00
committed by GitHub
parent b0282b2720
commit d8215ef5b3
36 changed files with 1404 additions and 31 deletions
@@ -17,7 +17,8 @@ conditionally; passing "am I first?" down would mean each one tracking what the
sections above it decided to render.
Sections render nothing when they have no rows, so the panel collapses upward
instead of reserving empty space.
instead of reserving empty space. Opt-in Turn stats keeps its header for a
selected session even without metrics, so a saved collapsed state can reopen.
## What it is not
@@ -103,11 +104,49 @@ which requests only providers enabled for this panel.
| Subagent blockers | directory `permission` / `question` maps | one subscription covers every child |
| Usage | `components/usage/usageGroups.ts` over `useQuotaStore` | grouping shared with the mobile popover; presentation is not |
| Linked threads | `lib/linkedIssues.ts` over session metadata | written by the flows that attach an issue or PR |
| Turn stats | `telemetry.ts` over `useSessionMessageRecords` | opt-in; computed only while expanded and authoritatively idle |
| Goal | `useSessionGoal` | respects the Settings toggle |
| MCP | `useMcpStore` | connect/disconnect reuses the dropdown's actions |
| Pinned messages | `getContextObligatoryMessages` + `state.part` | see below |
| Todos | live `state.todo[sessionId]`, persisted fallback | live channel wins |
### Turn stats
The section follows Usage and reuses the panel's existing rows. Only its header
has an icon; metric rows use labels and values without leading icons. It
reads already-loaded records without fetching history. The newest turn needs a
preceding user message and completed assistant steps. A truncated or unfinished
turn has no whole-turn result; later materialization can supply it.
Two rates answer different questions. Response speed uses the final assistant
message's output tokens divided by the union of its nonempty text intervals.
It excludes initial waiting, reasoning tokens and reasoning time, and earlier
tool steps. It requires complete, valid text timing and a final message without
tools, errors, synthetic text or ignored text. This measures text delivery from
stored timestamps, not provider-side decode speed. The header shows only this
rate; unavailable response timing never falls back to whole-turn speed.
Whole-turn speed uses output plus reasoning tokens from every step, divided by
elapsed assistant time minus the union of completed and failed tool intervals.
Waiting for each model response remains included. Invalid or missing inputs
omit the dependent metric rather than becoming zero; reported zeros remain
valid. TTFT averages the earliest text/reasoning start delay from every step,
only when all steps have a valid sample.
Metric labels stay short. Every row is a single hover and keyboard-focus target
for a shared tooltip, with a 750ms hover delay and a portal outside the panel's
scroller. Tooltips explain the measurement in every locale. The token row uses
compact input/output arrows; its tooltip gives full counts and explains that
input excludes cached tokens and output includes reasoning across all steps.
Records subscriptions and aggregation stop while collapsed, busy, retrying, or
awaiting status authority. Explicit idle events or a successful directory status
snapshot allow computation. One component-owned committed result keeps the
headline and rows stable during the next active turn. Its identity includes
runtime, normalized directory and session. Scope changes discard it; fresh empty
or reverted records clear it. There is no global message-ID cache. Corrections
to existing message/part identities invalidate the current result.
### Context usage has its own computation, on purpose
`useSessionUIStore.getContextUsage` cannot serve this panel for two reasons:
@@ -191,8 +230,9 @@ the row reflects the reset tree rather than a mid-creation snapshot.
Ordering is by durability, not category:
1. **Session** (goal, context, cost), **Project** (attention, branch,
changes, PR, checks) and **Usage** — true for as long as the session is
open. Usage sits here rather than lower down because a spent quota stops the
changes, PR, checks), **Usage**, and **Turn stats** (opt-in session telemetry:
throughput, duration, TTFT, cache hit rate) — true for as long as the session
is open. Usage sits here rather than lower down because a spent quota stops the
work outright;
2. **Subagents**, **Tasks** — what is happening right now;
3. **MCP**, **Pinned messages**, **Context sources** — supporting material.
@@ -201,10 +241,13 @@ Ordering is by durability, not category:
A persisted preference (`workStatusPanelEnabled`) drives a header toggle, and a
dialog behind the equalizer icon switches individual sections off. Hidden
sections are stored rather than visible ones, so a section added later appears
for everyone instead of staying invisible to whoever had saved settings before
it existed. Both travel the full settings pipeline, including the server
whitelist without which the keys never reach `settings.json`.
sections are stored rather than visible ones. Telemetry is the opt-in exception:
UI-store v20 and legacy server-list hydration add it to the hidden set. A
`workStatusHiddenSectionsExplicit` marker records that a list was chosen in a
client with telemetry support. The marker and list travel together through
autosave, sanitization, and server settings, so an explicit empty list enables
everything while an old empty list does not enable telemetry. Complete settings
snapshots own this preference; unrelated partial save echoes leave it unchanged.
`workStatusPanelVisible` is separate and transient: the switch can be on while
layout still refuses the panel. The header and the git rail read it to drop the