Skip to content

Feed RENDER-layer rewrite: artifact cards, sealed-line streaming highlight, ANSI output#524

Open
Juliusolsson05 wants to merge 23 commits into
mainfrom
feat/feed-render-rewrite
Open

Feed RENDER-layer rewrite: artifact cards, sealed-line streaming highlight, ANSI output#524
Juliusolsson05 wants to merge 23 commits into
mainfrom
feat/feed-render-rewrite

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

What this is

The full RENDER-layer rewrite per docs/superpowers/specs/2026-07-11-feed-render-layer-rewrite-design.md and its 21-task implementation plan (both in this branch). The DECIDE layer (ownership ledger, view bridge, FeedRenderItem contract) is untouched — every change is in the painter and provider renderer modules.

Headline fixes

  • Live code streams highlighted line-by-line with zero remounts. The open-fence path used to rebuild a full Monaco editor + model + LSP document on every streaming token (and remounted again when the fence language arrived a delta late). StreamingCodeBlock seals each completed line into a per-line hljs cache; only the tail line re-tokenizes per delta.
  • Streaming markdown stops re-parsing the whole message per delta (SegmentedMarkdown: the prefix through the last closed fence parses once; O(len²) → O(len)).
  • Command output is ANSI-aware everywhere (AnsiText on the terminal palette; \r progress rewrites collapse) and exit codes finally display (red exit N badges; Codex silent successes render a $ cmd ✓ record instead of vanishing).
  • One card per tool family, identical live and committed (ui/resolve/ + ui/artifacts/): CommandCard, DiffCard (Edit/MultiEdit/apply_patch incl. tinted patch-failure diffs — successes used to render nothing), FileWriteCard (live-highlighted streaming writes), ReadCard, TodoCard, WebCard/ImageGenCard (Codex special tools get committed parity), SlashCommandRow (no more raw <command-name> tag soup), ThinkingBlock (redacted gets an explicit pill), GenericToolCard (the ONE fallback — live MCP tools no longer dump raw partial JSON).
  • Feed.tsx 1060 → ~640 lines; the scarred scroll/picker/debug behaviors are ported verbatim into ui/hooks/. Deleted: both dispatch ladders' tool routing, JsonToolRow/ToolUseRow/DiffSlab-consumers, ClaudeRows, Codex exec/patch/stdin rows, both TruncatedOutputRow copies, renderToolUse from the capability contract.
  • Claude spawn matching accepts Task alongside Agent (future-proofing; current transcripts emit Agent).

Verification

  • tsc clean on both projects; 481/481 tests pass including the bundle + recording corpora (zero divergence — the one rollout change, silent-success results, was absorbed cleanly). The 1 failed test file is the documented pre-existing empty hotkeyBinding suite.
  • electron-vite build + remote-client build pass (kit stays browser-pure; Monaco remains desktop-lazy).
  • Needs a live eyeball before merge (the plan's phase-gate checklist, spec §9): a streaming multi-fence answer, Edit/apply_patch/Write bursts, a colored failing command, WebSearch/MCP/subagent/slash-command rows, provider switch mid-session, and the scroll behaviors.

🤖 Generated with Claude Code

Juliusolsson05 and others added 23 commits July 11, 2026 13:34
Approved design for the full feed painter rewrite: artifact view-model
layer normalizing committed + live planes, one shared component kit,
sealed-line streaming code highlighting, ANSI-aware command output,
per-tool cards for every Claude/Codex artifact family.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Grounded in a four-way audit + first-hand reads of the painter,
streaming path, and kit surfaces. Spec amended: hljs v11 has no
continuation API, so StreamingCodeBlock is stateless-per-line with
finalize repair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Command output has never been ANSI-aware anywhere in the feed — colored
test/build output rendered as literal escape garbage. parseAnsi handles
SGR color/weight (16/256/24-bit), collapses \r progress rewrites, and
strips cursor/OSC noise; AnsiText paints spans with the same
luminance-branched palette the terminal panes use (readXtermTheme), so
feed output and raw terminal output share one color dialect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sealed lines highlight once and cache by index (append cost = new
bytes, not total bytes); only the live tail line re-tokenizes per
delta. blockKey deliberately excludes the language so a late fence
info-string re-highlights once instead of remounting. Uses
toHighlightLanguage to bridge the Monaco-vocabulary names
(typescriptreact/shell) that hljs.getLanguage doesn't know.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r-delta Monaco

The open-fence branch rebuilt a full Monaco editor+model+LSP document
on every streaming token (CodeBlock's Monaco effect lists `code` in
its deps) and remounted again when the fence language arrived a delta
late (language was baked into the key). Live code now paints
highlighted line-by-line with sealed-line caching and zero remounts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nly the tail re-parses

StreamingProse memoizes on the full growing string, so during a stream
the memo never hit and the whole unified pipeline re-ran per delta:
O(len²) per message, and a second fence re-parsed the first through
markdown each delta. SegmentedMarkdown seals everything through the
last CLOSED fence (identity changes only when a fence closes), streams
only the tail, and routes open fences through StreamingCodeBlock.
Applied to both live block text (BlockRow) and blockless turn text
(Feed semantic-text).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
OutputWell is the single collapsible output region (successor to both
TruncatedOutputRow copies — behavior preserved, ANSI + loud byte-cap
added). StatusBadge is the one status vocabulary every artifact card
flips through. ExpandSection generalizes ToolResultRow's private
LazyDetails first-open Monaco gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pies

The shared row and Codex's byte-similar private twin both dumped raw
bytes into <pre> (escape codes rendered as literal garbage) with no
byte cap once expanded. All committed tool output, live Codex function
output, and live tool results now flow through the one kit OutputWell:
same 3-line/expand behavior, ANSI colors, loud truncation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…m port)

Feed.tsx drops from ~1060 to ~640 lines. The scroll family (mount
restore, sticky-bottom listener + auto-scroll, older-history load,
bootstrap pin-once, scroll-to-latest) moves to useScrollFeedBehaviors;
both picker tweens move to usePickerAutoScroll (which returns a stable
cancelTween — scroll-to-latest's one legitimate touch point on the
tween ref); feed-debug emission moves to useFeedDebugEmission. Logic
and WHY comments ported verbatim; zero behavior change intended.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…act)

Types only — the normalization contract both planes resolve into and
every card renders from. Live ≈ committed becomes structural: same VM,
same card, status flip on completion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… one VM

commandFromCommitted/commandFromLive and genericFromCommitted/
genericFromLive produce identical CommandArtifact/GenericToolArtifact
shapes from ToolUseBlock+result pairs and SemanticLiveBlock+snapshot
respectively. Provider wire knowledge moves to pure extractor modules
(claude: order-insensitive slash-command envelope parser, verified
against real transcripts where tag order VARIES between CC versions;
codex: execCommandInput canonical home + result meta reader). Headline
logic reuses the existing corpus-hardened smartHeadline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… + exit codes

Bash / exec_command / local_shell_call / write_stdin route through
routeFamily → CommandCard on BOTH planes: committed interception in
Block.tsx (paired result consumed into the card, suppressed below —
same predicate both branches per the #442 lesson) and live routing in
BlockRow (output streams in via the tool_output_delta fold). Exit
codes finally display (red 'exit N'); git-intent cards still win for
recognized git commands; empty write_stdin keeps its render-nothing
carve-out (renderUnits.isInvisibleWriteStdinBlock mirrors it).
CodexExecCommandRow/CodexWriteStdinRow and the duplicated
execCommandInput are deleted — extractors.ts is the canonical home.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he raw-JSON live dump

MCP and unknown tools render through ONE card on both planes:
JsonToolRow's corpus-hardened per-param presentation preserved, plus
StatusBadge, a result slot (JSON slab / ANSI OutputWell) consumed into
the card, and — the live upgrade — partial input streaming as growing
highlighted JSON via StreamingCodeBlock instead of a raw <pre> dump.
The interim Write preview also moves onto StreamingCodeBlock, so live
file writes are now line-by-line highlighted ahead of the dedicated
FileWriteCard. Suppression is gated by RESULT_CONSUMING_FAMILIES ⊂
landed cards, so un-carded families (apply_patch errors etc.) keep
their legacy result rendering — no data loss mid-migration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slash invocations render as a /name pill + message/args instead of
raw <command-name> tag soup, and the separate local-command-stdout
user entry renders as a muted ANSI OutputWell (real transcripts carry
escape codes in it — verified). The envelope parser is
order-insensitive because tag order varies between CC versions.
Codex rollout stops dropping exit-0/no-output exec results so the
CommandCard can paint the header-only '$ cmd ✓' record; the full
corpus runs clean with no divergence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iEdit/apply_patch

DiffView succeeds DiffSlab (line core + overflow sizer ported
verbatim) adding per-file headers with action badges / ±counts /
RTL-truncated paths and first-40/last-40 windowing for huge diffs.
DiffCard renders Claude Edit+MultiEdit (diffLines) and Codex
apply_patch (grammar parser, moved to extractors with the partial-JSON
wrapper decoder) identically on both planes; live paths show the file
the moment its JSON literal closes and the diff fills in as it streams.
Patch successes finally get a visible ✓ (they rendered NOTHING);
patch failures render tinted per-file unified_diffs inside the card.
Deleted: EditRow/MultiEditRow, CodexApplyPatchRow, PatchFileHeader,
CodexToolRow, the dead patch_apply_end result branch, and the
BlockRow live-tool conversion layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One Write surface for both planes: path + language pill + growing line
counter + StatusBadge over a sealed-line StreamingCodeBlock body. The
live preview is finally syntax-highlighted (the old one was forced
plain because whole-string hljs per delta was quadratic). Finalized
large files offer a lazy Monaco editor view. WriteRow deleted; Write
results consumed by the card (errors render inside it in red).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ource

One card owns both halves of a lookup (the use row was a bare generic
card; the result was special-cased by tool name inside the shared
ToolResultRow — audit gap #9). Codex parsed read/search execs keep
their 'Read N lines from path' summary, now inside CommandCard.
Also fixes a cutover regression: opencode's legacy-claimed read/
todowrite results bypass family suppression (isLegacyProviderClaimed),
so OpencodeReadResult keeps parsing its tag soup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unifies TodoRow (committed) and SemanticTodoList (live) — near-identical
JSX maintained twice. Glyphs preserved (☑ ◐ ☐); the label prop keeps
OpenCode's lowercase todowrite header honest and its dispatch special-
case dies. Also excises a wholesale duplicated dispatch block that a
splice had left in BlockRow (the shadowed copy had a wrong provider tag
and no ReadCard route) — BlockRow is now 336 lines, down from 635.
ClaudeRows.tsx and TodoList.tsx deleted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al tools

Codex web_search / tool_search / image_generation had bespoke live
chips and unrecognizable generic committed rows (the drift the rollout
synthesis comment itself flagged). One card each now renders both
planes; Claude WebSearch/WebFetch join the web family. The committed
synthesized names (web_search/tool_search/image_generation/local_shell)
enter the routing table alongside their live *_call kinds — committed
local_shell rows get CommandCard too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yloads + Task spawn synonym

One thinking renderer for both planes (the two copies had drifted);
redacted_thinking gets an explicit pill instead of silently vanishing;
Codex reasoning shows summary + full tracks when distinct. Image
blocks accept the spec's url source (http(s)-gated) and get a
dependency-free click-to-zoom lightbox. Hook/system entries expose
their payload behind a collapsed ExpandSection. Claude spawn matching
accepts 'Task' alongside 'Agent' (verified: this CLI generation emits
Agent — 184 transcript hits — but newer upstream vocabulary is Task).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wns the paint

renderToolUse leaves the capability contract and all three providers
(every family now has a landed card; routeFamily is total, with a loud
generic-card fallback instead of a silent non-render). ToolUseRow and
the JsonToolRow fallback path in Block.tsx are deleted. renderToolResult
stays: it still carries real behavior for unpaired results (Codex meta-
keyed rows) and OpenCode's legacy-claimed read tag-soup parser — whose
tool_use side now renders the generic card without consuming the result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stage RENDER now describes the resolve/artifacts/kit architecture, the
hooks extraction, and the streaming primitives; stale references to the
deleted dual-ladder dispatch (renderToolUse, JsonToolRow fallback,
EditRow reuse) are gone per the doc's own fix-stale-docs rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The duplicated-block cleanup kept the copy WITHOUT the Write branch, so
a streaming Write fell to the generic card and painted the raw partial
JSON buffer instead of the decoded file preview. The whole point of the
live path is decoding the action WHILE the JSON streams —
extractStreamingWriteInput pulls file_path + unescaped content out of
the incomplete buffer; it just wasn't being called.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant