Context that came with a turn is a row of its own #69

Merged
mph merged 14 commits from cc-chat into main 2026-09-01 07:44:14 +00:00
Owner

Slice — cc-chat/05-procedures-worker-chat

Goal: do a real piece of worker-session work in the procedures dashboard through <cc-chat>, with procedures' own worker chat deleted rather than beside it.

Fit test (close gate): Michal opens a Matter he cares about, works a worker session through the migrated dashboard, and would put the live system on it — nothing missing, nothing foreign, no second generation left in the tree.

Fit passed on the offline harness, 2026-08-30, against real Claude sessions.

This PR carries this repository's half. The migration itself lands in procedures on cc-chat-worker-chat (907e4d08), on its own PR there. Slices 01–04 of this effort already shipped as #64, #67 and #68.


What is in it

Context that came with a turn is a row of its own (CC-D4)

UserInput carries three things — what a person typed, what the application attached, and the channel that composed it. The Claude Code adapter decoded all three and kept one. So a brief the dashboard wrote and a prompt reacting to an event both reached the page as TranscriptEntry.UserMessage, documented "what the person typed".

The vendor cannot tell them apart for us. Across 780 entries procedures wrote — 508 reactive, 166 interactive, 106 matter-brief — every one carries promptSource: "sdk" and entrypoint: "sdk-cli": the vendor marks the transport, not the origin. The Messages API offers no field for injected content, and the CLI drops message.metadata, a top-level source and a top-level attachments outright (probed on 2.1.251).

  • TranscriptEntry.Context(at, name, prose) — a seventh row kind.
  • UserMessage carries the channel that composed it, absent when somebody typed it.
  • TranscriptMarkup renders <cc-context name="…"> as a folded row and puts channel on cc-message.
  • TranscriptCodec round-trips both, tolerantly in each direction.
  • cc-chat.css draws the row and says how a host takes one off the page.

The split of knowledge is the point: the SDK knows that a block is context, its name and its body, and renders the shape; the consumer knows what the name means and how it should look, in cc-chat.css. Nothing here learns any host's vocabulary.

A conversation opened at its oldest row

TranscriptScroll.scroller searched from the parent, so a host that put the scrolling overflow on cc-transcript itself — the box every host reaches for — was walked past and the element scrolled the page instead. Its own documentation already said the transcript counts. Found by the migration; fixed with a test.

Warnings stop this build

SharedModule.scalacOptions appends -Werror. The shared mill-iw-support baseline reports warnings without failing on them, and __.compile does not compile test sources at all — so an inexhaustive match introduced by the new enum case sat in the tree with every gate green. The tree was already clean under -Werror on the first compile; the flag is what makes a green gate evidence.


Review

Six reviewers (style, testing, security, scala3, composition, architecture): 1 critical, 5 warnings, 12 suggestions. The critical was the inexhaustive match above, found by the scala3 review rather than by the build — fixed, and the build now refuses it. Every warning was acted on; the suggestions taken and not taken are recorded with reasons in review-slice-20260830-145701.md.

Two findings worth surfacing:

  • Hiding a context row with CSS hides display, not delivery. The renderer's own docstring invited display: none, and hidden context is still in the document, the source and the accessibility tree. The model, the renderer and the sheet now say so, and say that anything a viewer must not have is left off the message rather than styled away.
  • Channel.Interactive is fail-open. Forget to name a channel and the turn renders as a person's words. Documented on Channel itself as what the sender declared, never an authenticated signal. Making omission a compile-time decision is parked — it breaks every caller.

Security review confirmed no attribute injection (scalatags escapes every attribute value) and no widened rendering trust boundary (context prose goes through the same Markdown.prose sanitiser message prose already did: escapeHtml, allow-listed URL schemes, no image fetching).


Gates

__.test 915 · __.checkFormat · __.fix --check · every main and test source compiles warning-free under -Werror.

Verified end to end on the offline harness against a real Claude session: the worker read the brief out of its context block and oriented correctly (7 tool calls, 17 s); the opening line is marked with the channel that composed it; a message Michal typed carries none.


Follow-ups (parked, flushed to the effort map)

  • The page reads from the transcript file; it should read from the live stream. Measured: the composer clears at 103 ms and nothing at all happens until 303 ms, one LookEvery = 250 ms poll after the CLI writes the file. Fast today on a warm local CLI; nothing in the contract makes it so.
  • The archive is a promise the SDK does not keep. procedures reads vendor-first and falls back to ~/.claude/transcript-archive — that directory does not exist, and nothing writes it, because archive defaults to None and installs SessionArchiveHook.none. The machinery all exists and is opt-in, defaults to nothing, and fails silently. Every consumer will get this wrong the same way.
  • The JSON encoding of a context item; the Channel.Interactive default; ContextItem.Viewing(url) going through the markdown parser; the global chat generation.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1

## Slice — cc-chat/05-procedures-worker-chat **Goal:** do a real piece of worker-session work in the `procedures` dashboard through `<cc-chat>`, with procedures' own worker chat deleted rather than beside it. **Fit test (close gate):** Michal opens a Matter he cares about, works a worker session through the migrated dashboard, and would put the live system on it — nothing missing, nothing foreign, no second generation left in the tree. **Fit passed** on the offline harness, 2026-08-30, against real Claude sessions. This PR carries **this repository's half**. The migration itself lands in `procedures` on `cc-chat-worker-chat` (`907e4d08`), on its own PR there. Slices 01–04 of this effort already shipped as #64, #67 and #68. --- ## What is in it ### Context that came with a turn is a row of its own (CC-D4) `UserInput` carries three things — what a person typed, what the application attached, and the channel that composed it. The Claude Code adapter decoded all three and kept one. So a brief the dashboard wrote and a prompt reacting to an event both reached the page as `TranscriptEntry.UserMessage`, documented *"what the person typed"*. The vendor cannot tell them apart for us. Across 780 entries `procedures` wrote — 508 reactive, 166 interactive, 106 `matter-brief` — every one carries `promptSource: "sdk"` and `entrypoint: "sdk-cli"`: the vendor marks the transport, not the origin. The Messages API offers no field for injected content, and the CLI drops `message.metadata`, a top-level `source` and a top-level `attachments` outright (probed on 2.1.251). - `TranscriptEntry.Context(at, name, prose)` — a seventh row kind. - `UserMessage` carries the channel that composed it, absent when somebody typed it. - `TranscriptMarkup` renders `<cc-context name="…">` as a folded row and puts `channel` on `cc-message`. - `TranscriptCodec` round-trips both, tolerantly in each direction. - `cc-chat.css` draws the row and says how a host takes one off the page. The split of knowledge is the point: the SDK knows *that* a block is context, its name and its body, and renders the shape; the consumer knows what the name means and how it should look, in `cc-chat.css`. Nothing here learns any host's vocabulary. ### A conversation opened at its oldest row `TranscriptScroll.scroller` searched from the parent, so a host that put the scrolling overflow on `cc-transcript` itself — the box every host reaches for — was walked past and the element scrolled the page instead. Its own documentation already said the transcript counts. Found by the migration; fixed with a test. ### Warnings stop this build `SharedModule.scalacOptions` appends `-Werror`. The shared `mill-iw-support` baseline reports warnings without failing on them, and `__.compile` does not compile test sources at all — so an inexhaustive match introduced by the new enum case sat in the tree with every gate green. The tree was already clean under `-Werror` on the first compile; the flag is what makes a green gate evidence. --- ## Review Six reviewers (style, testing, security, scala3, composition, architecture): **1 critical, 5 warnings, 12 suggestions**. The critical was the inexhaustive match above, found by the scala3 review rather than by the build — fixed, and the build now refuses it. Every warning was acted on; the suggestions taken and not taken are recorded with reasons in `review-slice-20260830-145701.md`. Two findings worth surfacing: - **Hiding a context row with CSS hides display, not delivery.** The renderer's own docstring invited `display: none`, and hidden context is still in the document, the source and the accessibility tree. The model, the renderer and the sheet now say so, and say that anything a viewer must not have is left off the message rather than styled away. - **`Channel.Interactive` is fail-open.** Forget to name a channel and the turn renders as a person's words. Documented on `Channel` itself as what the sender declared, never an authenticated signal. Making omission a compile-time decision is parked — it breaks every caller. Security review confirmed no attribute injection (scalatags escapes every attribute value) and no widened rendering trust boundary (context prose goes through the same `Markdown.prose` sanitiser message prose already did: `escapeHtml`, allow-listed URL schemes, no image fetching). --- ## Gates `__.test` 915 · `__.checkFormat` · `__.fix --check` · every main and test source compiles warning-free under `-Werror`. Verified end to end on the offline harness against a real Claude session: the worker read the brief out of its context block and oriented correctly (7 tool calls, 17 s); the opening line is marked with the channel that composed it; a message Michal typed carries none. --- ## Follow-ups (parked, flushed to the effort map) - **The page reads from the transcript file; it should read from the live stream.** Measured: the composer clears at 103 ms and nothing at all happens until 303 ms, one `LookEvery = 250 ms` poll after the CLI writes the file. Fast today on a warm local CLI; nothing in the contract makes it so. - **The archive is a promise the SDK does not keep.** `procedures` reads vendor-first and falls back to `~/.claude/transcript-archive` — that directory does not exist, and nothing writes it, because `archive` defaults to `None` and installs `SessionArchiveHook.none`. The machinery all exists and is opt-in, defaults to nothing, and fails silently. Every consumer will get this wrong the same way. - The JSON encoding of a context item; the `Channel.Interactive` default; `ContextItem.Viewing(url)` going through the markdown parser; the global chat generation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1
mph added 13 commits 2026-09-01 07:02:10 +00:00
TranscriptScroll.scroller searched from the parent, so a host that put the
scrolling overflow on cc-transcript itself — the conversation scrolls, the
composer under it stays put — was walked straight past, and the element moved
the page instead. A conversation then opened at its oldest row.

Its own documentation already said the transcript counts: "a host that gives
the transcript, or anything around it, a scrolling overflow gets a chat that
scrolls inside its own box".

Found migrating the procedures worker chat, which reaches for exactly that box.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1
A turn this SDK composed says three things — what the person typed, what
the application attached, and which channel carried it — and the adapter
kept only the first. A brief the application wrote and a prompt reacting
to an event both reached the page as UserMessage, documented "what the
person typed", so an application's words were read as a person's.

TranscriptEntry gains Context(at, name, prose) and UserMessage gains the
channel that carried it, absent when somebody typed it. The renderer
gives context its own folded row, `<cc-context name="…">`, and puts the
channel on the message as an attribute. What a name means, and whether
a reader should see it, stays the host's: cc-chat.css draws the row and
says how to take one off the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1
The checklist read as one list of paths in one tree, and two of its lines
were files in procedures. It is now two lists: this repository's, and the
sibling's — where the box is checked from here but the work and its
review live on that repo's own branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1
Ratified by Michal on 2026-08-30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1
One critical, found by the scala3 review: adding TranscriptEntry.Context
left a match in ClaudeCodeTranscriptTest covering six of seven cases. The
compiler warned and nothing failed — __.compile skips test sources and
this build does not promote warnings to errors. The case is added, and
CLAUDE.md's compile gate now says what it does not cover.

The rest:
- context routes through machineryRow instead of hand-building the same
  details/summary markup, so every folded row is built in one place
- the CSS section that styles it is named for what it now covers
- three tests: several context items keep their order, an older writer's
  channel-less user entry, and a context entry read from the wire
- the model, the renderer and the sheet now say that hiding a context row
  hides display, not delivery — it is still in the document, the source
  and the accessibility tree
- Channel's doc says Interactive is the trust fallback, so no reader
  mistakes it for an authenticated signal
- carriedBy reads as channelName at its call site
- CC-D4 propagates to PRODUCT.md, whose depth rule names two categories
  where there are now three

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1
build: warnings stop this build
All checks were successful
CI / Check Formatting (pull_request) Successful in 53s
CI / Compile (pull_request) Successful in 1m16s
CI / Check Linting (pull_request) Successful in 54s
CI / Run Tests (pull_request) Successful in 1m38s
CI / Run Integration Tests (pull_request) Successful in 1m47s
CI / Check Bundle Size (pull_request) Successful in 43s
5c1dedfe1a
A warning nobody has to act on is a warning nobody reads. The shared
baseline in mill-iw-support reports them; SharedModule now appends
-Werror, so a green gate is evidence about the tree rather than only
about what was fatal. A project may add to the plugin's baseline without
changing it, so no other project pays for this.

The tree was already clean under it — every main and test source, first
compile. Confirmed fallible: restoring the inexhaustive match this slice
introduced fails ui.test.compile, where before it printed a warning and
passed.

Both lessons triaged with Michal: the process-name kill goes to kanon as
note #195 (a clause in advise-command-traps.sh plus a store page), and
this one is fixed at source rather than written down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1
workflow(sl): close cc-chat/05-procedures-worker-chat (flush parked items)
All checks were successful
CI / Check Formatting (pull_request) Successful in 42s
CI / Compile (pull_request) Successful in 1m0s
CI / Check Linting (pull_request) Successful in 57s
CI / Run Tests (pull_request) Successful in 1m24s
CI / Check Bundle Size (pull_request) Successful in 46s
CI / Run Integration Tests (pull_request) Successful in 1m37s
e080b4b2ce
Fit passed on the offline harness against real Claude sessions. Four
parked items become map candidates: the owned session archive (verified
broken, asked for soonest), reading from the live stream, converging
procedures' four conventions for attached context, and four one-line
asides from the reviews.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DS9XLuXRvpbEuV9ZE3Whn1
mph merged commit c8a46af715 into main 2026-09-01 07:44:14 +00:00
mph referenced this pull request from a commit 2026-09-01 09:00:40 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
iterative-works/claude-code-query!69
No description provided.