Commit graph

4 commits

Author SHA1 Message Date
Pascal André
f400d5beb9
fix(transcript): load large tool images on demand instead of inline (#1118)
Some checks are pending
Publish help / build (push) Waiting to run
Publish help / deploy (push) Blocked by required conditions
A session whose history holds image tool results (for example `read` on
PNG files) never opened over a slow link (#1113): the 200-message page
was 16.8 MB, 15.7 MB of it base64 in five tool results. It took 20 s at
6.5 Mbit/s, and a reverse proxy cut the transfer, so the UI showed
"Failed to fetch". Every open, reload and history window paid for every
image, shown or not.

Transcript pages now carry large tool images by reference. The UI's
native message list reads (createInstanceFetch) and history windows send
the `x-codenomad-media: deferred` opt-in. For those reads only, the
server replaces each tool image whose data URI exceeds 32 KiB with a
`/api/workspaces/<id>/session-media/<session>/<message>/<part>/<index>/
<sha256>` path, in the instance proxy (forwardRuntimeRequest gains an
optional whole-JSON transform for successful JSON responses) and in the
history window route. toolImageSource resolves that path on the API
base, so the existing lazy `<img>` loads only the images that are shown,
as binary rather than base64.

The new session-media route checks that the workspace owns the session,
reads the native message, and refuses a session that moved during the
read and messages at or after an undo boundary. It serves the bytes only
when the image still matches the digest, as an immutable, nosniff,
sandboxed image; anything else falls back to "Unable to display this
image".

What stays whole:
- Single-message reads (`session.message.get`), so pruning revisions
  keep hashing native content.
- Live events, user attachments, small images, non-image files and
  error responses.
- Requests without the opt-in.
The opt-in header is never forwarded to OpenCode. On the reported
session, the page drops from 16.8 MB to about 1.1 MB.

Validation:
- session-media.test.ts: only large renderable tool images change, and
  only their URI.
- routes/session-media.test.ts: exact bytes and headers; invalid
  references, digest and position mismatches, the undo boundary,
  ownership and a move during the read.
- proxy.test.ts: the transform applies to successful JSON only and
  releases once.
- instance-proxy.test.ts: an opted-in list read is deferred, the opt-in
  is not forwarded, and plain list or single-message reads stay whole.
- session-navigation.test.ts: history windows.
- sdk-manager.test.ts: only list reads opt in, keeping other headers.
- tool-content.test.ts: path resolution, forged paths rejected; it is
  now listed in the PR build.
- tool-images.test.ts: a deferred image decodes from the route in the
  real ToolCall renderer, and a missing one shows the fallback.

The UI own-tsconfig, server and root typechecks pass, as do:
- the server suite (2,467 passed; one index.test.ts process test timed
  out under load and passes alone, as on dev);
- the UI unit step of the PR build (564 tests);
- seven browser suites (278 tests).
2026-10-23 23:38:00 +02:00
Pascal André
47e02795b2
fix(session): clear a queued follow-up's badge when it is delivered (#1117)
A follow-up sent while the agent works (follow-up behaviour "queue") shows
the "Queued" badge until the engine delivers it. After some turns the
badge never cleared: the message was answered, the engine went idle, and
only closing and reopening the session removed it (#1112). The stale
pending entry also kept the message pinned below the running turn.

The session view lists queued prompts from the session reducer's inbox
through a memo. CodeNomad replaces that reducer while the session stays
open: an authoritative resync after an event-queue overflow, a rotation
failure or a reconnect, and a pruning invalidation. The replacement was a
plain property write, so the memo kept reading the disposed reducer,
which never sees `session.inbox.delivered`. On 0.20.1 nothing rebound it
until the view remounted; on dev the reducer retirement at settlement
happened to rebind it at the end of the turn, but the badge stayed for
the rest of the turn, and indefinitely when retirement waits (another
prompt still parked, an unsettled tool or shell).

Every replacement now goes through replaceTranscriptEntry, which assigns
the new reducer, disposes the previous one and bumps a per-transcript
entryRevision signal that getOpenCodeSessionInbox tracks. Only that
session's inbox observers re-run; a transcript destroyed and recreated is
still rebound by the instance revision. Several queued follow-ups keep
their badges until each is delivered; steer-mode actions on a delivered
prompt disappear the same way.

Validation: opencode-inbox-rebinding.test.ts queues two follow-ups,
replaces the reducer by pruning invalidation and by an event-queue
overflow, then delivers them one by one with no terminal event: each
leaves the memo at once. Both cases fail without the fix. A browser test
in session-rendering.test.ts runs the real SessionView and SSE dispatcher:
queued badge, a pruning event, delivery, badge gone while the turn still
runs; it fails without the fix with the badge still visible. The session
fixture's native inbox now lists parked prompts as an array and stubs the
permission and form lists, as a real daemon does, so an authoritative
resync completes. The new unit file is listed in the PR build's UI unit
step, which passes locally (64 files, 562 tests), as do the UI own-tsconfig
and root typechecks and the six browser suites using the session fixture
(206 tests).
2026-10-23 22:48:05 +02:00
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
Pascal André
7a1fc90310
docs(agents): keep AGENTS.md to universal rules and agent docs to the current state (#1082)
AGENTS.md loaded about 33,000 tokens into every agent session, subagent and reviewer, mostly per-feature contracts, and the agent documentation around it had accumulated duplicates, dated logs and history of removed designs. This PR makes AGENTS.md a short set of rules that apply everywhere plus an index of area documents, and leaves AGENTS.md, dev-docs and the repository skills describing only the current state.

Three commits, reviewable separately:

1. **Move (lossless).** Every per-area rule moved verbatim from AGENTS.md into its area document (existing ones, or six new ones: UI_CONVENTIONS, UI_SHELL, COMPOSER, TRANSCRIPT, SETTINGS, PROJECTS). A script split the old file into 208 units, required one destination for each and checked all 199 content units verbatim at their destination. AGENTS.md went from 134,000 to 16,000 characters.
2. **Trim.** A per-area ponytail review removed rules stated two or three times (keeping the more precise copy), dated validation logs, per-version qualification sections below the current minimum, abandoned designs and self-references; seven documents whose surviving rules live elsewhere were deleted. It also fixed statements the code contradicts (locale registration, the Mission play/recovery condition, plugin package updates, cleanup progress UI, timeline index persistence, automation restart/targets, stale pins and minimums).
3. **Current state only.** The new AGENTS.md rule: no history (removed features and their names, former behavior, provenance, dated logs) and no denials; correct a contradicted statement at its source. DEVELOPER_MODE.md is merged into BROWSER_AUTOMATION.md and every Developer Mode mention is gone (including the README and the browser help page in ten locales). Names that only made sense against older designs are renamed with every reference updated, MISSIONS_ENVIRONMENT_ADMISSION is folded into SESSION_ENVIRONMENT, and INDEX lists every document. The server npm README now states the real OpenCode minimum (2.1.2).

AGENTS.md is now about 14,000 characters (roughly 3,500 tokens); AGENTS.md, dev-docs and the vendor README went from about 11,200 lines to under 4,700.

Validation: every relative Markdown link and every cited dev-docs path resolves; no reference to a renamed or deleted document remains; `npm run test:help` passes for all ten locales.

Review: three independent GPT-6.1 Sol (high) reviews by area found binding rules lost in the trim and current-state passes; 25 were restored as present-tense facts or prohibitions and four contradicting statements corrected, each checked against code. A Fable 5.1 gatekeeper review of the whole PR passed; its follow-up routes session recovery from the index, points to dev-docs/INDEX.md and documents the family identity read.
2026-10-21 16:59:43 +02:00