Repository navigation
Conversation
Grow the open live tail of a streamed markdown projection in place instead of re-lexing the whole accumulated message on every delta. Previously only a still-open code tail was extended incrementally; ordinary prose, lists, and tables re-lexed the full text on each delta, making a stream of n deltas O(n^2). project() now: - preserves frozen completed blocks and extends an open live tail in place, - re-lexes only the tail when it exceeds LIVE_TAIL_MAX (2048) and the appended text introduces a blank-line block boundary, - falls back to a full re-lex when a late reference definition arrives, so it can still merge with earlier frozen uses, - re-projects the tail when a fence opens inside it, so code mode (and streaming syntax highlighting) engages promptly, - keeps the existing unfinished-code-block fast path and the replacement, truncation, and reset fallbacks. Adds regression tests covering frozen-block identity, bounded tail re-lexing, oversized boundary-less tails, boundary re-projection, lists, tables, links, math, reference definitions (single and split deltas), fences, non-append updates, final incremental vs full projection, and text partition integrity.
Review fixes for the incremental live-tail projection: - stream(): rebuild the live tail raw from the source text instead of marked's normalized token.raw, so a bare trailing list marker such as `1. ` no longer corrupts the accumulated projection. - project(): re-lex from scratch when a frozen block carries a display-math delimiter, repairing the block split produced while `$$...$$` is still open. - project(): heal only the segment after the last blank-line boundary so remend cannot rewrite already-complete earlier blocks (for example strip a leading escaped \[). Adds regression tests for each case.
Add a projection micro-benchmark and a size sweep for the markdown-stream incremental live-tail projection, plus the results in docs/markdown-stream-benchmark.md. No production code changes.
Rename the feature branch to markdown-incremental-projection (no slash/type prefix, three words) and update the port plan and benchmark report references.
Run prettier over the changed files and add the blank lines the anti-slop readable-spacing rule requires in the new tests, so lint:changed passes.
|
The following comment was made by an LLM, it may be inaccurate: |
There was a problem hiding this comment.
The speed-up is real, but once a reply's live tail passes 2048 characters, whether it ever gets split again depends on how the deltas happen to be chunked. I'd fix that before merge.
How I checked: I called project() from a script against the base and this branch with the same delta sequences. A 3,000-word paragraph streamed one word per delta takes 38.7 s on the base and 0.25 s here. I then sent 60 paragraphs with each blank line arriving as two deltas ("…\n" then "\n"). The base gives 60 blocks with a 231-char live tail. This branch ends with a single 13,850-char live block that never gets split, while stream() on the same text gives 60. markdown-stream.test.ts (53) and all session-ui tests (275) pass, and typecheck passes.
Public API: unchanged. Everything here is internal to @opencode/session-ui.
Concerns:
- Split paragraph breaks: the boundary check only looks at the new delta (inline comment). Once the tail stops being split, the renderer re-renders the whole block on every delta, so the slow part moves from the worker to the DOM.
- Leftover docs:
docs/markdown-stream-port-plan.mdis your planning notes (fork, local branches, remotes) and shouldn't be committed.docs/markdown-stream-benchmark.mdcreates a new top-leveldocs/folder just for one report. Please drop both. If the numbers are worth keeping, a short README inpackages/session-ui/performance/markdown-stream/(like the existingmarkdown-lifetimeandhighlightingbenchmarks have) or the PR description is enough. - Display-math check: when the text contains
$,hasFrozenDisplayMathrescans every frozen block on every delta. That keeps a full pass over the message on each delta for any reply that mentions a price or shell variable. It's cheaper than a lex, but please keep it in mind.
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
A blank-line separator arriving split across two deltas was invisible to the boundary check, which only looked at the new suffix, so an over-cap live tail could absorb following blocks and never split. Check the join of the tail's last character with the suffix, and freeze a tail already parked at a blank-line boundary. Frozen blocks keep their identity and the incremental projection still matches a fresh projection.
Drop the top-level planning and benchmark docs and keep the harness notes next to the benchmark under performance/markdown-stream/, matching the other session-ui performance benchmarks.
Add a Results section to the markdown-stream benchmark README with a representative baseline-vs-incremental run and the boundary-less scaling sweep.
|
Benchmark results — Fresh run on this branch (
Boundary-less scaling sweep (
No measured workload regressed, and Scope: this measures |
|
I'm asking for credit for the diagnosis, measurements, and completed solution — not for the implementation that shipped.
What I'm not claiming: What I am claiming: Asks:
I'm closing |
Issue for this PR
Closes #56202
Type of change
What does this PR do?
Ordinary Markdown streaming re-lexes the entire accumulated message text on every delta.
project()inpackages/session-ui/src/components/markdown-stream.tsonly had a fast path for a still-open fenced code tail; for prose, lists, and tables it rebuilt the tail by re-lexing everything, so a stream ofndeltas over a message of final lengthLis O(n·L) ≈ O(L²) and the UI freezes on long responses.This extends the incremental fast path to the live Markdown tail. While the last block is
mode: "live",project()appends the new delta to that block in place instead of re-lexing, preserving already-frozen completed blocks by object identity.The relevant correctness fallbacks remain:
codemode promptly so streaming highlighting engages;heal(remend) only touches the segment after the last blank-line boundary;stream()rebuilds the live tail'srawfrom the source text so a bare trailing list marker (for example1.) no longer corrupts later projections.This also addresses the review note that a blank-line separator arriving across two deltas could leave an oversized tail unsplit.
How did you verify your code works?
bun test src/components/markdown-stream.test.tsinpackages/session-ui— 54 pass / 0 fail.bun test srcinpackages/session-ui— 276 pass / 0 fail.bun typecheckinpackages/session-ui— no errors insession-ui. Pre-existing errors inschemaanduicome from a local Effect version mismatch and are unrelated to this change.Screenshots / recordings
No visual change — this is a projection/performance fix, so there is nothing to capture.
Checklist