Repository navigation
fix(render): stream text on word boundaries so the glasses don't split words - #37
Conversation
…t words The paced OutputStream sliced a fixed code-point count per frame, so a text_delta could end mid-word (e.g. "hel" then "lo"). The glasses wrap at spaces, so when such a boundary landed near the line edge a word's halves rendered on separate lines. Worse, the small-backlog step (3) is shorter than most words, so short answers split words routinely. frameEnd now takes ~step code points then EXTENDS past the rest of the current word and its trailing whitespace, so every frame ends on a word boundary and a word is never split across frames; a token longer than the remaining text (a bare URL) becomes one frame. Lossless and order-preserving (existing tests still pass); pure + unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELxUVZ1WcJWfYbAdY5uMs4
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0968b2a6fd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * than the whole remaining text (a bare URL) just becomes one frame. Pure + unit-tested. */ | ||
| export function frameEnd(chars: string[], pos: number, step: number): number { | ||
| let end = Math.min(pos + step, chars.length); | ||
| while (end < chars.length && !isSpace(chars[end])) end++; // finish the current word |
There was a problem hiding this comment.
Bound word-boundary scans for unspaced runs
When a queued assistant block contains a long span with no \s characters (for example CJK prose, minified JSON, or a long generated token), this loop advances end all the way to chars.length, so OutputStream emits the entire block as one text_delta instead of pacing it. That regresses the documented streaming behavior for normal no-space text and can make long answers arrive on the glasses in one burst; consider capping the extension or segmenting such text without relying solely on whitespace.
Useful? React with 👍 / 👎.
…l paces The first cut extended a frame to the next whitespace unconditionally, so a long run with no spaces — CJK prose, minified JSON, a bare URL — extended to the block end and emitted the whole block as one text_delta (no pacing / a burst on the glasses). Cap the reach at MAX_WORD_EXTEND (24) past pos+step: extend only when a space is that close (a normal space-delimited word, kept whole), otherwise return the raw step boundary so a spaceless run keeps normal step pacing. Notably fixes CJK, which has no inter-word spaces. Added tests for a CJK run and a long URL both pacing rather than bursting. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELxUVZ1WcJWfYbAdY5uMs4
|
Fixed in 81053d8 — important catch, especially since CJK (the maintainer's language) has no inter-word spaces, so the unconditional extend would have burst whole Chinese paragraphs. Bounded the reach at |
Symptom (user-reported, live test)
Streaming prose on the glasses sometimes rendered a single English word split across two lines — the halves landing on separate lines when a chunk boundary fell mid-word near the line edge.
Cause
OutputStreampaced a fixed code-point count per frame (slice(pos, pos+n)), so atext_deltacould end mid-word ("hel" then "lo"). The glasses wrap at spaces, so a word split across frames could land its halves on different lines. And the small-backlog step is 3 — shorter than most words — so short answers split words routinely.Fix
frameEnd(chars, pos, step)takes ~stepcode points, then extends past the rest of the current word and its trailing whitespace, so every frame ends on a word boundary and no word spans two frames. Extending (rather than backing up to the last space) is what keeps whole a word longer thanstep. A token longer than the remaining text (a bare URL) just becomes one frame.Tests
pnpm checkclean;pnpm test→ 190 pass (+ frameEnd unit tests: word longer than step, last-frame remainder, long-token-whole, and a full-sentence stream that never splits a word).🤖 Generated with Claude Code