A settled top-level `read_image` call printed its raw attachment object as
literal text in the tool card — `{"type":"image","attachment":{…}}` —
instead of the image, because no presentation metadata told a client card
how to present the reference and the tool-card layer had no image concept.
Host: `read_image` declares an `output.presentationMeta` persisting
`{ path }` only. The attachment reference deliberately lives in the
settled result content — the single record a `tools/post-execute`
replacement rewrites — not in `meta`; the id is opaque and
provider-owned, checked for existence only.
Client: `imageCardModel` derives the card from the call head, the meta
path, the result's own image block, and a shape-matched envelope. ToolRow
gains an `image` card slot; the `read_image` toolview declares the
Tool-owned `tool.call.images` slot as its child and dispatches the
gallery through it. ui-chat down-threads the session-authorized loader
(`ChatNodeOwnerProps.loadImage`), so the tool layer supplies only derived
references plus the loader and never imports an attachment
implementation; ui-attachment fills the slot with its message gallery
renderer. The card keeps the envelope text below the gallery for the
no-attachment-plugin deployment. An image-bearing tool registers a keyed
toolview; the generic fallback keeps its flattened text. `read_image`
joins the read variant with its own locale title key; both rows share
`read-family-row.tsx`.
Verification: `read-image.spec.ts` (metadata projection, envelope by
shape, reference narrowing, real-execution round trip, rejection
branches incl. non-digest ids), `image-card.client.spec.tsx` (derivation,
row render site dispatching the slot, keyed registration with the
child-slot declaration, empty-slot fallbacks, media-type enum), keyless
snapshots (`read-image-gif` added; read-image/-dimension/-reencode
updated to the `{path}` meta), five injected-defect negative controls,
and a demo GIF recorded from this PR's head through the official
image-capable model.
- api/gateway: $host caches its RemoteHostFacts record and mints a new
one only when home changes, so snapshot readers compare by reference;
identity pinned in the client spec.
- client/ui-tool, client/ui-workspace: the hooks channel exposes the
host facts record as hostInfo and components select the field they
need (useHostInfo(info => info.home)); row-component contracts are
unchanged.
- api/gateway: mark terminal Remote-stream escapes (carrier retry
exhaustion and pre-acceptance end classification) as
RemoteError('gateway/internal') at the two escape points; marked
failures pass through verbatim. The carrier class stays the
retry-internal signal for carrierFailed and the ended(true) retry
trigger. Regression coverage lands on the session and workspace
stream consumers.
- client/ui-tool, client/ui-workspace: read $host.home through a hooks
observable subscribed to connection/reset; the slot renderer memoizes
inject results per entry, so the previous plain-value injection froze
home at the first render.
- client/ui-settings-models: components no longer receive ctx; apply
binds the credential and settings Remote operations into callbacks,
and the settings/conflict code judgment stays in the apply world.
Single RemoteError with a merge-extensible, domain-prefixed code map;
owners throw at the failure point; streams surface marked failures;
clients consume ctx.remote directly with isRemoteFailure as the only
discrimination point and construct no failure instances.
Batch of visually verified Web UI fixes: trigger-menu z-index over the
resize handle (#3228, #3229), input scrollbar offset, tool-row file
links and diff stats (#3230), @ menu crumb alignment, light-mode
divider, and @ menu flicker while typing (#3234) via
stale-while-revalidate — a refinement hit keeps the previous items and
highlight on screen until the new generation settles, so neither the
skeleton nor the first-row focus blinks per keystroke.
* feat(ui-conversation): fold turn process before final answer
* fix(ui-chat): polish turn-process control row from review
* test(web): drive preset slash catalog with gestures
* fix(ui-chat): keep turn process order stable
* fix(ui-chat): preserve prompt order after pagination
Co-authored-by: Yif <877193178@qq.com>
Derive --dsh-content-font-size-secondary (setting -1 at <=14, setting -2
above; 13px at the default) with --dsh-content-font-delta-secondary in
gradient-shadow-text.css, and move every one-step-under-the-body text
onto it: think text and reasoning summaries, the shared DisclosureRow
title, ToolRow and bash-row summaries and file links, compaction/
context/command/retry/error rows, StatsLine, the workflow-run panel
tiers, reference summaries, the turn-status clock, and the feedback
note trigger. The markdown table variants join the same tier instead of
staying fixed; its 13px floor keeps them legible at the 12px setting.
At the default setting the flow-row titles and summaries render at 13px
(previously 14px) so they match think text at every setting instead of
sitting 2px above it.
Adopt --dsh-content-font-size / --dsh-content-font-delta across the flow
rows around the transcript body: DisclosureRow header (row height, title,
leading box, and registered glyphs, with StateDot exempt), ToolRow and
bash-row summaries and file links, think text, compaction/context/retry/
error rows, message clock and icon actions, the workflow-run panel, and
the workspace browser. Update the font-size Agent Note and add CSS-text
specs for the new adoptions.
The rail's items now ride the Chat snapshot: a structural upsert re-derives
the loaded Turn set, a content-only upsert re-derives only the Turns whose
nodes changed, and each preview is capped so navigation state never holds a
copy of the transcript. The published array keeps its identity until an item
changes, so ChatView selects it as both data and change signal — and a
streaming reply's preview follows the in-place node update instead of the
last structural publication.
A scroll frame resolves the active mark with one hit test at the reading
line, falling back to a single row scan, rather than a DOM query per mark.
Flow-height changes resync through the existing column observer, navigating
during a pending page keeps the paging anchor, and the rail height no longer
holds a floor taller than the band it centers in.
- stop attributing the 45-case helper/Node divergence to the path port spec,
which pins only the Node-facing port to Node
- keep the divergence measurement as provenance, without a dead owner
- fix the path corpus comment so it does not claim divergence from the spec
that asserts equality
- drop the v2 generation stamp from the cordis mount fallback test
- restate the watcher refusal rationale in current-state terms
- re-record the ui-workspace bilingual pair after rebase
Remove dead design-session citations, change narration, indexical
stamps, and review-adjacent justification found by the
dsh-trim-cot-leakage recall batteries in prose that landed after the
last purge. Bilingual README pairs are re-recorded.