fix(chat): don't fold a pipe-bearing prose line into a preceding table (#3978)

Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
This commit is contained in:
ly-wang19
2026-06-23 11:47:45 -07:00
committed by GitHub
co-authored by ly-wang19
parent 8be534447d
commit e15f90ef63
2 changed files with 42 additions and 0 deletions
@@ -51,6 +51,29 @@ describe('splitAgentMessageIntoBubbles', () => {
]);
});
it('does not absorb a following prose line whose pipes give a different column count', () => {
// The prose line satisfies looksLikeMarkdownTableRow (it contains pipes) but
// has 3 "cells" vs the table's 2; absorbing it makes parseMarkdownTable
// reject the block (null), suppressing the table renderer and merging the
// prose into the table bubble.
const content =
'| Plan | Cost |\n| --- | --- |\n| Basic | $10 |\nEither pick Basic | Pro, then run `ps aux | grep node`.';
expect(splitAgentMessageIntoBubbles(content)).toEqual([
'| Plan | Cost |\n| --- | --- |\n| Basic | $10 |',
'Either pick Basic | Pro, then run `ps aux | grep node`.',
]);
});
it('keeps a table whose first data row carries a code-span pipe as one readable block', () => {
// splitMarkdownTableCells splits naively on "|", so a genuine first data row
// with a pipe inside a code span over-counts its cells (3 vs the header's 2).
// Dropping it would leave a header+separator-only block that renders as an
// empty-row table and detaches the real row; keeping the first data row makes
// parseMarkdownTable return null so the block falls back to readable markdown.
const content = '| Command | Use |\n| --- | --- |\n| `ps aux | grep node` | Find it |';
expect(splitAgentMessageIntoBubbles(content)).toEqual([content]);
});
it('keeps double-newline paragraphs in the same bubble', () => {
const content = 'First line\nSecond line\n\nThird paragraph\nFourth line';
expect(splitAgentMessageIntoBubbles(content)).toEqual([
+19
View File
@@ -47,10 +47,29 @@ export function splitAgentMessageIntoBubbles(content: string): string[] {
if (currentLines.length > 0) {
flushCurrent();
}
// A prose line that merely contains a pipe (e.g. "pick A | B, then run
// `ps aux | grep x`") satisfies looksLikeMarkdownTableRow but is not a
// table row. Require the same column count as the header so it isn't
// absorbed into the table — otherwise parseMarkdownTable rejects the
// mismatched block (returns null), suppressing the table UI and merging
// the prose line into the table bubble.
//
// Always keep at least the first data row, though: splitMarkdownTableCells
// splits naively on "|" and does not honor escaped "\|" or pipes inside
// code spans, so a genuine first row carrying such a pipe over-counts its
// cells. Dropping it here would leave a header+separator-only block that
// parseMarkdownTable renders as an empty-row table (and detaches the real
// row). Keeping it preserves the count mismatch so parseMarkdownTable
// returns null and the block falls back to readable raw markdown.
const headerCellCount = splitMarkdownTableCells(line).length;
const tableLines = [line, lines[index + 1]];
index += 2;
let dataRowsAbsorbed = 0;
while (index < lines.length && looksLikeMarkdownTableRow(lines[index])) {
const matches = splitMarkdownTableCells(lines[index]).length === headerCellCount;
if (!matches && dataRowsAbsorbed > 0) break;
tableLines.push(lines[index]);
dataRowsAbsorbed += 1;
index += 1;
}
index -= 1;