Injected context reached the transcript as one anonymous shape whose expanded body was the whole message serialized as inline JSON, so the escaping collapsed the only readable part — the model-facing prose — into a single line. `MessageSource` gains an optional producer-declared `form`, a small semantic vocabulary of information shapes independent of `kind`: kind says who produced the context, form says what shape it is, so several producers may share one presentation. Two values ship. `instructions` (workspace-context) lists the reconciled files above their text and keeps the `<system-reminder>` framing verbatim, because the framing is part of what the model read. `catalog` moves dsh-tool-skill off the shared plugin kind onto a `skill-catalog` source carrying the published name/description entries, and the body lists those instead of re-parsing `<available_skills>` out of the prose. Catalog identity moves with it: the republish digest now covers the durable entries, deleting the text-slicing that recovered them from a logged message. Everything else renders the opaque body — the model-facing text with its real line breaks, then the remaining provenance as fields. That is the documented default, not a leftover: a resumed, forked, or foreign log must render whether or not its producer is mounted here, which is why the classification lives in the durable source rather than a client-side table keyed by producer.
6.3 KiB
Agent Note: Web transcript marks context source, recall, and steering
Status: implemented
English | 中文
Problem
Everything a producer adds to the model-facing conversation reached the Web transcript as one of two anonymous shapes. Every logged non-user user/message — the skill catalog, the runtime snapshot, reconciled AGENTS.md instructions, a guard notice, a subagent report, a cross-session snapshot — collapsed into one identical 上下文注入 row, so a reader could not tell what had been added without expanding each row and reading raw JSON. Mid-turn steering was worse: it rendered in exactly the bubble a turn-opening prompt uses, leaving the transcript unable to say which message interrupted a running turn.
The distinctions are already durable. user/message.source is the merge-extensible provenance every producer must supply, and steering has its own steering/message event; only the presentation discarded them. The terminal transcript this Web UI replaced did name each card's producer, so the Web surface was a regression for the same log.
Decision
The transcript names all three roles a non-prompt message can play — injected context, recalled session, and steering.
TranscriptAdapter and the history fold attach a provenance view to every ContextMessageNode, computed by contextProvenance() from the durable source alone. It returns a role (inject, or recall for a cross-session snapshot) and a label naming the producer. ContextInjectionRow titles itself from the role and shows the label beside that title in ToolRow's summary geometry, so the collapsed row already answers what was added and by whom; the 141px scrollport and truncation bound are unchanged from the disclosure decision. What renders inside that scrollport is chosen by the independent form axis added in the context form decision.
The label is read out of the log, never from a client-side table of producer names. workspace-instructions is named by the distinct instruction paths it reconciled, session-reference by the titles of the sessions it read, a plugin source by its logged plugin id, and any other source by its own kind — the documented default arm for a merge-extensible union. A source carrying no readable kind degrades to an unnamed injection. A new or renamed producer is therefore identifiable without a client release, no label can go stale against the code, and a resumed, forked, or foreign log projects exactly like a live session.
recall covers session-reference because that is the one shipped source that lifts another session's material into this one. No Web leaf mounts dsh-session-reference today — it had only a terminal host — so the arm exists for log portability rather than for a bundled producer, and it is exercised by unit coverage rather than an assembled Web scenario.
MessageItem captions durable and pending steering bubbles with 插话. This reverses one clause of no steer entry or interjection chrome, which removed the badge because the composer could not steer and the label named a gesture users could not perform. The composer gained a Steer gesture afterwards without amending that note; this decision supplies the product decision its reintroduction clause required, and corrects the stale facts left in it. The caption is the only steering chrome here: composer modes, the Queue dock's strict-steer action, and pending-steering lifecycle stay with their own owners.
Alternatives considered
Localize producer names in the client. A dictionary keyed by plugin id would read better than @deepseek-ai/dsh-system-prompt, but it drifts silently on every rename, needs a client change per new producer, and cannot name a producer from a foreign log at all. Provenance the log already carries is worth more than prose the client invents.
Register presentations per source kind. The disclosure decision deferred a keyed context-view slot until source-owned presentations emerged. Naming a row is not a distinct presentation, and a registry keyed on mounted producers would fail exactly where it matters — a resumed log whose producer is no longer mounted still has to render.
Compute the role and label on the host. The host would have to attach a view to each event copy, duplicating what the durable source already states and adding a wire field per context message. The projection derives it once per node instead, where the transcript's other derived facts live.
Give steering its own row instead of a captioned bubble. Steering is a user message that arrived mid-turn; a separate row shape would break the right-aligned reading rhythm and duplicate the bubble's copy and branch actions for no new information.
Extend the trajectory table with the same names. Out of scope: the table's context cell has its own text derivation, and the issue asks for the conversation surface.
Testing
packages/client/runtimeunit coverage pins each provenance arm, the label fallbacks when a name field is missing, empty, or wrongly typed, and the unnamed degradation for a source with no readable kind.packages/client/ui-conversationjsdom coverage pins the role title, the producer label beside it, the label's survival while expanded, the roleless header, and the steering caption on both durable and pending bubbles.- The keyless assembled-Web goldens carry the named header and the steering caption, so the assembled transcript — not only component tests — proves the marks.
Consequences
- A reader can attribute every non-prompt message in the transcript at a glance, and the header stays honest for logs this client version has never seen a producer for.
- Producer names in the UI are package-shaped (
dsh-tool-skill,@deepseek-ai/dsh-system-prompt) wherever the source carries only a plugin id. That is the cost of refusing a client-side name table; a producer that wants a better name records better provenance. ContextMessageNodegains a required field, so every constructed node — including test fixtures — must supply it.- The
recallarm has no producer in a shipped Web leaf until a host mountsdsh-session-reference; it is reachable only through logs written elsewhere.