Repository navigation
Agent chat: streaming replies grow by whole words, no block cursor - #22339
teamleaderleo wants to merge 4 commits into
Conversation
…or after it Leo (2026-10-19): "remove that block ascii blinking cursor thing" and make streaming text fade in by word without layout jumps. The test streams a reply in deltas that cut words in half and checks that no .cv-caret draws, that every word on screen is whole, and that the rest shows once the stream ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The live edge was a 0.5em block bar after the newest character that pulsed while the reveal waited. It is gone: nothing trails the last word. The reveal still paces the text over display frames (StreamReveal), but what shows now ends at a whole word (wordEdge): a word appears once its end has arrived and fades in as one piece (.cv-fresh, opacity only, so nothing reflows), never a letter at a time. A word longer than 48 characters (a URL, a hash) shows as it arrives; once the stream ends all of it shows. Reduce Motion shows text as it arrives, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Probably yours: CI stopped on
Failures:
Matched log linesNot re-run automatically: Written by red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on dfaaee2 to compare; do not merge until green; see lane-rules |
|
Found 17 test failures on Blacksmith runners: Failures
...and 7 more test failures. View all on Blacksmith: run 39573688313, run 39573407946
|
[Low impact] The PR appears safe to merge, with non-blocking polish needed for Japanese character reveals and long tokens. FindingsSummaryStreaming replies reveal whole words without a blinking caret.
Reviews (2) · Last reviewed commit: "fix: a streaming Chinese, Japanese or Ko..." · Reviewed by Greptile |
| while (end > shown && !(end < text.length && /\s/.test(text[end]!) && !/\s/.test(text[end - 1]!))) end -= 1; | ||
| if (limit - end > LONG_WORD_CHARS) return limit; |
There was a problem hiding this comment.
wordEdge restarts the long-word threshold after every reveal. For an uninterrupted URL or hash, raw=49, shown=0 reveals 49 characters, but raw=50, shown=49 still returns 49. It then waits until another 49 characters accumulate. Long tokens freeze and jump instead of showing as they arrive.
Measure the whole token’s length rather than only its hidden suffix, and test successive calls with nonzero shown.
| if (done && limit >= text.length) return text.length; | ||
| let end = limit; | ||
| // The word ending at `end` is whole only when what follows it has arrived and is a space. | ||
| while (end > shown && !(end < text.length && /\s/.test(text[end]!) && !/\s/.test(text[end - 1]!))) end -= 1; |
There was a problem hiding this comment.
Replies without spaces stay hidden
wordEdge recognizes only whitespace as a word boundary. A newly streamed Japanese reply such as 今日はいい天気です has no such boundary and stays below LONG_WORD_CHARS, so its visible length remains zero until the stream ends. Longer Chinese or Japanese passages appear in large threshold-sized jumps.
Use language-aware word boundaries, with a fallback for text without spaces. Add a test that starts empty and receives Japanese or Chinese deltas.
|
Bead |
…at a time Review on #22339: wordEdge only ended a word before a space, so a reply in a script with no spaces held until 48 characters piled up, then jumped. Each Han, kana or Hangul character (and full-width punctuation) is whole now. The fade test runs 8 frames and requires a fading span. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| CJK.test(text[at - 1]!) || | ||
| (at < text.length && CJK.test(text[at]!)); |
There was a problem hiding this comment.
Japanese characters appear in pieces
The new CJK checks can split a Japanese character from its voiced mark. For example, wordEdge("か\u3099", 1, false, 0) returns 1, so the reply briefly shows か instead of が. Half-width バ has the same problem, with the voiced mark appearing and fading in separately.
Keep each base character and its marks together before allowing a reveal boundary, and cover both forms in a test.
|
Orphaned: no activity for 15 h, and no lane holds a lease on it. The orphan sweep could not merge |
…e last line A streaming reply now grows a whole word at a time, each word fading in, instead of a letter at a time (wordEdge from #22339; CJK grows per character, words over 48 chars show as they arrive). The Debug toggle agentPane.debug.streamWords (on) switches back to characters. The transcript keeps 24px of runway under its last row over the bottom stack's room, so the streaming line sits above the composer instead of on its edge. It is always there, so a reply that starts or ends moves nothing. Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray
|
Owned by chief-pet-message-no-persist on cosmic-gray · session |
Conflicts: RevealedMarkdown.tsx keeps base's away/flushSync backlog path and applies wordEdge to the reveal length before it; Markdown.tsx keeps the no-block-cursor comment. Base #22730 already removed the caret CSS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15bbf-a30f-7992-abba-3e3ca6f5adc1&host=cosmic-gray
|
codex-review: SHIP at ceb0a91
|
…e composer and no Markdown flips (#23094) * Agent chat: streaming replies grow by whole words, with room under the last line A streaming reply now grows a whole word at a time, each word fading in, instead of a letter at a time (wordEdge from #22339; CJK grows per character, words over 48 chars show as they arrive). The Debug toggle agentPane.debug.streamWords (on) switches back to characters. The transcript keeps 24px of runway under its last row over the bottom stack's room, so the streaming line sits above the composer instead of on its edge. It is always there, so a reply that starts or ends moves nothing. Co-authored-by: Leo Li <cheerleaderleo@outlook.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * Agent chat: the reload copy of the transcript is written at most every 250 ms A streaming reply sends a snapshot every display frame, and each one serialized the whole transcript into sessionStorage for a reload of the page: main-thread time that grew with the chat, every frame. It is now written at most every 250 ms with the latest snapshot, and at once when the page hides or unloads. Adds the What's New highlight for smooth streaming. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * fix(next): hold half-formed markdown so blocks don't flip while streaming A list marker, heading hashes, rule line, lone emphasis marker or open italic run no longer draws as text first and then turns into its block. A fence closer still being typed is held with the newline before it, so a code card never shrinks a line when it closes, and the open-fence card is read from the parsed block so a held opener can't reopen a closed one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(next): streaming text frame budget harness; highlight docs link Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(next): share the streaming reply fixture; harness saves and preloads as the app does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * gallery(next): streaming text entry, the frame test's reply at both cadences Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(next): a closing fence's highlighted card is as tall as its plain card Fails in WebKit: Pierre's scrollbar strip makes the highlighted card 6px taller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * fix(next): Pierre's scrollbar strip no longer grows a closed code card in WebKit The strip hangs into the card's bottom padding, so text under a closing fence stays put. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(next): the reveal steps on every frame of a 60 Hz display with WebKit's jittery frame times WebKit's frame times are whole milliseconds and jitter around 17 ms, so a 14 ms frame after a 19 ms one fell under the COMMIT_MS - 1 gate and skipped its reveal step: the streaming-text harness counted 64 to 119 stalled frames a reply in WebKit, none in Chromium. A 120 Hz display still steps on every other frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * fix(next): the streaming reveal no longer skips frames in WebKit The reveal loop learns the display's frame time and skips a frame only on a display faster than 60 Hz, so a 60 Hz display commits every frame however its frame times jitter, and a 120 Hz one still commits 60 times a second. One loop per row reads the latest text itself instead of restarting on every snapshot. WebKit stalls in the frame harness: 72 -> 0 (Claude cadence), 121 -> 0 (Codex). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(agent-pane): a reply's end shows no raw Markdown, a held last word shows after a pause Review of #23094: the last word shows a frame after the row stops streaming, and that frame parses without safeTail (a literal backtick). A last word with no space after it stays hidden while the agent works on a tool call. A long word grows in 48-character jumps once part shows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * fix(agent-pane): a reply's end draws no raw Markdown and its last word shows during a pause A reply draws as streaming until all of it shows, so the frame after the stream ends parses the held last word's span with safeTail. The last word counts as whole once no text has come for StreamReveal.stallMs; the loop waits that out without committing. A long word is measured from its start, so once part of it shows the rest grows as it arrives. A tick clears its handle first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * docs(agent-pane): note that a fenced code line starting with a backtick holds until its line ends Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(agent-pane): hold each frame's work to the budget, report intervals CI's react-apps-check shard 3 failed the frame test on 91e0ad3 with a 300 ms frame and a 33.3 ms p95 interval in Chromium. The long frames are Pierre highlighting the closed fence on the main thread: the test's dev page has no highlight worker, the app's page does (highlightPool.ts). The 33 ms intervals came while CI ran other dev-server browser files beside it. Neither is the reveal's work. The harness now times each frame's work (React's render through a Profiler, and the style and layout it caused) and marks frames that waited on a main-thread highlight. The test holds p95 work under 16.7 ms and any frame under 50 ms in both engines; intervals are reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(agent-pane): time every main-thread task in a frame's work budget The work figure counted React's render time and the layout left at the sample, so the layout a commit forces (the follow effect reads scrollHeight), layout effects, resize observers and the reveal's frame callbacks went unmeasured. streaming-text.clock.ts now wraps the task sources before React's scheduler loads (MessagePort onmessage, frame callbacks, timers, queueMicrotask, ResizeObserver) and a frame's work is the script of every task since the last sample plus the layout left. Pierre's synchronous highlighting on this dev page (draws and tokenizing, which the app does in a worker) comes off a frame's work; intervals are tagged by when highlighting ran, so the long interval after it is tagged. Work is wall-clock, so a starved host can break it once: the work budget fails on two runs in a row; layout shifts, flips and pin fail at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(agent-pane): count nested highlighting once, time event listeners Review of e2875bd: WebKit's File.render tokenizes inside itself, and both calls were taken off a frame's work, so a highlight frame went about 1 s negative and could hide real work beside it; only the outermost call counts now. Event listeners are timed too (the transcript's scroll handler, and the layout its reads force), the comments name what stays untimed, a work-budget retry warns, the resize observer callback keeps its `this`, and the budget comment carries the new measurements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray * test(agent-pane): the recap card's fit test leaves the runway under the chat CI at 11a3ffb: "the card leaves the turn's footer in view above it" sized the pane so the last row ended 4 px above the stack, which no longer fits now that the transcript keeps TRANSCRIPT_RUNWAY under its last row (it overflowed with "spare: 24"). It now sizes the pane to the runway plus 4 px. TRANSCRIPT_RUNWAY moves to model.ts with the other row geometry so the test can import it without the pane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d2c-9556-70a3-8034-46aaacf15f3a&host=cosmic-gray --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Leo (2026-10-19, nightly): remove the blinking block cursor in streaming text; text should fade in by word or chunk without layout jumps.
.cv-caretlive edge (a pulsing 0.5em bar after the newest character) is removed.wordEdge(streamReveal.ts) caps the revealed length at a whole word: a word shows once its end has arrived and fades in as one.cv-freshspan (opacity only, so no reflow). Words over 48 chars (URLs, hashes) show as they arrive; the full text shows when the stream ends. Reduce Motion is unchanged.Tests:
RevealedMarkdown.test.tsx(committed first): deltas that split words; asserts no caret, every visible word whole, full text after the stream ends; unit cases forwordEdge.Risk tier: low (webview rendering only, no data or protocol change).
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Streaming agent replies no longer show a blinking block cursor and now grow by whole words instead of single characters, so the text reads more naturally without layout jumps.
feat-cmux-next, which already dropped the caret CSS.RevealedMarkdown.test.tsxcovering mid-word deltas, whole-word visibility, CJK scripts, and long-word passthrough.Written for commit ceb0a91. Summary will update on new commits.