Skip to content

fix(cli): smoother live streaming preview — drop "generating more" cue, hold back partial table rows - #6340

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
MikeWang0316tw:fix/remove-generating-more-cue
Jul 5, 2026
Merged

fix(cli): smoother live streaming preview — drop "generating more" cue, hold back partial table rows#6340
wenshao merged 4 commits into
QwenLM:mainfrom
MikeWang0316tw:fix/remove-generating-more-cue

Conversation

@MikeWang0316tw

@MikeWang0316tw MikeWang0316tw commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Two related tidy-ups to the non-VP live streaming markdown preview:

  1. 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. The TableRenderer … more rows streaming … clamp is deliberately kept.

  2. Hold back the unterminated table row while streaming. While a table streams, the frontier line is often a half-typed row like | a | b with 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. The TableRenderer clamp 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:

  • 80×20 (short viewport): during streaming, no ... 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.
  • 134×65 (large viewport): the table fits within the height so nothing is clamped — no cue strings appear at all, the table streams smoothly, and scrolling stays stable.

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)

  • Before: a dim ... 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.
  • After: no ... 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

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

Local npm run dev on macOS.

Risk & Scope

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 预览,做两处相关的整理:

  1. 移除 ... generating more ... 提示。 预览会被裁切到「算绘高度」预算内,避免画面超出视窗(超出会触发 ink 从顶端整屏重绘)。原本会在裁切后的头部下方附加一行 ... generating more ...,共四处:外层预览裁切,以及 code / mermaid / math 区块。四处全部移除,且每个提示原本占用的那一行改还给实际内容,因此总算绘高度不变。TableRenderer… more rows streaming … clamp 刻意保留。

  2. 串流中保留还没输入完的表格列。 表格串流时,最前缘那一行常是还没收尾的 | 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 列),在两种视窗大小下观察串流:

  • 80×20(矮视窗): 串流中任何位置都不该出现 ... generating more ...;live 表格每次长一整列、没有逐 token 的翻牌/reflow;… more rows streaming … clamp 仍显示在还在写入的超高表格上(刻意保留);串流中往上滚动不会跳到最顶并锁死;结束后完整表格正常算绘。
  • 134×65(大视窗): 表格放得下、不会被 clamp——完全不出现任何提示字串,表格串流顺畅,滚动稳定。

另外串流一段长 code 区块(约 80 行),确认两种大小下都不出现 ... generating more ...

单元测试:npx vitest run packages/cli/src/ui/utils/MarkdownDisplay.test.tsx → 128 passed(含四个新案例:保留半成品前缘行、收尾后出现、非 pending 的半成品行不保留、非管线行仍正常关表)。

风险与范围

关联 Issue

关联 #6170(让提示变多余的 incremental scrollback commit)与 #5941(原始 scroll-to-top 锁死问题,已由 #6170 修复)。未使用关闭关键字。

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>
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

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. terminal-ux is on the roadmap. No product-direction concerns.

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 TABLE_PENDING_RESERVED_ROWS de-duplication (moved from RenderTable to module scope) is a clean constant-consolidation, not a drive-by.

Moving on to code review. 🔍

中文说明

感谢贡献!重新对更新后的 commits 进行 triage。

模板完整 ✓ — 所有必要标题齐全,双语区段完整,tested-on 表格已填。

问题:已观测到的视觉 bug。"generating more" 提示在串流期间会随 incremental scrollback commit 周期闪烁;表格半成品行每个 token 都会在纯文字行和表格列之间翻转,造成逐 token reflow。两者都是真实的算绘瑕疵,不是理论性顾虑。

方向:对齐。仅影响 live 串流预览的呈现层整理——完全在 CLI UX 使命范围内。terminal-ux 已在路线图上,无产品方向顾虑。

方案:范围合理。两处互补的视觉修正(移除多余提示 + 保留半成品列)打包在一起——都改善串流平顺性且紧耦合在同一代码路径。无顺手重构、无范围蔓延。TABLE_PENDING_RESERVED_ROWS 去重(从 RenderTable 搬到模组层级)是干净的常数整并,不是顺手改。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal (before reading diff): To solve the "generating more" flicker, I'd remove the four <Text> elements that emit the cue (outer clip, code block, mermaid, math), reduce RESERVED_LINES in each block by 1 to reclaim the freed row for real content, and update tests. For the table jitter, I'd add a guard in the line parser that detects an unterminated final row during streaming and skips it so the parser stays in inTable state.

PR's approach vs mine: matches closely. The PR does exactly what I'd proposed and does it cleanly.

Findings:

  • Cue removal — all four sites (MarkdownDisplay outer clip, RenderCodeBlock, RenderPendingMermaidBlock, RenderMathBlock) correctly stripped. The pendingClipped flag removed since it was only used for the cue. The keptLines < allLines.length replacement check is correct and simpler.
  • Reserved line adjustments — code block 2→1, mermaid 2→1, math 3→2. All correct: one row of headroom remains for rendered-height estimation error, the cue row is reclaimed.
  • Table hold-back logic — five conditions correctly gate the behavior: isPending (only during streaming), inTable, !tableRowMatch (not a complete row), index === lines.length - 1 (only the frontier), tableHeaders.length > 0 (table structure exists), /^\s*\|/ (line starts a pipe — it's a partial row, not a line that closes the table). Edge cases covered: non-pipe line still closes the table, committed output renders partials as-is.
  • TABLE_PENDING_RESERVED_ROWS de-dup — moved from inside RenderTable to module scope so both fitPendingSlice (slice-side) and RenderTable (render-side) share one constant. Prevents drift. Good.
  • .gitignore — adds **/.qwen/computer-use/ for nested packages. Benign, out of the critical path.

No correctness bugs, no security concerns, no regressions found.

Tests: 130/130 passed (MarkdownDisplay.test.tsx), 27/27 passed (pending-rendered-height.test.ts). Five new table hold-back test cases cover the key edge cases (partial held back, complete shown, first-row partial, non-pending unaffected, non-pipe closes table).

Real-Scenario Testing (tmux, 80×20 viewport)

After (this PR — npm run dev)

Table streams cleanly with all 60 rows rendered in box-drawing format. No "generating more" cue at any point during streaming. No stray partial rows visible.

  ◆
    ┌────────┬────────┬────────┐
    │ Number │ Square │ Cube   │
    ├────────┼────────┼────────┤
    │ 1      │ 1      │ 1      │
    ├────────┼────────┼────────┤
    │ 2      │ 4      │ 8      │
    ├────────┼────────┼────────┤
    │ 3      │ 9      │ 27     │
    ...
    │ 58     │ 3364   │ 195112 │
    ├────────┼────────┼────────┤
    │ 59     │ 3481   │ 205379 │
    ├────────┼────────┼────────┤
    │ 60     │ 3600   │ 216000 │
    └────────┴────────┴────────┘

    60 rows generated.

Before (installed qwen v0.19.6)

Same prompt, same viewport. Table renders identically — no "generating more" in any mid-stream capture. The installed build already includes the incremental scrollback commit (#6170) which reduced the cue's information value; the PR removes the now-redundnant visual artifact.

  ◆
    ┌────────┬────────┬────────┐
    │ Number │ Square │ Cube   │
    ├────────┼────────┼────────┤
    │ 1      │ 1      │ 1      │
    ...
    │ 60     │ 3600   │ 216000 │
    └────────┴────────┴────────┘

Note: capture-pane takes point-in-time snapshots, so the transient per-token flicker (the PR's primary motivation) can't be caught at 1-5s intervals. The unit tests and code review are the primary evidence that the flicker source (partial-row flip-flop + cue on/off cycle) has been removed.

中文说明

代码审查

独立提案: 移除四个 "generating more" 的 <Text> 元素,各区块 RESERVED_LINES 减 1 以回收空间;表格抖动则加 guard 跳过串流中的半成品尾行。

PR 方案对比: 高度吻合,且实作干净。

  • 四处提示移除正确、pendingClipped flag 移除合理
  • 保留行数调整正确(code 2→1、mermaid 2→1、math 3→2)
  • 表格保留逻辑五个条件精准:仅串流中、仅前缘行、仅半成品
  • TABLE_PENDING_RESERVED_ROWS 去重防止 slice/render 两侧漂移
  • .gitignore 无害

无正确性 bug、无安全顾虑、无回归。

测试: 130/130 + 27/27 通过。五个新表格测试覆盖关键边界情况。

真实场景测试(tmux,80×20 视窗)

PR 代码:表格串流干净,60 列全部以 box-drawing 格式呈现,无 "generating more" 提示。
安装版 qwen v0.19.6:同样干净。capture-pane 为瞬间快照,无法捕捉逐 token 闪烁——单元测试和代码审查是主要证据。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

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 <Static> within a commit cycle), and the partial-row jitter was the most visible source of live-table reflow. Both problems are real; both fixes are minimal.

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 capture-pane can't freeze-frame, but the code review confirms both flicker sources have been eliminated (cue removal + hold-back logic).

No concerns. Approving.

中文说明

这个 PR 是干净、范围合理的视觉整理,解决了真实的串流 UX 问题。

回顾我在 Stage 2 的独立提案——PR 方案几乎完全吻合:移除四处提示、减少保留行数、为半成品表格列加上串流前缘 guard。实作直白,每个改动都服务于既定目标,测试覆盖完整且边界情况良好(首列半成品、非 pending 直通、非管线行关表)。

动机扎实:#6170 的 incremental scrollback commit 让 "generating more" 提示变多余,半成品行翻转是 live 表格 reflow 最明显的来源。两个问题都真实存在;两个修正都是最小改动。

tmux 测试确认 80×20 下 PR 代码表格算绘干净——无提示字串、无游离半成品行、无视窗不稳。逐 token 闪烁本质上是亚秒级瑕疵,capture-pane 无法冻结画面,但代码审查确认两个闪烁源都已消除(提示移除 + 保留逻辑)。

无顾虑,予以批准。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@MikeWang0316tw MikeWang0316tw changed the title fix(cli): drop redundant "generating more" cue from the live preview fix(cli): smoother live streaming preview — drop "generating more" cue, hold back partial table rows Jul 5, 2026

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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>
@MikeWang0316tw

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Both stale-comment findings are addressed in f1b318030 (comment-only, no behaviour change):

  • TABLE_PENDING_RESERVED_ROWS (line 902) — I kept the value at 3 and updated the comment instead of reducing to 2. The clamp is a wrapped-cell overflow backstop, and tables under-estimate their rendered height the most (a wrapped cell), so they intentionally keep one more reserved row than the other blocks — lowering it would shrink that safety margin (this margin is exactly what the fix(cli): stream long responses into scrollback to stop scroll-to-top lock #6170 scroll-to-top fix guards). Comment now reads marginY 2 + one row of wrapped-cell safety headroom rather than referencing the removed outer cue.
  • pending-rendered-height.ts:135 JSDoc — reworded to renders nothing rather than an oversized row (dropped the "more" cue phrasing).

Note: the branch also gained two commits since the review ran — the … more rows streaming … clamp is unchanged, but I added a fix that holds back the unterminated frontier table row while streaming (removes the per-token flip/reflow jitter). Test count is now 128 for MarkdownDisplay.test.tsx. PR body updated accordingly.

if (pendingRenderedBudget !== undefined) {
const tableClampRows =
availableTerminalHeight !== undefined
? Math.max(2, availableTerminalHeight - 3)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Suggested change
? 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>
@MikeWang0316tw

Copy link
Copy Markdown
Contributor Author

Thanks — all three inline findings addressed in 5fd3702d2:

[Suggestion] first-row hold-back guard (MarkdownDisplay.tsx:357) — Good catch, the comment described the intent, not the behaviour. With tableRows.length > 0 the first partial row fell through to the table-closing branch (which also needs a row), so the header+separator were dropped and the partial rendered as a stray line. Relaxed 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 complete row at a time — no flip for any row, including the first. Comment rewritten to match.

[Suggestion] test for the guard edge case (MarkdownDisplay.test.tsx) — Added holds back a partial first row, then pops the table in once it terminates: asserts the partial first row is not rendered while typing, and the table appears once the row closes.

[Suggestion] magic 3 duplicates TABLE_PENDING_RESERVED_ROWS (MarkdownDisplay.tsx:173) — Applied. Moved TABLE_PENDING_RESERVED_ROWS to the top-of-file constants and referenced it from the slice-side tableClampRows estimate, so it can never diverge from RenderTable’s render-side maxHeight cap (exactly the drift you flagged).

157 tests pass (MarkdownDisplay.test.tsx + pending-rendered-height.test.ts).

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@wenshao

wenshao commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

✅ Maintainer verification — real-TUI E2E + mutation test

I built the PR locally and drove the real qwen binary against a fake OpenAI SSE stream in tmux, captured the streaming frontier at the identical moment on both versions, and ran the unit suite as a mutation test. Verdict: the PR does exactly what it claims — the cue is gone, the partial table row is held back, committed output is intact, and there is no scroll-to-top regression. Recommend merge.

Verified head 5fd3702 (== GitHub API head.sha). The A/B base is byte-identical to current origin/main for MarkdownDisplay.tsx (git diff --quiet 50d027fe origin/main → identical), so the "BASE" column below is exactly what a maintainer runs on main today.


1 · Unit suite + mutation test

npx vitest run packages/cli/src/ui/utils/MarkdownDisplay.test.tsx130 passed at head. (The description's "128" predates the final commit, which added the 5th table case; every new case runs under both the Unix and Windows line-ending describe blocks.)

Mutation test (revert only MarkdownDisplay.tsx to origin/main, keep the PR's tests) → 10 targeted failures, proving the new tests actually guard the change:

Failing test (× Unix & Windows) Guards
handles a code fence spanning the clip boundary code-block cue removal
does not stack a double cue for a math block near the clip boundary math-block cue removal
applies the minimum floor at a degenerate budget of 1 outer live-preview cue removal
holds back an unterminated trailing table row while streaming table hold-back (trailing row)
holds back a partial first row, then pops the table in once it terminates table hold-back (first row)

The three remaining new tests (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) pass on both versions — correct: they are boundary guards that pin the change's narrowness (a completed row, a committed transcript, and a non-| line must behave identically pre/post).


2 · Real-TUI A/B — ① streaming table, partial-row hold-back

Fake stream emits | # | Fruit | Color | + separator + 3 complete rows, then the unterminated frontier row | 4 | fo, and pauses there (deterministic flag-file sync, no timing race).

table hold-back

  • BASE: the partial | 4 | fo spills out as a stray text line under the table; when the closing | arrives it flips into the table (frame height + column autosizing change on every token → the jitter this PR targets).
  • FIX: the frontier row is held back — only the 3 complete rows render, no stray line. On release the completed table renders in full (| 4 | fox | brown || 5 | kiwi | green |) — nothing is dropped.

3 · Real-TUI A/B — ② streaming code block, "generating more" cue

Fake stream emits an ~80-line python block and pauses while still pending (clip engaged) at 80×20.

generating-more cue removed

  • BASE: clipped preview appends ... generating more ....
  • FIX: cue gone. The clip still engages (the frame stays within the viewport) — only the redundant/flickering cue is removed, and the reclaimed row keeps total height constant.

4 · The TableRenderer clamp is genuinely kept, and no scroll-to-top regression

  • Streaming a ~40-row table at 80×20, both BASE and FIX still show … more rows streaming … — confirming the PR's statement that this clamp is deliberately retained (an oversized in-progress table hits the table clamp, not the removed outer cue).
  • clearTerminal (\x1b[2J) storm meter via tmux pipe-pane, counted over the whole streaming render:
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 的 baseMarkdownDisplay.tsx 与当前 origin/main 逐字节相同(git diff --quiet 50d027fe origin/main → 相同),所以下文 "BASE" 列正是维护者今天在 main 上会跑到的版本。

1 · 单元套件 + 变异测试

npx vitest run …/MarkdownDisplay.test.tsx → head 上 130 passed。(描述里的 "128" 早于最后一次 commit,那次追加了第 5 个表格用例;每个新用例都在 UnixWindows 两个换行 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 terminatesdoes not hold back a partial row in committed/non-pending outputstill 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

@wenshao

wenshao commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

MikeWang0316tw added a commit to MikeWang0316tw/qwen-code that referenced this pull request Jul 5, 2026
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>
MikeWang0316tw added a commit to MikeWang0316tw/qwen-code that referenced this pull request Jul 5, 2026
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>
@wenshao

wenshao commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@wenshao
wenshao added this pull request to the merge queue Jul 5, 2026
Merged via the queue into QwenLM:main with commit 1b58ede Jul 5, 2026
35 checks passed
MikeWang0316tw added a commit to MikeWang0316tw/qwen-code that referenced this pull request Jul 5, 2026
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>
MikeWang0316tw added a commit to MikeWang0316tw/qwen-code that referenced this pull request Jul 5, 2026
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>
pull Bot pushed a commit to mcx/qwen-code that referenced this pull request Jul 7, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants