fix(cli): smoother live streaming preview — drop "generating more" cue, hold back partial table rows - #6340
Conversation
In non-VP mode the live markdown preview is clipped to a rendered-height budget so the frame never overflows the viewport and triggers ink's scroll-to-top full redraw. It used to append a "... generating more ..." cue (and code/math/mermaid blocks appended their own) to signal that the clipped tail was still coming. Since QwenLM#6170 landed the incremental scrollback commit, that tail is streamed into <Static> in real time — clipped content is "still streaming" and reappears within a commit cycle, not "delayed output". The cue is therefore redundant noise that flickers in step with the commit cycle, so remove all four occurrences (outer preview clip, code block, mermaid block, math block). The row each cue used to occupy is reclaimed for content, so the total rendered height is unchanged: the code/math/mermaid RESERVED_LINES drop by one and the outer slice trigger switches from the (now-inlined) `clipped` flag to `keptLines < allLines.length`. The TableRenderer "… more rows streaming …" clamp is intentionally kept — an in-progress oversized table is not yet in scrollback, so that cue still carries information. Also gitignore the nested `.qwen/computer-use/` marker so the auto-generated artifact stops showing up as untracked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the PR! Re-running triage on the updated commits. Template looks good ✓ — all required headings present, bilingual section included, tested-on table filled. Problem: observed visual bug. The "generating more" cue flickers during streaming because it appears/disappears in sync with the incremental scrollback commit cycle (landed in #6170). The table partial-row jitter is also observable — each token flips the frontier row between a stray text line and a table row, causing per-token reflow. Both are real rendering artifacts, not theoretical concerns. Direction: aligned. These are presentation-only polish fixes to the live streaming preview — squarely within the CLI UX mission. Approach: scope feels right. Two complementary visual fixes (remove redundant cue + hold back partial rows) bundled together — both improve streaming smoothness and are tightly coupled in the same code paths. No drive-by refactors, no scope creep. The Moving on to code review. 🔍 中文说明感谢贡献!重新对更新后的 commits 进行 triage。 模板完整 ✓ — 所有必要标题齐全,双语区段完整,tested-on 表格已填。 问题:已观测到的视觉 bug。"generating more" 提示在串流期间会随 incremental scrollback commit 周期闪烁;表格半成品行每个 token 都会在纯文字行和表格列之间翻转,造成逐 token reflow。两者都是真实的算绘瑕疵,不是理论性顾虑。 方向:对齐。仅影响 live 串流预览的呈现层整理——完全在 CLI UX 使命范围内。 方案:范围合理。两处互补的视觉修正(移除多余提示 + 保留半成品列)打包在一起——都改善串流平顺性且紧耦合在同一代码路径。无顺手重构、无范围蔓延。 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewIndependent proposal (before reading diff): To solve the "generating more" flicker, I'd remove the four PR's approach vs mine: matches closely. The PR does exactly what I'd proposed and does it cleanly. Findings:
No correctness bugs, no security concerns, no regressions found. Tests: 130/130 passed ( Real-Scenario Testing (tmux, 80×20 viewport)After (this PR —
|
|
This PR is a clean, well-scoped visual polish that solves a real streaming UX problem. Going back to my independent proposal from Stage 2 — the PR matches it almost exactly: remove the four cue sites, reduce reserved lines, add a streaming-frontier guard for partial table rows. The implementation is straightforward, every edit serves the stated goal, and the test coverage is thorough with good edge cases (first-row partial, non-pending passthrough, non-pipe table close). The motivation is solid: #6170's incremental scrollback commit made the "generating more" cue redundant (clipped content now streams to The tmux testing confirms clean table rendering at 80×20 on the PR code — no cue strings, no stray partial rows, no viewport instability. The per-token flicker is inherently a sub-second artifact that No concerns. Approving. 中文说明这个 PR 是干净、范围合理的视觉整理,解决了真实的串流 UX 问题。 回顾我在 Stage 2 的独立提案——PR 方案几乎完全吻合:移除四处提示、减少保留行数、为半成品表格列加上串流前缘 guard。实作直白,每个改动都服务于既定目标,测试覆盖完整且边界情况良好(首列半成品、非 pending 直通、非管线行关表)。 动机扎实:#6170 的 incremental scrollback commit 让 "generating more" 提示变多余,半成品行翻转是 live 表格 reflow 最明显的来源。两个问题都真实存在;两个修正都是最小改动。 tmux 测试确认 80×20 下 PR 代码表格算绘干净——无提示字串、无游离半成品行、无视窗不稳。逐 token 闪烁本质上是亚秒级瑕疵, 无顾虑,予以批准。 — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
While a markdown table streams, the frontier line is often a half-typed row like `| a | b` with no closing `|` yet. Because TABLE_ROW_RE requires both a leading and trailing pipe, that partial line does not match, so the parser closed the table and rendered the partial as a plain text line below it — then, once the closing `|` arrived, flipped it into the table. This per-token flip changed the frame height and re-ran column autosizing on every keystroke, jittering the live table. Hold the partial row back instead: when pending, if the final line is an unterminated table row and at least one complete row already exists, skip it so `inTable` stays set and the end-of-content handler keeps rendering the accumulated rows as a live table. The row appears the moment it terminates. The `tableRows.length > 0` guard keeps the header + separator from blanking out while the very first row is still being typed. Note: this smooths the table content itself; it does not change the streaming repaint frequency, so the fixed bottom controls still repaint on each tick (that is the domain of the flicker-reduction work, e.g. QwenLM#5396). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Two findings that don't map to diff lines:
[Suggestion] TABLE_PENDING_RESERVED_ROWS stale comment (line 902)
The comment at line 902 still reads reserve 3 rows: marginY 2 + the outer cue, but the outer ... generating more ... cue was removed in this PR. Other renderers reclaimed their cue row (code 2→1, math 3→2, mermaid 2→1), but TABLE_PENDING_RESERVED_ROWS stays at 3. Either reduce it to 2 (matching the pattern) or update the comment to explain why tables keep the extra row.
[Nice to have] Stale JSDoc in pending-rendered-height.ts:135
The JSDoc on keptLines says caller then renders nothing plus a "more" cue rather than an oversized row, but the "more" cue was removed. Should say renders nothing rather than an oversized row.
— qwen3.7-max via Qwen Code /review
| inTable && | ||
| !tableRowMatch && | ||
| index === lines.length - 1 && | ||
| tableRows.length > 0 && |
There was a problem hiding this comment.
[Suggestion] The tableRows.length > 0 guard means the hold-back doesn't fire when the first data row is still being typed. In that case the partial row falls through to the table-closing branch (line 375), which also checks tableRows.length > 0 — so the header+separator are silently discarded and the partial text renders as a plain line below nothing. The comment on line 368 says "header + separator never blank out" but that describes the intent, not the actual behavior.
This is transient (lasts until the closing | arrives), but it's the same jitter the PR aims to prevent — just for the first row instead of subsequent rows. Consider relaxing to tableHeaders.length > 0 (the header+separator are already committed) or adding a separate hold-back for the header+separator when tableRows.length === 0.
— qwen3.7-max via Qwen Code /review
| expect(lastFrame()).toMatchSnapshot(); | ||
| }); | ||
|
|
||
| it('holds back an unterminated trailing table row while streaming', () => { |
There was a problem hiding this comment.
[Suggestion] The four new table-streaming tests cover the main hold-back paths well, but none covers the tableRows.length > 0 guard edge case: a table with header+separator present but the first data row still partial (e.g. | A | B |\n|---|---|\n| one | tw). A test asserting current behavior (header/separator not rendered, partial row as plain text) would document this known limitation and prevent regressions if it's later changed.
— qwen3.7-max via Qwen Code /review
Review follow-up: two comments still referenced the removed outer cue. - TABLE_PENDING_RESERVED_ROWS: reword "marginY 2 + the outer cue" to "marginY 2 + one row of wrapped-cell safety headroom". The reserve stays at 3 on purpose — tables under-estimate their rendered height the most (wrapped cells), so they keep one more backstop row than the other blocks; lowering it would shrink that safety margin. - pending-rendered-height PendingSliceResult.keptLines JSDoc: drop the "plus a 'more' cue" phrasing — the caller now renders nothing rather than an oversized row. Comment-only; no behaviour change. 155 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the review! Both stale-comment findings are addressed in
Note: the branch also gained two commits since the review ran — the |
| if (pendingRenderedBudget !== undefined) { | ||
| const tableClampRows = | ||
| availableTerminalHeight !== undefined | ||
| ? Math.max(2, availableTerminalHeight - 3) |
There was a problem hiding this comment.
[Suggestion] The magic number 3 here duplicates TABLE_PENDING_RESERVED_ROWS = 3 (line ~904) without referencing the constant. Both govern the same physical constraint (rows reserved from the viewport for table rendering), but live 730 lines apart. If someone bumps TABLE_PENDING_RESERVED_ROWS to 4, the slice-side estimate here would undercount, potentially letting a table through the slicer that overflows the render-side cap.
| ? Math.max(2, availableTerminalHeight - 3) | |
| ? Math.max(2, availableTerminalHeight - TABLE_PENDING_RESERVED_ROWS) |
(This requires moving TABLE_PENDING_RESERVED_ROWS above MarkdownDisplayInternal, or to a shared constants block at the top of the file.)
— qwen3.7-max via Qwen Code /review
…onstant Review follow-up on the streaming table hold-back. - The `tableRows.length > 0` guard skipped the hold-back for the FIRST data row: a partial first row fell through to the table-closing branch, which also requires a row, so the header + separator were dropped and the partial rendered as a stray text line — the same per-token flip the change is meant to remove, just for the first row. Relax the guard to `tableHeaders.length > 0` so an unterminated first row/separator is held back too; the table is simply not drawn until its first row terminates, then pops in complete and grows one row at a time. Comment corrected to describe the actual behaviour. - Add a test for that edge case (partial first row held back, table appears once the row terminates). - De-duplicate the magic `3`: the slice-side `tableClampRows` estimate now references `TABLE_PENDING_RESERVED_ROWS` (moved to the top-of-file constants) instead of a literal, so the estimate and RenderTable's render-side `maxHeight` cap can never diverge. 157 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks — all three inline findings addressed in [Suggestion] first-row hold-back guard ( [Suggestion] test for the guard edge case ( [Suggestion] magic 157 tests pass ( |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
LGTM — clean, well-tested presentation improvement. The cue removal and table hold-back logic are sound. All 130 tests pass, build succeeds.
— qwen3.7-max via Qwen Code /review
✅ Maintainer verification — real-TUI E2E + mutation testI built the PR locally and drove the real Verified head 1 · Unit suite + mutation test
Mutation test (revert only
The three remaining new tests ( 2 · Real-TUI A/B — ① streaming table, partial-row hold-backFake stream emits
3 · Real-TUI A/B — ② streaming code block, "generating more" cueFake stream emits an ~80-line python block and pauses while still pending (clip engaged) at 80×20.
4 · The
|
| Scenario | BASE 2J |
FIX 2J |
|---|---|---|
| code block ~80 lines (80×20) | 0 | 0 |
| table ~40 rows (80×20) | 0 | 0 |
| table hold-back (100×30) | 0 | 0 |
Removing the cue and reclaiming its row does not reintroduce viewport overflow — the safety-net clip still fires, so ink never enters its from-top full-redraw path. (This is a no-regression result: the base already carries #6081's clip, so both sides are 0 by design.)
Notes / scope
- Mermaid cue (the 4th removal site) was not independently exercised in the TUI; it is a symmetric trivial edit (reserve
-2 → -1, drop the cue line) identical in shape to the code/math sites which are covered by the mutation test — verified by inspection. - Windows/Linux rendering not tested locally (macOS only);
Test (ubuntu-latest)is green on CI. .gitignore**/.qwen/computer-use/— trivial, by inspection.
Harness: isolated git worktree at the PR head, npm ci in-tree (ink 7.0.3 matches lockfile), node --import tsx/esm packages/cli/index.ts so tsx picks up the source variant live; A/B = revert/restore MarkdownDisplay.tsx only.
🇨🇳 中文版(点击展开)
✅ 维护者验证 — 真实 TUI 端到端 + 变异测试
我在本地构建了该 PR,并在 tmux 中用真实 qwen 二进制对接一个伪 OpenAI SSE 流,在两个版本上于完全相同的串流前缘时刻抓取画面,同时把单元测试套件当作变异测试来跑。结论:PR 的行为与其声明完全一致——提示已移除、半成品表格行被保留、已提交输出完整无损、且无 scroll-to-top 回归。建议合并。
验证的是 head 5fd3702(== GitHub API 的 head.sha)。A/B 的 base 对 MarkdownDisplay.tsx 与当前 origin/main 逐字节相同(git diff --quiet 50d027fe origin/main → 相同),所以下文 "BASE" 列正是维护者今天在 main 上会跑到的版本。
1 · 单元套件 + 变异测试
npx vitest run …/MarkdownDisplay.test.tsx → head 上 130 passed。(描述里的 "128" 早于最后一次 commit,那次追加了第 5 个表格用例;每个新用例都在 Unix 与 Windows 两个换行 describe 块下各跑一次。)
变异测试(只把 MarkdownDisplay.tsx 回退到 origin/main,保留 PR 的测试)→ 10 个定向失败,证明新测试确实为改动承重:
| 失败用例(× Unix & Windows) | 承重点 |
|---|---|
handles a code fence spanning the clip boundary |
code 块 cue 移除 |
does not stack a double cue for a math block … |
math 块 cue 移除 |
applies the minimum floor at a degenerate budget of 1 |
外层 live 预览 cue 移除 |
holds back an unterminated trailing table row … |
表格 hold-back(尾行) |
holds back a partial first row, then pops the table in … |
表格 hold-back(首行) |
其余三个新测试(renders the previously-held frontier row once it terminates、does not hold back a partial row in committed/non-pending output、still closes a streaming table when a non-pipe line follows)在两个版本都通过——这是对的:它们是边界护栏,钉住改动的收敛性(完整行、已提交文本、非 | 行前后必须行为一致)。
2 · 真实 TUI A/B — ① 串流表格,半成品行 hold-back
伪流发出 | # | Fruit | Color | + 分隔行 + 3 行完整行,然后是未收尾的前缘行 | 4 | fo,并在此暂停(用标记文件做确定性同步,无时序竞争)。
(见上方英文版第一张截图)
- BASE:半成品
| 4 | fo溢出为表格下方的一行杂散文字;结尾|到达时它翻牌进表格(每个 token 都改变画面高度并重算栏宽 → 正是本 PR 要消除的抖动)。 - FIX:前缘行被保留——只算绘 3 行完整行,无杂散行。释放后完整表格照常算绘(
| 4 | fox | brown |…| 5 | kiwi | green |),无内容丢失。
3 · 真实 TUI A/B — ② 串流 code 块,"generating more" 提示
伪流发出约 80 行 python 区块,在仍 pending(裁切已生效)时于 80×20 暂停。
(见上方英文版第二张截图)
- BASE:被裁切的预览附加
... generating more ...。 - FIX:提示消失。裁切仍然生效(画面仍在视窗内)——只是移除了那个多余且会闪烁的提示,回收的一行让总高度不变。
4 · TableRenderer clamp 确实保留,且无 scroll-to-top 回归
- 在 80×20 串流约 40 行的表格,BASE 与 FIX 都仍显示
… more rows streaming …——印证 PR 所说该 clamp 是刻意保留的(一张还在写入的超高表格命中的是表格 clamp,而非被移除的外层 cue)。 - 用
tmux pipe-pane统计整段串流算绘期间的clearTerminal(\x1b[2J)风暴:
| 场景 | BASE 2J |
FIX 2J |
|---|---|---|
| code 块约 80 行(80×20) | 0 | 0 |
| 表格约 40 行(80×20) | 0 | 0 |
| 表格 hold-back(100×30) | 0 | 0 |
移除提示并回收那一行,不会重新引入视窗溢出——安全网裁切照常触发,ink 不会进入从顶端整屏重绘的路径。(这是无回归结论:base 已含 #6081 的裁切,故两侧本就应为 0。)
备注 / 范围
- Mermaid 提示(第 4 个移除点)未在 TUI 中独立验证;它是与 code/math 同形的对称小改动(reserve
-2 → -1、删提示行),而 code/math 已被变异测试覆盖——此点靠代码审阅确认。 - Windows/Linux 算绘未在本机验证(仅 macOS);CI 的
Test (ubuntu-latest)为绿。 .gitignore的**/.qwen/computer-use/——琐碎,靠审阅确认。
Harness:在 PR head 建隔离 git worktree、树内 npm ci(ink 7.0.3 与 lockfile 一致)、node --import tsx/esm packages/cli/index.ts 让 tsx 实时读取源码变体;A/B = 只回退/还原 MarkdownDisplay.tsx。
|
@qwen-code /triage |
Follow-up to the streaming table hold-back, on its own branch so the cue-removal PR (QwenLM#6340) can land undisturbed. Makes a live table stream predictably instead of jittering. - Atomic rows: hold a frontier row back until it has ALL its columns. A multi-column row passes through intermediate states that are themselves valid rows with fewer cells (`| a |`, `| a | b |` toward `| a | b | c |`), which the old hold-back let through, so the row filled in cell by cell. Now the whole row (border + every cell) appears in one step. - Widths track the current rows (no freeze): a wider row redraws the whole table once; a narrower row changes nothing (widths are a max over all rows, so they only ever grow). Redraw-on-wider only, never per token. - Bias the streaming preview to the horizontal format: while a table is the live frontier it only falls back to the vertical `label: value` list when the terminal is genuinely too narrow, not because an early row wraps tall. This stops a table from briefly rendering as a vertical list and then flipping to a horizontal table (a visible jump) as more rows arrive. Only the frontier table is affected (isStreamingFrontier); a table closed earlier in the same message, and the final render, use the normal logic. 200 tests pass (MarkdownDisplay + TableRenderer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the streaming table hold-back, on its own branch so the cue-removal PR (QwenLM#6340) can land undisturbed. Makes a live table stream predictably instead of jittering, flashing, or hanging. - Atomic rows: hold a frontier row back until it has ALL its columns. A multi-column row passes through intermediate states that are themselves valid rows with fewer cells (`| a |`, `| a | b |` toward `| a | b | c |`), so the old hold-back let it fill in cell by cell. Now the whole row (border + every cell) appears in one step. - Widths track the current rows (no freeze): a wider row redraws the whole table once; a narrower row changes nothing (widths are a max over all rows, so they only ever grow). Redraw-on-wider only, never per token. - Bias the streaming preview to the horizontal format: while a table is the live frontier it only falls back to the vertical `label: value` list when the terminal is genuinely too narrow, not because an early row wraps tall. This stops a table from briefly rendering as a vertical list and then flipping to a horizontal table (a visible jump). - Hold a forming table back until it is recognizable: a header (and any partial separator) is trimmed while pending until a separator matching the header's column count arrives, so the header no longer streams in char by char as raw `| a | b |` text before snapping into a box. Fenced code-block content is left untouched. - Draw the empty header box as soon as the table is recognized, before the first row completes, so the table area does not sit blank (no box, no cue) and look like a hang if generation stalls in that window. A zero-row box omits the header/body divider so it reads as a clean header, not an empty row. Only the live frontier table is affected; completed and committed tables use the normal logic. 211 tests pass (MarkdownDisplay + TableRenderer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Follow-up to the streaming table hold-back, on its own branch so the cue-removal PR (QwenLM#6340) can land undisturbed. Makes a live table stream predictably instead of jittering, flashing, or hanging. - Atomic rows: hold a frontier row back until it has ALL its columns. A multi-column row passes through intermediate states that are themselves valid rows with fewer cells (`| a |`, `| a | b |` toward `| a | b | c |`), so the old hold-back let it fill in cell by cell. Now the whole row (border + every cell) appears in one step. - Widths track the current rows (no freeze): a wider row redraws the whole table once; a narrower row changes nothing (widths are a max over all rows, so they only ever grow). Redraw-on-wider only, never per token. - Bias the streaming preview to the horizontal format: while a table is the live frontier it only falls back to the vertical `label: value` list when the terminal is genuinely too narrow, not because an early row wraps tall. This stops a table from briefly rendering as a vertical list and then flipping to a horizontal table (a visible jump). - Hold a forming table back until it is recognizable: a header (and any partial separator) is trimmed while pending until a separator matching the header's column count arrives, so the header no longer streams in char by char as raw `| a | b |` text before snapping into a box. Fenced code-block content is left untouched. - Draw the empty header box as soon as the table is recognized, before the first row completes, so the table area does not sit blank (no box, no cue) and look like a hang if generation stalls in that window. A zero-row box omits the header/body divider so it reads as a clean header, not an empty row. Only the live frontier table is affected; completed and committed tables use the normal logic. 211 tests pass (MarkdownDisplay + TableRenderer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to the streaming table hold-back, on its own branch so the cue-removal PR (QwenLM#6340) can land undisturbed. Makes a live table stream predictably instead of jittering, flashing, or hanging. - Atomic rows: hold a frontier row back until it has ALL its columns. A multi-column row passes through intermediate states that are themselves valid rows with fewer cells (`| a |`, `| a | b |` toward `| a | b | c |`), so the old hold-back let it fill in cell by cell. Now the whole row (border + every cell) appears in one step. - Widths track the current rows (no freeze): a wider row redraws the whole table once; a narrower row changes nothing (widths are a max over all rows, so they only ever grow). Redraw-on-wider only, never per token. - Bias the streaming preview to the horizontal format: while a table is the live frontier it only falls back to the vertical `label: value` list when the terminal is genuinely too narrow, not because an early row wraps tall. This stops a table from briefly rendering as a vertical list and then flipping to a horizontal table (a visible jump). - Hold a forming table back until it is recognizable: a header (and any partial separator) is trimmed while pending until a separator matching the header's column count arrives, so the header no longer streams in char by char as raw `| a | b |` text before snapping into a box. Fenced code-block content is left untouched. - Draw the empty header box as soon as the table is recognized, before the first row completes, so the table area does not sit blank (no box, no cue) and look like a hang if generation stalls in that window. A zero-row box omits the header/body divider so it reads as a clean header, not an empty row. Only the live frontier table is affected; completed and committed tables use the normal logic. 211 tests pass (MarkdownDisplay + TableRenderer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): smoother streaming table rendering Follow-up to the streaming table hold-back, on its own branch so the cue-removal PR (QwenLM#6340) can land undisturbed. Makes a live table stream predictably instead of jittering, flashing, or hanging. - Atomic rows: hold a frontier row back until it has ALL its columns. A multi-column row passes through intermediate states that are themselves valid rows with fewer cells (`| a |`, `| a | b |` toward `| a | b | c |`), so the old hold-back let it fill in cell by cell. Now the whole row (border + every cell) appears in one step. - Widths track the current rows (no freeze): a wider row redraws the whole table once; a narrower row changes nothing (widths are a max over all rows, so they only ever grow). Redraw-on-wider only, never per token. - Bias the streaming preview to the horizontal format: while a table is the live frontier it only falls back to the vertical `label: value` list when the terminal is genuinely too narrow, not because an early row wraps tall. This stops a table from briefly rendering as a vertical list and then flipping to a horizontal table (a visible jump). - Hold a forming table back until it is recognizable: a header (and any partial separator) is trimmed while pending until a separator matching the header's column count arrives, so the header no longer streams in char by char as raw `| a | b |` text before snapping into a box. Fenced code-block content is left untouched. - Draw the empty header box as soon as the table is recognized, before the first row completes, so the table area does not sit blank (no box, no cue) and look like a hang if generation stalls in that window. A zero-row box omits the header/body divider so it reads as a clean header, not an empty row. Only the live frontier table is affected; completed and committed tables use the normal logic. 211 tests pass (MarkdownDisplay + TableRenderer). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): guard the two remaining zero-row / non-table edge cases Review follow-up (two [Critical] findings). - TableRenderer: the maxLineWidth safety check is a second path to the vertical format, unguarded for zero-row tables. On a very narrow terminal a zero-row streaming header box would fall through it and render an empty string — the box vanishes. Skip that fallback when there are no rows so the header stays visible even if it slightly overflows. - MarkdownDisplay: the pre-loop header hold-back trimmed ANY trailing run of pipe-leading lines. When the first line is not a complete `| … |` row, headerCells was 0 and the run was trimmed anyway — so non-table pipe text (an un-fenced shell pipeline `| grep foo`, pipe-prefixed log output) would vanish from the live preview until commit. Only hold back when the first pipe-line is a plausible table header (a complete row). Tests cover both. 215 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): hold a multi-column header mid-type without hiding pipe text The previous commit (restricting the header hold-back to a complete `| … |` row, to stop non-table pipe text from vanishing) reintroduced the cell-by- cell header flash: while a header is typed (`| Alpha`, `| Alpha | Bet`, …) it is not yet a complete row, so it rendered as raw text. Discriminate by column count instead of closed-ness: a table header has ≥2 columns; a single-pipe line (shell pipeline `| grep foo`, pipe-prefixed log) has one cell. Count cells on the first line whether or not it is closed, and hold the run only when it has ≥2 columns and no matching separator yet. So a multi-column header held mid-type no longer flashes, while single-pipe non- table text still renders (the earlier [Critical] fix stands). A header still typing its very first cell is indistinguishable from a single-pipe line, so it shows briefly until the second column appears — the narrowest flash possible without hiding real pipe text. 217 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): make table format decision consistent, not streaming-biased The horizontal-vs-vertical bias (force a live table horizontal while streaming) backfired for tables that genuinely belong in the vertical `label: value` format — a wide table with many columns of long, wrapping text. It rendered horizontal (tall, clamped, looking stuck) while it was the streaming frontier, then flipped to vertical the moment it stopped being the frontier (the next block started) or committed — a visible format flip, and worse than the vertical-list flash it was meant to avoid. Drop the streaming bias: the horizontal-vs-vertical decision is now the same while pending and once committed, so a table never flips format between the two. Removes the now-unused isStreaming / isStreamingFrontier plumbing. Known residual (pre-existing, not from this change): because column widths track content (redraw-on-wider), a borderline table's wrapped-row height can still cross the vertical threshold mid-stream. Fully stabilizing that needs a content-independent format decision — a separate change. 217 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(cli): note the redraw-on-wider format-oscillation trade-off Document the accepted limitation next to the horizontal-vs-vertical decision: because column widths track content (redraw-on-wider), a table with very long cell text sitting right at MAX_ROW_LINES can still oscillate format while streaming. Only extreme wide/long-text tables hit it; the alternatives (content-independent decision, or frozen widths) each cost more than the residual is worth. Comment-only; no behaviour change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): count held-back header columns like the table detector The streaming hold-back counted header columns on the full line with empty cells filtered out, while the main table detector strips the outer pipes and splits without filtering. For a header with an empty-named column like `| A || B |` the two disagreed (2 vs 3), so the hold-back never found the matching 3-column separator and hid the table for the whole stream. Count columns the same way in both places. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): release multi-cell non-table pipe content during streaming The streaming hold-back keeps a run of pipe-lines back until a matching separator arrives, so a real multi-column header does not flash in cell by cell. But multi-cell non-table pipe content — a shell pipeline (`| grep foo | wc -l`), a log excerpt (`| 200 | OK | GET /x`), an ASCII-art border — also has >=2 cells, so it was held for the entire stream and only appeared on commit. A markdown table's separator is the line immediately after the header, so once a line follows the header and does not even begin like a separator (optional pipe, optional colon, then a dash), the run is decided: not a forming table. Release it. A lone header still being typed (no line after it yet) is still held, so the no-flash behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): anchor the vertical-format decision to the first row (no flip) The horizontal-vs-vertical choice used maxRowLines measured over EVERY row, so a table that started horizontal (short first row) flipped to vertical the moment a later, taller-wrapping row streamed in — a visible mid-stream format change. Measure only the header + the first data row instead. The first row is representative for the common case, so the format is decided once and stays put as rows append. Column widths still track all rows (redraw-on-wider is unchanged); only the format choice is anchored. Trade-off: a table whose first row is short but a later row wraps very tall stays a (taller) horizontal grid rather than flipping to vertical — rare, and preferable to a visible flip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): release a dash-led data row from the streaming hold-back The "could this pipe run still become a table?" check treated any line after the header that merely started with a dash as a possible separator, so an options table whose first data cell begins with a flag — `| --verbose | … |` — was held back for the whole stream. Use tableSeparatorRegex instead: it still matches a partial separator being typed (`|--`) so a real header is held until its separator lands, but rejects a dash-led data cell (trailing letters), which now renders live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): defer a streaming table until its first row (no empty-box flip) A recognized table with no complete data row yet was drawn immediately as an empty header box. A zero-row table can only render horizontally (the vertical fallback needs rows), so once a long first row landed the box flipped to the vertical label:value format — a visible format change that cannot be avoided by looking at the header alone (column names are short; width comes from the values). Defer the table while pending until its first row completes, so it first appears already in its final format with no flip. Cost: the table area stays blank while the header + first row stream (the pre-loop trim already hid the header text, so this only extends that blank). Committed tables always have rows, so their behavior is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): address review — code-fence tracking, held-back edge cases, committed format Five review findings: - [Critical] The pre-loop hold-back's code-fence check used a naive boolean toggle that ignored fence char/length, so a nested fence (```` with an inner ```) mis-closed and a real code line like `| A | B |` was held back and vanished while streaming. Track the open fence's delimiter and validate the close (same char, >= length), mirroring the main parser. - A COMPLETE separator whose column count already differs from the header can never match, so release the pipe run instead of holding it for the whole stream (the main parser treats it as text). - The end-of-content table flush now uses the same `tableRows.length > 0` guard as the mid-content handler, so a degenerate zero-row table behaves the same whichever way it ends — no EOF-vs-mid asymmetry. - TableRenderer's first-row-only maxRowLines (no-flip) applied to committed tables too; a committed short-first-row + tall-later-row table wrongly stayed horizontal. Gate on a new `isPending` prop: measure the first row only while streaming, every row once committed (most readable, no flip concern). - Renamed the test block that claimed a nonexistent `isStreaming` prop; added committed-vs-streaming format tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): don't flip a completed mid-content table's format at commit A table closed by a following line is complete even while the message keeps streaming, but it was still rendered with the first-row-only format anchor — so a short-first-row + tall-later-row mid-content table showed horizontal and then flipped to vertical the moment the message committed. Split the two concerns that were both riding on `isPending`: - the height clamp still tracks whether the MESSAGE is streaming (so a mid-content table stays bounded and the estimator's clamped cost can't under-estimate the render); - the format anchor now tracks whether THIS TABLE is the streaming frontier. The mid-content flush passes isFrontier={false} → all rows measured → final format now; only the end-of-content (frontier) table anchors to the first row. Renamed TableRenderer's format-anchor prop to `isStreaming` (it is not the message-level pending flag). Added mid-content and tilde-fence tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(cli): don't hold a pipe line inside an open $$ math block The streaming table hold-back tracked code fences so a `| A | B |` code line would render, but not display-math (`$$ … $$`) blocks. The main parser pushes math content verbatim (never as a table), so a `| a | b |` norm/matrix line at the frontier of an open math block was treated as a forming table and blanked until the block closed. Track math fences in the trim's fence scan too, mirroring the main parser's precedence (code block wins, then math), and skip the hold-back while inside one. Addresses the low-confidence review observation on QwenLM#6345. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>


What this PR does
Two related tidy-ups to the non-VP live streaming markdown preview:
Drop the
... generating more ...cue. The preview is clipped to a rendered-height budget so the frame never overflows the viewport (which would trigger ink's scroll-to-top full redraw). It used to append a... generating more ...line under the clipped head, in four places: the outer preview clip and the code / mermaid / math blocks. All four are removed and the single row each cue occupied is reclaimed for real content, so the total rendered height is unchanged. TheTableRenderer… more rows streaming …clamp is deliberately kept.Hold back the unterminated table row while streaming. While a table streams, the frontier line is often a half-typed row like
| a | bwith no closing|yet. Since a table row needs both a leading and trailing pipe, that partial line did not match, so the parser closed the table and rendered the partial as a plain text line below it — then flipped it into the table once the closing|arrived. That per-token flip changed the frame height and re-ran column autosizing on every keystroke, jittering the live table. Now the partial row is held back (when at least one complete row already exists) so the table renders steadily and the row appears the moment it terminates.It also gitignores the nested
.qwen/computer-use/marker so that auto-generated artifact stops showing up as untracked.Why it's needed
The cue existed to tell the user the clipped tail was still coming. Since #6170 landed the incremental scrollback commit, that tail is streamed into
<Static>in real time — clipped content is "still streaming" and reappears within a commit cycle, not "delayed output". The cue is therefore redundant, and because it is emitted in step with the commit cycle it visibly flickers on and off. TheTableRendererclamp is different and kept on purpose: an in-progress oversized table is not yet committed to scrollback, so… more rows streaming …still carries information.The table hold-back removes the most visible source of live-table jitter — the partial-row flip-flop that reflowed the table on every token.
Reviewer Test Plan
How to verify
Ask the model for a long markdown table (~60 rows) and watch the stream at two terminal sizes:
... generating more ...appears anywhere; the live table grows one complete row at a time without the per-token flip/reflow; the… more rows streaming …clamp still shows on the in-progress oversized table (kept by design); scrolling up mid-stream does NOT jump the viewport to the top and lock it; on completion the full table renders.Also stream a long code block (~80 lines) and confirm no
... generating more ...at either size.Unit tests:
npx vitest run packages/cli/src/ui/utils/MarkdownDisplay.test.tsx→ 128 passed (includes four new cases: a partial frontier row is held back, it appears once terminated, a committed/non-pending partial row is not held back, and a non-pipe line still closes the table).Evidence (Before & After)
... generating more ...line renders under the clipped head and flickers with each scrollback commit; a streaming table flips its frontier row between a stray text line and a table row on every token, reflowing the table.... generating more ...at any viewport size; the streaming table grows one complete row at a time with no per-token reflow. The… more rows streaming …table clamp is unchanged.Tested on
Environment (optional)
Local
npm run devon macOS.Risk & Scope
useGeminiStream.ts(one comment only), which overlaps the file edited by fix(ui): reduce UI flicker — throttle + startTransition + batch STREAM_TEXT + debounce refresh + resize settle #5396 — trivial to resolve.Linked Issues
Relates to #6170 (incremental scrollback commit that makes the cue redundant) and #5941 (original scroll-to-top lock, already fixed by #6170). No closing keyword.
中文说明
这个 PR 做了什么
针对 non-VP 模式的 live 串流 markdown 预览,做两处相关的整理:
移除
... generating more ...提示。 预览会被裁切到「算绘高度」预算内,避免画面超出视窗(超出会触发 ink 从顶端整屏重绘)。原本会在裁切后的头部下方附加一行... generating more ...,共四处:外层预览裁切,以及 code / mermaid / math 区块。四处全部移除,且每个提示原本占用的那一行改还给实际内容,因此总算绘高度不变。TableRenderer的… more rows streaming …clamp 刻意保留。串流中保留还没输入完的表格列。 表格串流时,最前缘那一行常是还没收尾的
| a | b(缺结尾|)。因为表格列需要首尾都有|,这半成品行不符合,于是 parser 会关闭表格、把它画成表格下方的一行纯文字,等结尾|到了再翻进表格。这个「每个 token 翻牌」会改变画面高度并重算栏宽,让 live 表格抖动。现在改成把半成品行保留(在已有至少一列完整资料时),表格稳定成长,该列一收尾就出现。另外把巢状的
.qwen/computer-use/标记加入 gitignore,让这个自动产生的档案不再显示为未追踪。为什么需要
提示原本是要告诉使用者被裁掉的尾巴还在后面。但自从 #6170 加入 incremental scrollback commit,那段尾巴会即时串流进
<Static>——被裁的内容是「还在串流」、会在一个 commit 周期内重新出现,而非「被延迟的输出」。因此提示多余,且它随 commit 周期出现/消失、会明显闪烁。TableRenderer的 clamp 情况不同、刻意保留:一张还在写入的超高表格尚未提交进 scrollback,所以… more rows streaming …仍带有资讯。表格保留则移除了 live 表格最明显的抖动来源——每个 token 让表格 reflow 的半成品行翻牌。Reviewer 测试计划
请模型产生一个长 markdown 表格(约 60 列),在两种视窗大小下观察串流:
... generating more ...;live 表格每次长一整列、没有逐 token 的翻牌/reflow;… more rows streaming …clamp 仍显示在还在写入的超高表格上(刻意保留);串流中往上滚动不会跳到最顶并锁死;结束后完整表格正常算绘。另外串流一段长 code 区块(约 80 行),确认两种大小下都不出现
... generating more ...。单元测试:
npx vitest run packages/cli/src/ui/utils/MarkdownDisplay.test.tsx→ 128 passed(含四个新案例:保留半成品前缘行、收尾后出现、非 pending 的半成品行不保留、非管线行仍正常关表)。风险与范围
useGeminiStream.ts(仅一行注解),与 fix(ui): reduce UI flicker — throttle + startTransition + batch STREAM_TEXT + debounce refresh + resize settle #5396 编辑同一档案,冲突极易解决。关联 Issue
关联 #6170(让提示变多余的 incremental scrollback commit)与 #5941(原始 scroll-to-top 锁死问题,已由 #6170 修复)。未使用关闭关键字。