Commit db8e344
fix(cli): smoother streaming table rendering (#6345)
* fix(cli): smoother streaming table rendering
Follow-up to the streaming table hold-back, on its own branch so the
cue-removal PR (#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 #6345.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 6863268 commit db8e344
4 files changed
Lines changed: 669 additions & 40 deletions
File tree
- packages/cli/src/ui/utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
329 | 330 | | |
330 | 331 | | |
331 | 332 | | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
332 | 351 | | |
333 | 352 | | |
334 | 353 | | |
| |||
366 | 385 | | |
367 | 386 | | |
368 | 387 | | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
369 | 421 | | |
370 | 422 | | |
371 | 423 | | |
| |||
391 | 443 | | |
392 | 444 | | |
393 | 445 | | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
| 679 | + | |
| 680 | + | |
| 681 | + | |
| 682 | + | |
| 683 | + | |
| 684 | + | |
| 685 | + | |
| 686 | + | |
| 687 | + | |
394 | 688 | | |
395 | 689 | | |
396 | 690 | | |
| |||
0 commit comments