When tc.snippet === tc.preview (common for no-progress tools where
both are set to the same result_snippet), the detail block would show
identical content as the header. Skip the detail block in this case.
This also handles the reload-path where derived entries get snippet
populated but no preview, so displaySnippet falls back to the snippet
content for the header — same deduplication applies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
#3020: Sync viewed-count in the polling path for actively-viewed sessions
so navigating away doesn't show a stale unread dot. Defensive clear of
completion-unread marker in _setSessionViewedCount.
#2973: Clear elapsed-timer attributes and interval when a live compression
card transitions from running to done, preventing the orphan timer from
overwriting the completed card state. Guarded by active-session check.
Tool card duplication: Route tool_complete result to tc.snippet (detail)
instead of tc.preview (header) to prevent identical content appearing in
both the card header and expanded detail section.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`_turnUsage` (and `_turnDuration`, `_turnTps`, `_gatewayRouting`,
`_statusCard`) are computed client-side in `_finishDone()` and attached
to the last assistant message in `S.messages`. Three code paths replace
`S.messages` wholesale with fresh API data that lacks these fields:
1. `_restoreSettledSession()` after a late `stream_end` or SSE error.
2. The late-restore branch after `done` (messages.js ~L2247).
3. `loadSession()` for active-session external refresh / focus-change.
When any of these fire after `done`, the badge/duration/gateway-pill
flashes for ~1s and disappears, never returning until the next message
or page reload.
Add `_carryForwardEphemeralTurnFields(prev, next)` that matches messages
by `(role, timestamp, content prefix)` — the same identity the renderer
already uses — and copies forward the ephemeral fields when the server
payload is missing them. Wire it into all three replace sites. The fix
is conservative: it only fills slots that are `null` on the new message,
so an authoritative server-side value (if/when the API ever surfaces
per-turn usage) wins automatically.
Picked Option A from the bug report (preserve on the client side) over
Option B (synthesize from `S.lastUsage`) because `lastUsage` is a
session-level aggregate; reconstructing per-turn breakdowns from it is
lossy. Option C (set `_streamFinalized` earlier) would suppress legit
late-arriving server data on transient errors.
In multi-step turns (assistant -> tool_call -> assistant -> tool_call ->
final assistant), only the turn-final assistant bubble was rendering the
'jump to question' navigation button because the gate keyed on
isTurnFinalAssistant. Intermediate assistant bubbles that *do* have a
resolvable question raw-index lost the affordance entirely.
Switch the gate to 'show whenever questionRawIdxByAssistantRawIdx has a
target for this rawIdx', which is the actual precondition for the button
being meaningful. Turn-finality was a proxy for 'has a question target'
that under-covered multi-step turns.
No template/CSS change needed; _questionJumpButtonHtml already handles
the rawIdx-or-undefined contract.
- Stop provider-qualified or slash-qualified model inputs from fuzzy-matching a
sibling catalog entry when the exact requested model is missing from the
curated picker list.
- Preserve the raw typed selection so uncatalogued provider-routed models
fall through to a temporary custom option instead of silently snapping to a
nearby curated model.
- Add generalized regression coverage for provider-qualified uncatalogued
picker selections.