Repository navigation
Render transcript Markdown with CommonMark and GitHub's extensions (CF-4 parser slice) - #283
Conversation
Markdown.tsx's hand-written subset becomes micromark with its GFM extension (mdast-util-from-markdown 2.0.3, micromark-extension-gfm 3.0.0, mdast-util-gfm 3.1.0, pinned and bundled into the renderer). The tree renders straight to React elements: tables, task lists (inert boxes), block quotes, nested lists, strikethrough, footnotes, reference links and language-tagged fences. Raw HTML stays text, only http(s) links are links, other targets stay as written, and an image becomes a link to it instead of being fetched. Parsing every delta again would cost a long reply about 4 ms a render here, five times the old subset. While text keeps changing, only its last block (split at blank lines outside fences) is parsed again, about 0.2 ms a delta; the whole text is parsed once it rests for 300 ms. Settled renders are cached, so reopening a thread does not parse it again. First open of a 200-message, 239 KB thread: about 160 ms, against 40 ms before. Part of #277 (parser slice); semantic turn rendering waits for #274. Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
The transcript's Markdown also renders fetched pages, which can be large. RenderCache keeps the 400 most recent settled renders of text up to 64,000 characters; longer text renders each time and is never kept. Coven-Harness: Claude Code / claude-opus-5-5
CodeBlock (components/transcript/) heads a fenced block with its
language ("text" when it has none) and a Copy button. Copy sends the
code through main's clipboard:write and does nothing else: no tool,
approval or turn starts. A status line tells screen readers it copied,
or that copying failed. The e2e copies a block and checks the clipboard
holds exactly its code while the thread stays idle.
Part of #277.
Coven-Harness: Claude Code / claude-opus-5-5
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Code coverage overviewLanguages: TypeScript TypeScript / code-coverage-agent
View coverage of the 2 most impacted files.
ℹ️ Calculations represent line coverage. Results may change as calculations are updated. Updated |
CodeQL flagged the Markdown test's visible() helper as incomplete multi-character sanitization: it removed tags with a regex and then decoded entities. It only reads React's static markup in a test, but the helper now skips tags character by character and decodes each entity once, which is also what a reader sees. Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
CI run 39112253838 failed the code-block Copy e2e once: Copied never showed after a 42 ms click, while local runs and two other CI runs passed. The likely cause: the follow-up suggestion arrives after the turn ends, grows the composer, and the transcript moves under the pointer. The test now clicks until Copied shows. A missed click copies nothing, so this cannot hide a wrong copy, and the clipboard and nothing-started assertions are unchanged. 9 of 9 local repeats passed. Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
|
Holding this PR (back to draft): I found a defect in it. While a reply streams, Markdown renders it as memoized pieces ( #277 requires selection and focus to survive sustained updates. I'm adding a failing e2e test for it, then a fix: the settled parse keeps the same piece structure, so React updates in place instead of remounting. I'll mark this ready again with fresh CI. |
While a reply streamed, Markdown rendered it as memoized pieces; once the text rested it switched to plain elements from one full parse. The children's component type changed, so React unmounted every piece and mounted new DOM: text the reader had selected in the reply, keyboard focus on a control in it, and a code block's Copied state were lost the moment it finished (e2e/transcript-continuity.spec.ts, red on 74833f7: the selection read "" and Copy lost focus). Both ways now render one Piece per streaming piece. The settled render parses the whole text and groups its top-level blocks back into those pieces by source offset, so settling updates each piece where it stands. Streaming pieces are kept per component instance by text, so an unchanged piece is still not parsed again. Part of #277. Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
At 900x600 with the Review panel open, the transcript is 265 px wide. A table with an unbreakable path took 413 px for that column and squeezed the others to a word: Detail measured 58 px and its row 1,009 px tall. Cells now keep at least 10em and let a long path or URL wrap inside, so the table scrolls sideways in its own box (the row is 344 px; nothing is cut to one word per line). conversation-rendering.spec.ts checks, in Graphite, Jev and OpenCoven at 900x600: no sideways page scroll, a wide table and a long code line scroll in their own boxes, every cell is at least about ten characters wide, and Copy stays visible. It attaches a screenshot per theme (MODEX_SCREENSHOTS keeps them). Part of #277. Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
Both entries kept in docs/status.md. Coven-Harness: Claude Code / claude-opus-5-5
Both entries kept in docs/status.md. Coven-Harness: Claude Code / claude-opus-5-5
Both entries kept in docs/status.md. Coven-Harness: Claude Code / claude-opus-5-5
#283 merged as a4039b2, but the CF-1 block's Still open line still listed "the Markdown parser (#283)". #293 rewrote that line before #283 landed and kept the item, so it stayed on main after #293 merged (57e7607). Name what CF-4 still has open instead: #277's checkpoint lists reference-viewport screenshots and a keyboard and reduced-motion review of the Markdown elements; human VoiceOver is already in the list. Coven-Harness: Claude Code / claude-opus-5-5
… from Still open (#306) * Record #185's downgrades in the approval rules spec The spec's Known limits said commands are clipped before judgment "without a truncation downgrade", and its rule section listed only the destructive and escalation downgrades. Since #185 (2026-10-12), an allow also asks when the full command line chains, redirects or substitutes commands, or when the digest clipped text Jev needed (approvals/gate.ts:155-158, digest.ts:103-111). Add a dated progress note under the enablement list: #185 closed the clipped-command false allow, the quoted-command one is open and the corpus has not been run live since, so item 1 is unmet. The note names the next step. The enablement criteria themselves are unchanged. Coven-Harness: Claude Code / claude-opus-5-5 * Drop the merged Markdown parser from status.md's Still open list #283 merged as a4039b2, but the CF-1 block's Still open line still listed "the Markdown parser (#283)". #293 rewrote that line before #283 landed and kept the item, so it stayed on main after #293 merged (57e7607). Name what CF-4 still has open instead: #277's checkpoint lists reference-viewport screenshots and a keyboard and reduced-motion review of the Markdown elements; human VoiceOver is already in the list. Coven-Harness: Claude Code / claude-opus-5-5
#307) Since #283 the transcript parses Markdown with micromark and imports only safeHref from shared/inline. Two comments still described the old arrangement: - engine/canvas.ts said Canvas renders "the same small Markdown subset as transcript previews". The transcript now renders full GFM; the comment now names Canvas's own subset, which docs/canvas.md already lists. - shared/inline.ts said the renderer's Markdown is a thin mapper over tokenizeInline. Canvas is now its only caller outside the tests. Comments only; no behaviour changes. Coven-Harness: Claude Code / claude-opus-5-5
CF-2 (#286), CF-3 (#293), CF-4's Markdown slice (#283), CF-6's request half (#308) and CF-7's clarifications (#312) are on main, with their merge commits. The candidate note says the fixture rows now run on main, and no candidate is pinned while the live-provider and human gates stay open. Refs #281 Coven-Harness: Claude Code / claude-opus-5-5
…nts at once, reconnect, a clarifying question (CF-8) (#313) * Add the request variants to the fidelity path Four cases in e2e/conversation-fidelity.spec.ts, through the real app: - A native withdrawal: the Claude fixture takes its own request back (control_cancel_request). The card closes as withdrawn, no answer reaches the CLI, a late answer is refused, and the file is unchanged. - The desktop and the phone answer at once, over the bridge and the companion's HTTPS. Exactly one is accepted, the CLI reads one decision, the card names who gave it, and the file matches the winner. - With the journal on, a phone that loaded the open card reconnects after the desktop's decision. Its delta shows the card answered by the desktop and sent, its late answer is a 409, and every count stays at one. - A clarifying question inside the path, from a codex-cli 0.163.0-shaped fixture: answered on its card, heard once, followed by a real edit and check, saved to Notes, and inert after a restart that re-runs nothing. The fixture now counts every decision the CLI reads, so the earlier cases assert that too. The golden path is marked slow: since #301 it also takes 18 reference captures and relaunches, and it ran past 90 s on a Mac at load average 100. These cases found the companion's 404 for threads started on the Mac, fixed in the CF-6 branch. Refs #281 Coven-Harness: Claude Code / claude-opus-5-5 * Record the request variants in the acceptance receipt The matrix rows for a clarifying question, a native withdrawal, simultaneous answers and a reconnect after a decision move from open to verified with a fixture, with what each case asserts. The defects list adds the companion's 404 for threads started on the Mac. The fidelity summary gains rows for questions and approvals. Refs #281 Coven-Harness: Claude Code / claude-opus-5-5 * Record CF-8's request variants in status Refs #281 Coven-Harness: Claude Code / claude-opus-5-5 * Record the landed children in the acceptance receipt CF-2 (#286), CF-3 (#293), CF-4's Markdown slice (#283), CF-6's request half (#308) and CF-7's clarifications (#312) are on main, with their merge commits. The candidate note says the fixture rows now run on main, and no candidate is pinned while the live-provider and human gates stay open. Refs #281 Coven-Harness: Claude Code / claude-opus-5-5
… and the routines spec (#310) v0.2.0 was published on 2026-10-18 (23:14Z) from the signed tag on 01b1b8a. This follows Val's status layout (#290): - Shipped gains a v0.2.0 row linking its release and its review. - The 13 "On main, unreleased" items move unchanged to a new "Status summary" at the end of the v0.2.0 review, along with the old In flight entry. Each one was introduced by a commit in the tag (#248, #253, #254, #257 to #261, #263, #264, #266, #267, #269). The CF slices and #283's Markdown parser came after the tag, so they stay. - In flight keeps a short follow-ups entry for the gates no receipt records yet: the independent download checks, the banner upgrade on the release Mac, and the Companion 0.2.0 (7) TestFlight upload. - The routines spec and the README now say Phase 1, and most of Phase 0, ship from v0.2.0. Unchanged: the website's static fallback (apps/site/release.json) and the release evidence, which stay with the release owner. Claimed on #290. Coven-Harness: Claude Code / claude-opus-5-5
…, lands #295) (#333) * Preserve typed conversation fidelity and streaming reader state (CF-4) Lands #295, which another session wrote, onto current main. It was stacked on #283's pre-squash history and had sat for a day with conflicts. This applies #295's own net change, 74833f7..dad15cd (its commits cce0a40, 00576be, c974d0f and dad15cd), and resolves the conflicts against what has landed since. From #295: typed records take their final answer only from the CLI's own final phase, never by position; failed, denied, stopped and unknown messages and reasoning stay visible, including after a retry; reasoning summaries keep their label while streaming; a streaming reply keeps its code controls, focus and selection; and returning to a thread restores its expansion and scroll position for the session. Its verification record is in docs/reviews/2026-10-18-cf4-completion/. Resolutions: - TurnContent keeps main's footer and #295's remembered expansion. - ThreadView keeps both import additions. - turn-layout keeps both new tests. - typed-turns.spec keeps main's version (setTheme, and the expanded-turn check from #301). #295's selectTheme duplicated setTheme, so conversation-rendering.spec now uses setTheme and the duplicate is gone. - status.md takes #295's corrected streaming sentence and lists the change under On main, unreleased, not In flight. Supersedes #295. Part of #277, tracker #273. Coven-Harness: Claude Code / claude-opus-5-5 * Keep Claude's last reply as its answer; only a phase-typed turn needs an explicit final #295 made every typed turn answer only with an explicit final message. That fixed a real case: a Codex turn of partial answers (codex-cli 0.163.0's partial_answer, stored as unspecified) could make its last partial the final answer by position. It also removed CF-1's documented fallback for a CLI that reports no message phase at all. Claude Code then never showed a final answer, and CF-8's golden path failed waiting for one. finalAnswerIndex now treats a turn as typed when any message carries a final or commentary phase, or any native phase the CLI sent. Such a turn answers only with an explicit final. A turn whose CLI reports no phase (Claude, old history, older Codex models) keeps the last reply, and layoutTurn uses this rule again for the latest attempt. Tests: a Claude turn answers with its last reply (red against #295's rule: final -1, expected 2), and a Codex partial-answer turn never invents one. turn-layout and conversation-events pass 39/39. The golden path, typed-turns, conversation-rendering and conversation-continuity specs pass 22/22. Coven-Harness: Claude Code / claude-opus-5-5 * Restore main's fold expectations for the phase-less offline mock #295 rewrote app, approvals, layout and routines specs for its explicit-final-only rule. For example: "The scripted adapter reports an unspecified phase. Do not invent a final answer by position: its successful work remains visible." The offline mock reports no message phase, so with CF-1's fallback restored (0532b8b) its last reply is the answer again and its finished work folds, as on main. CI failed nine of these tests on 0532b8b ("Expected: 0, Received: 1" final answers). These four specs are main's versions again. Locally, app, approvals, layout, routines, conversation-fidelity and typed-turns pass 50/50. Coven-Harness: Claude Code / claude-opus-5-5
Part of #277 (CF-4), tracker #273. This is the parser slice that the issue says can land before #274. Semantic turn rendering waits for #274's landed contract (PR #282) and is not in this PR: final answer by phase, outcomes, reasoning labels, and Jump to latest.
What changes
Markdown.tsxreplaces its hand-written subset with micromark plus its GFM extension. The parsed tree renders straight to React elements, with no HTML string anywhere.####+ headings, and fences that keep their language (<pre class="code" data-lang="ts"><code>).components/transcript/CodeBlock.tsx): a language label (textwhen there is none) and an accessible Copy. Copy goes through main'sclipboard:writeand nothing else; a status line announces success or failure.http(s)links become links, through the existingsafeHref.javascript:,file:,data:, relative and autolinked non-web targets stay as the text that was written, as before.Image: <alt>, so reading never fetches a remote image.streamingChunks), and only the last piece is parsed again. Each piece is a memoized component.Dependency choice
Pinned exact, as renderer-only
devDependenciesbundled by Vite (like React and xterm):mdast-util-from-markdown@2.0.3(Feb 2026),micromark-extension-gfm@3.0.0,mdast-util-gfm@3.1.0,@types/mdast@4.0.4.micromark is the unified collective's CommonMark reference implementation, with spec-tested GFM extensions. Using it at the mdast level means Modex owns every element it outputs: there's no rehype, no
dangerouslySetInnerHTML, and no component loading.@create-markdown/core(already used by Notes) is a block-editor format, not a CommonMark/GFM parser, so it was not reused.react-markdownwould pull in the whole remark/rehype pipeline for the same parse.Measurements
Apple M3 Max, Node 24.18.1, macOS, renderer code loaded through Vite SSR. The machine was shared with other sessions (load average 21–50), so treat these as relative.
89c98f2)The first-open cost is a real regression, and it is in this PR's evidence for review. A later CF-4 slice can defer parsing of folded work items if it matters in practice. The bundle now crosses Vite's 500 kB advisory warning; nothing gates on it.
Tests
test/markdown-render.test.ts, 11 tests:89c98f2five of them fail for missing behavior. Four pass and stay as safety guards (prefixes, Unicode, raw HTML, link schemes). The two streaming tests failed withstreamingChunks is not a functionbefore it existed.test/markdown-render.test.tsalso covers the bounded settled cache, and the code-block label and Copy markup. Both were seen failing before their code existed.e2e/conversation-rendering.spec.ts, 3 tests, through the real app and mock backend. The two Markdown tests fail with the89c98f2Markdown.tsx swapped in. The Copy test fails with the505b488one: no code-block header. All three pass at head. Copy puts exactly the block's code on the clipboard, and no tool, approval or new turn starts.sources,app,titlesandagentspassed locally: 28/28.Checks on
68b89c8Base
89c98f280292b13fd9b1c7af1bf2fe65cd2fa0d1, head68b89c8dac5436012e498958bf6f4f6b89e88f61. Run locally on macOS (Apple Silicon), Node 24.18.1, pnpm 10.34.5, on a machine shared with other sessions:pnpm install --frozen-lockfilepnpm buildpnpm typecheckpnpm testpnpm test:e2eThe 3 e2e skips are the real-Open-Knowledge cases in
knowledge.spec.ts, which skip without that local install. The 16 unit skips are the suite's existing platform and native skips. This PR adds none.After the local run
135069fchanges only a test helper. CodeQL flaggedvisible()inmarkdown-render.test.tsas "incomplete multi-character sanitization" (high): it stripped tags with a regex before decoding entities. It now skips tags character by character and decodes each entity once. On135069f,markdown-render.test.tspassed 13/13 and the typecheck is clean, and CodeQL passes.68b89c8, so the full local run above still describes what ships.68b89c8(run 39108256492):unit + e2e (macOS)andiPhone companionpassed.After CF-1 (#282) merged
acba096mergesmain(7780093) into this branch with no conflicts. A fresh full local run onacba096(acba09642fe805a451714ace122e7198e6942f92) passed:pnpm install --frozen-lockfilepnpm buildpnpm typecheckpnpm testpnpm test:e2eCI on
135069fpassed every check, CodeQL included.Latest head:
046df0emainkept moving (#284, then #285).ab8474aand046df0emerge them in, both without conflicts.09ae3e5(mainat8e26534merged in): install, build, typecheck andpnpm testall passed.pnpm test:e2eran 324 passed, 3 skipped, 1 failed. The failure,sidebar-details.spec.ts:40, was its setup'sgit inithitting No space left on device while the machine's disk was full. Run alone afterwards, that spec passed 3/3.09ae3e5also makes the Copy e2e click again until Copied shows. CI run 39112253838 had missed once there, likely because a late follow-up suggestion moved the transcript under the pointer. 9/9 local repeats passed.046df0e(mainatfb76aacmerged in): typecheck, build,markdown-render13/13, andconversation-rendering,typed-turns,sourcesandsidebar-detailse2e 15/15. CI on046df0e(run 39116117568) passed every check, including the requiredunit + e2e (macOS). Merge state: CLEAN.74833f7(mainatd3711c6, Show the command that ran, its exit code, bounded output and honest source labels (CF-5, first part) #287 merged in, no conflicts): typecheck, build,markdown-render13/13, andconversation-rendering,typed-turns,sourcesandconversation-evidencee2e 16/16. CI on74833f7passed every check. Merge state: CLEAN.Reshaped at
29a74e8: the settle fix lives in #295After review I found two more defects in this PR:
I fixed both here (
083b8fb,291b08b, verified onfc16df4). Then #295 appeared, another session's CF-4 completion, stacked on this branch at74833f7, with its own fix for the same remount, its own continuity tests and its own theme captures. Keeping both fixes would conflict, so29a74e8hands that defect to #295:Markdown.tsx,markdown-render.test.tsandconversation-rendering.spec.tsreturn to74833f7.transcript-continuity.spec.tsis dropped.Until #295 merges, this PR alone still has the remount-at-settle defect. #295 depends on this PR, so they land in order.
Kept here, because #295 does not touch
styles.css:e2e/markdown-narrow.spec.ts, run in Graphite, Jev and OpenCoven at 900×600, checks:On
f1bbb28(mainat04b7ec7merged in): typecheck, build,markdown-render13/13, andconversation-rendering,markdown-narrow,typed-turnsandsourcese2e 13/13. The full suites are left to CI. The last full local run was onfc16df4, all passed.Ready:
44b21bc44b21bcmergesmainatd3dfe76(#294). Before pushing, typecheck, build and theconversation-fidelity,conversation-renderingandmarkdown-narrowe2e specs (8/8) passed. CI then passed every check. Merge state: CLEAN, current withmain.Not done here (left open on #277)