CodeNomad/dev-docs/TRANSCRIPT.md
heunghingwan c716f796d8
ui: keep the message token disclosure on one row in both states (#1084)
## Summary

The token/cost line that closes every agent message previously turned
into two
rows when expanded: a bare `Tokens` label line (chevron + label) with
the six
metric chips on a second line below it. This PR keeps the disclosure on
one row
in both states — expanding swaps the collapsed summary chips for all six
chips
on the very same line as the chevron.

## Problem

- Collapsed, the row is a compact single line: `› INPUT 3K · OUTPUT 54 ·
COST $0.76`.
- Expanding it replaced that line with a label-only row (`› Tokens`)
plus a
separate chip row, so a one-line affordance grew to two rows and pushed
the
  transcript content down twice as much.

## Change

- **Expanded = same row.** The six chips (input, output, reasoning,
cache read,
cache write, cost) now render in place of the summary chips, beside the
chevron. The `Tokens` label line and the separate content row are gone;
at
wide widths the expanded card is exactly as tall as the collapsed card.
- **Wrapping is preserved.** Expansion exists to read the values, so the
expanded row is never ellipsized: when width runs out, the chips wrap
onto
further lines in place (this wrapping behaviour is unchanged from
before).
- **Collapsed is untouched.** The collapsed summary keeps its single
  ellipsized line; its rendering is pixel-identical to before.

**Expanded — wide layout** (before shows the label line + chip row;
after is one row):

<img width="884" height="344" alt="wide-expanded"
src="https://github.com/user-attachments/assets/06abd5cd-b15d-4b29-ab39-1202d4c35ca5"
/>


**Expanded — narrow layout, 220 px** (both wrap instead of truncating;
after drops the label line and starts the chips beside the chevron):

<img width="390" height="339" alt="narrow-expanded"
src="https://github.com/user-attachments/assets/e4e5fbe7-a37c-42f6-8f73-3628d21ac2c6"
/>


**Collapsed — unchanged** (wide and narrow; pixel-identical
before/after, narrow still ellipsizes):

<img width="884" height="310" alt="wide-collapsed"
src="https://github.com/user-attachments/assets/71b40a15-6a19-4232-8eef-e0ce1327ff90"
/>


<img width="390" height="175" alt="narrow-collapsed"
src="https://github.com/user-attachments/assets/f77ebdd9-0540-4c74-9f2b-3a3fb64a7d9a"
/>


## Implementation notes

- `packages/ui/src/components/message-usage.tsx`: the toggle now always
renders
`chevron + chips` — the collapsed state renders the three summary chips,
the
  expanded state renders all six in the same slot. The separate expanded
  content `<div>` and its `aria-controls` wiring are removed.
- `packages/ui/src/styles/messaging/message-usage.css`:
`.message-step-usage`
is the expanded wrapping row (`flex-wrap`, no truncation); the collapsed
`.message-step-usage-summary` keeps its single-line ellipsis. The unused
  `.message-step-usage-label` / `.message-step-usage-content` rules are
  deleted. The chevron pins to the first chip row when the row wraps.
- **Accessibility:** the button's `aria-label` carries all six full
values when
expanded (`aria-label` overrides the visible chips, so they are repeated
there deliberately); the collapsed label is unchanged. The shared
disclosure
  chevron rotation keeps working via the generic `aria-expanded` rule.
- **dev-docs/TRANSCRIPT.md:** the message-token disclosure convention
note is
  updated to describe the one-row layout.

## Help pages

No help change needed: `docs/help/*/conversations.md` documents the line
as
*"expand it for reasoning and cache counts"*, which remains true — the
documented behaviour is unchanged and the two-row layout was never
documented.

## Validation

- TDD red→green: the new assertions were watched failing first (`0 !==
6`
  chips on the row, stale `aria-controls`), then passing.
- `tests/browser/usage-metrics.test.ts` — 5/5, now also covering:
expanded
chips on the toggle's row; single row at wide width; wrapping onto
multiple
  rows at 220 px in LTR **and** RTL with no horizontal overflow and no
  ellipsis; chevron level with the first chip row; Space/Enter collapse
  restores the three-chip summary.
- Related suites that render the usage row: `status-tokens` (4/4),
  `transcript-presets` (1/1), `search-highlight` (0 failures).
- Visual: before/after screenshots captured from the real fixture at 900
px and
220 px (the composites above); collapsed rendering verified
pixel-identical.
2026-10-21 18:52:11 +02:00

4.9 KiB

Transcript rendering

Message content visibility and presets, token lines, tool renderers, native session lifecycle signals and transcript re-projection.

Agent conventions

  • The message-content popup and Chat settings share components/transcript-visibility.ts and the semantic icons from components/message-content-icons.ts; tool presentation metadata lives independently of renderers in components/tool-call/tool-presentation.ts. Popup styles live in styles/components/transcript-filters.css.
  • Transcript presets are Nothing (hides every row, including all tools), Minimal, Balanced and Everything (TRANSCRIPT_PRESETS); saved Detailed choices load as Custom with their effective tool column. Chat settings shows the four as buttons; the popup shows the same choice as a native select under its title, where a disabled Custom entry appears only after a manual row change. Escape closes an open list before the popup. Preset names and descriptions are localized in every locale; covered by tests/browser/transcript-presets.test.ts.
  • Status Tokens are independent of message token visibility. A collapsed section uses the non-interactive renderSummary slot for one ellipsized session input/output/cost line; expanded content shows session metrics then descendant-inclusive totals as separate wrapping groups. Keep this layout in styles/panels/status-tokens.css.
  • Message token disclosures keep one row in both states: collapsed shows the input/output/cost summary chips on a single ellipsized line; expanded swaps the same row to all six chips beside the chevron, which wrap onto further lines when narrow instead of truncating (expansion exists to read every value). Keep the metric renderer local and chip/row styles in styles/messaging/message-usage.css.
  • Tool-result images retain native state.content and render through the shared components/tool-call/output-images.tsx surface, including MCP and specialized tools. Keep image bytes out of text projections (copy/search/speech); image styles live in styles/messaging/tool-call/images.css.
  • Code Mode reads native metadata.toolCalls in tool-call/renderers/execute-data.ts; keep call-list styling in styles/messaging/tool-call/execute.css, with native image/output ownership in the shared tool shell.
  • Native websearch text is parsed conservatively by tool-call/renderers/websearch-data.ts; unknown/oversized responses use the shared raw renderer. Recognized provider-consent Forms retain the generic schema/answer path, with layout in styles/messaging/tool-call/websearch.css.
  • Session lifecycle features use OpenCode V2's published signals: one session.execution.{succeeded,failed,interrupted} per busy period (lib/native-execution-events.ts) and session.retry.scheduled for provider backoff. The schema-declared session.idle/session.status are never published; never wire a feature only to them. A terminal event is a hint until session.active confirms settlement, which alone announces "A session finished", only for root sessions ending in success, failure or an inactivity stop. Transcript reducers retire at settlement, deferred while prompts stay parked in their inbox or a user shell or unsettled tool call still needs reconciling there. Token/cost lines (components/message-usage.tsx) appear once per agent message: the row of an assistant message's final text (the one carrying "delete tools and reasoning") renders, once that message has native usage, the sum (sumMessageUsage) of its own usage and of every message holding a tool or reasoning part that cleanup selects (technicalCleanupParts). Reuse that selection rather than a separate projection. The line renders inside the content row, so it never shifts <Index> items or splits a technical group (whose Delete needs two tools); tool-only steps get no line. A message whose text is followed by tools also counts in the next message's line, as its tools belong to that selection. Sums cover the loaded transcript window only, like cleanup. Capture real streams with scripts/test-session-lifecycle-native.mjs.
  • Native transcript events reduce only through stores/opencode-data.ts (never add another partial-part reducer), which publishes bounded snapshots through hydrateMessages (message-v2/instance-store.ts); these per-event re-projections must not invalidate unchanged messages: hydrateMessages reuses the resident MessageRecord object whenever content, explicit timestamps and prompt-display metadata are unchanged, and an identical whole snapshot returns before any store write or session-revision bump. Part/info comparisons use allocation-free structural equality (partDataEqual), never JSON.stringify. The SDK reduce mutates rows in place, so reference identity can never prove a session unchanged — never add a reference-anchored projection skip.
  • Shared speech byte/MediaSource transport lives in lib/audio-utils.ts; playback ownership and cancellation stay with its callers.