emrg: GUI streaming markdown block projection in React TranscriptView - #1112
Conversation
… (rant 2026-09-02T21:07:35) Streaming assistant messages previously rendered as plain text until the done frame (`**bold**`, code fences raw while typing). Restore EMRG's own block-projection engine (方案 B — already ported into markdown.ts, not react-markdown): TranscriptView now projects each delta through streamProject into a host node during typing — closed blocks (paragraphs, lists, closed code fences) render as markdown immediately and are cached in the DOM, while the open tail (esp. unclosed fences via isFenceClosed) stays plain text; on done, streamFinalize performs the one-shot full render. Renderers without marked fall back to the previous plain-text + full renderMarkdown path. Aligns streaming output with TUI and react-markdown competitors (rant 2026-09-02T21:07:35). - TranscriptView.tsx: wire streamProject/streamFinalize into AssistantSegmentView (layout-effect projection per delta, finalize on done/seal, plain fallback when engine unavailable) - TranscriptView.test.tsx: E2E stream test — closed js fence renders while typing, open py fence held as plain .stream-code, done replaces with full markdown (11 tests) - markdown.test.ts: live-fence open/close engine tests (13 tests) - Agent.md: renderer test count 508 -> 511 Verified: renderer typecheck + vitest 511 pass, GUI static suite 93 pass, doc-count guard 5 pass, full pytest 1224 pass/1 skip, renderer build OK.
…ng-markdown # Conflicts: # Agent.md
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260902-213455 (1/3)
Authoring cycle = cyc20260902-212024 (distinct cycle, no ❌). Re-reviewed the merged-tree diff (head cf4db0a, conflict-resolved against #1110's Agent.md 511 → 514):
- TranscriptView.tsx: streaming segments project through the existing block-projection engine (markdown.ts streamProject/streamFinalize) — useLayoutEffect projects each delta before paint into an uncontrolled host node; closed blocks (paragraphs/lists/closed fences) render as markdown immediately with DOM caching, the open tail (unclosed code fences via isFenceClosed) stays plain
.stream-code; done/seal triggers one-shot streamFinalize; renderers without marked fall back to the previous plain-text + full renderMarkdown path (all prior tests untouched and passing). - Tests: engine-level live-fence open/close branches + incremental open→closed convergence (markdown.test.ts, 11 → 13); E2E React test asserting closed js fence renders a .code-block while typing, open py fence held plain, done replaces with full markdown and drops .stream-code (TranscriptView.test.tsx, 10 → 11).
- Agent.md renderer count synced (508 → 511 on branch; 514 after master merge with #1110's Composer 41), doc-count guard green.
CI on merged-tree head cf4db0a: test + test-windows PASS (run 33636014876). MERGEABLE/CLEAN.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260902-214024 (2/3)
Second consecutive vote (1/3 = cyc20260902-213455, authoring cycle = cyc20260902-212024, no ❌ between). Re-checked this cycle: head cf4db0a unchanged (merged-tree conflict resolution vs #1110), MERGEABLE/CLEAN, CI test + test-windows PASS (run 33636014876). No new feedback on the diff since 1/3 — implementation verified sound (block projection during typing, unclosed-fence plain-text hold, done finalize, plain fallback, Agent.md 514).
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260902-214638 (3/3)
Third consecutive vote (1/3 = cyc20260902-213455, 2/3 = cyc20260902-214024, authoring cycle = cyc20260902-212024, no ❌ between). Head cf4db0a unchanged, MERGEABLE/CLEAN, CI test + test-windows PASS (run 33636014876). No new feedback on the diff since 2/3. Merging.
|
I tested this PR at head cf4db0a on the Windows host (independent of CI): renderer vitest 514 passed (45 files, matching the Agent.md count) and `npm run typecheck` is clean. The new fence-projection tests behave as described — a closed `js` fence renders a .code-block while typing, an open `py` fence stays plain .stream-code until closed, and `done` triggers the full-markdown finalize. Fallback to the plain-text + renderMarkdown path when projection is unavailable also looks correct in the component wiring. |
Summary
Fixes the GUI chat showing AI replies as plain text during streaming —
**bold**,# headingand code fences were only formatted after thedoneframe. The TUI and react-markdown competitors render correctly during output (rant ts2026-09-02T21:07:35).Decision: 方案 B — restore EMRG's own block-projection engine (already ported into
lib/markdown.tsfrom the vanilla renderer, G74/G132), NOT react-markdown.Changes
TranscriptView.tsx—AssistantSegmentViewnow projects every streamed delta throughmd.streamProjectinto an uncontrolled host node during typing:isFenceClosed— stays plain text until the fence closes (same heuristic as TUIfence_count%2)done/segment seal,streamFinalizeperforms the one-shot full-markdown correctionrenderMarkdownpathTranscriptView.test.tsx(10 → 11): E2E stream test with a text-driven fake marked — closedjsfence renders a.code-blockwhile typing, an openpyfence is held as plain.stream-code, anddonereplaces the whole stream with full markdownmarkdown.test.ts(11 → 13): engine-level live-fence open/close branch testsAgent.md: renderer test count 508 → 511Verification
npm run typecheckclean; vitest 511 passed