mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(web): decode od-card in the thinking stream The thinking stream was the one prose channel that never saw the shared od-card parser: `ExecutionShell` handed `kind:'thinking'` text straight to `ThinkingMarkdown` -> `renderMarkdown`, with no `splitShellCards` anywhere on the chain. A perfectly valid `<od-card type="memory-applied">…</od-card>` written into reasoning therefore reached the user as raw tag text — the same symptom #7956 closed for in-shell narration (OPEND-2607), on the third channel. Route thinking prose through `splitShellCards`, the same parser `SayBlock` and `AssistantMessage` already use, so there is exactly one answer to "which bytes of a model message are a protocol card and which are user-readable prose". Streaming semantics come from that parser too: a half-written opener waits for the next delta instead of flashing raw, and an unclosed card does not swallow the prose in front of it. Deliberately unchanged: `splitOnOdCards` still keeps a malformed card as raw text ("Malformed — keep raw text so the user can still see it"). That is a separate product call and is not touched here. Refs OPEND-2745 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(web): keep thinking-stream cards out of the char reveal Rendering `OdCardView` inside `ThinkingMarkdown` put it under that component's single char-reveal root, and `useCharReveal` walks the whole subtree. So the frame where a streaming card closed, the visible-text length jumped and the reveal shredded the card's OWN text nodes into per-character `data-char-reveal` spans — truncating React-owned nodes React was still updating, which is the exact hazard `useCharReveal`'s header warns about. Mark the card subtree with `[data-no-reveal]`, the opt-out that hook already declares (`SKIP`) for content that sits inside a reveal root without being prose the model is typing. This is its first consumer. `collect()` and `measure()` share the filter, so a card's text no longer counts toward the length delta either: a card appearing can no longer be mistaken for a burst of new characters that re-reveals prose which never changed. `SayBlock` needs no such attribute because its reveal roots live INSIDE `SayText`, one per prose run, so cards are siblings of every root rather than descendants. Thinking cannot copy that shape: the reveal budget, the "settled on mount" state and the module-level `claimHistoryReplayLanded` token are all per-root, so N roots would mean N independent budgets and a replay token only the first root claims — breaking OPEND-2590's "replayed history does not re-reveal" for the thinking slot. One root plus a subtree opt-out keeps both properties. Refs OPEND-2745 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>