diff --git a/packages/cli/src/__tests__/pi-transcript.test.ts b/packages/cli/src/__tests__/pi-transcript.test.ts index b57e43775c..ba098060a4 100644 --- a/packages/cli/src/__tests__/pi-transcript.test.ts +++ b/packages/cli/src/__tests__/pi-transcript.test.ts @@ -126,6 +126,22 @@ describe('Maka Pi TUI transcript', () => { assert.match(chinese, /\/session\s+切换或恢复会话/); }); + test('keeps every pending-queue preview on exactly one terminal row (#3824)', () => { + const state = createMakaPiTranscriptState(); + // limitText appends its truncation suffix behind a newline once the 200-char + // cap trips; an embedded newline shifts every later row down while pi-tui + // still counts one row written, corrupting the frame until the queue drains. + state.steering = ['s'.repeat(250)]; + state.followup = ['f'.repeat(250)]; + + const lines = renderMakaPiPendingQueue(state, 400); + for (const line of lines) { + assert.doesNotMatch(line, /[\r\n]/, `pending-queue row must be a single row: ${line}`); + } + // The truncation notice still reaches the user, just on the same row. + assert.match(stripAnsi(lines[0] ?? ''), /50 chars truncated/); + }); + test('renders the pending-queue edit shortcut for the current platform', () => { const state = createMakaPiTranscriptState(); state.steering = ['Keep going']; diff --git a/packages/cli/src/pi-transcript.ts b/packages/cli/src/pi-transcript.ts index 35de5bbd80..a732bcab09 100644 --- a/packages/cli/src/pi-transcript.ts +++ b/packages/cli/src/pi-transcript.ts @@ -49,6 +49,7 @@ import type { MakaSessionDriver } from './session-driver.js'; import { BoundedChunkBuffer } from './bounded-chunk-buffer.js'; import { ansi } from './tui-ansi.js'; import { + collapseToSingleLine, fitLine, formatTokenCount, formatUnknown, @@ -1502,14 +1503,24 @@ export function renderMakaPiPendingQueue( return lines; } -/** First non-empty line of a queued message, trimmed for a one-line preview. */ +/** + * First non-empty line of a queued message, trimmed for a one-line preview. + * + * `limitText` appends its truncation suffix behind a newline, so its output is + * multi-line whenever the cap trips. Each element the pending-bar returns must + * occupy exactly one terminal row — pi-tui writes them between explicit \r\n + * separators and counts one row each, so an embedded newline shifts every + * later row down while the diff accounting still believes one row was written + * (#3824). `fitLine` cannot catch it: visibleWidth treats controls as + * zero-width. Sibling call sites collapse the same output the same way. + */ function firstLinePreview(text: string): string { const line = text .split('\n') .map((part) => part.trim()) .find((part) => part.length > 0) ?? ''; - return limitText(line, 200); + return collapseToSingleLine(limitText(line, 200)); } /**