diff --git a/.changeset/sanitize-task-output.md b/.changeset/sanitize-task-output.md new file mode 100644 index 0000000000..0477295aea --- /dev/null +++ b/.changeset/sanitize-task-output.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Prevent background task output from disrupting terminal pane borders. diff --git a/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts b/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts index 4a463671cb..ad266fbb62 100644 --- a/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts +++ b/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts @@ -22,6 +22,7 @@ import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi import { currentTheme } from '#/tui/theme'; import { printableChar } from '@/tui/utils/printable-key'; +import { sanitizeShellOutput } from '#/tui/utils/shell-output'; const ELLIPSIS = '…'; @@ -104,7 +105,7 @@ export class TaskOutputViewer extends Container implements Focusable { } private splitOutput(output: string): string[] { - return (output.length > 0 ? output : '[no output captured]').split('\n'); + return (output.length > 0 ? sanitizeShellOutput(output) : '[no output captured]').split('\n'); } // ── input ────────────────────────────────────────────────────────── diff --git a/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts b/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts index 1874d0e7a0..1b33a5bf3a 100644 --- a/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts +++ b/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts @@ -27,6 +27,7 @@ import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi import { SELECT_POINTER } from '@/tui/constant/symbols'; import { currentTheme } from '#/tui/theme'; import { printableChar } from '@/tui/utils/printable-key'; +import { sanitizeShellOutput } from '#/tui/utils/shell-output'; const ELLIPSIS = '…'; @@ -603,7 +604,7 @@ export class TasksBrowserApp extends Container implements Focusable { if (this.props.tailLoading) body = '[loading…]'; else if (this.props.tailOutput === undefined || this.props.tailOutput.length === 0) body = '[no output captured]'; - else body = this.props.tailOutput; + else body = sanitizeShellOutput(this.props.tailOutput); const rawLines = body.split('\n'); const tailLines = rawLines.slice(-innerHeight); diff --git a/apps/kimi-code/test/tui/task-output-viewer.test.ts b/apps/kimi-code/test/tui/task-output-viewer.test.ts index 5948ec6c84..41470c96b3 100644 --- a/apps/kimi-code/test/tui/task-output-viewer.test.ts +++ b/apps/kimi-code/test/tui/task-output-viewer.test.ts @@ -111,6 +111,17 @@ describe('TaskOutputViewer — rendering', () => { expect(out).toContain('delta'); expect(out).toContain('echo'); }); + + it('does not pass terminal controls from task output into the framed body', () => { + const rendered = makeViewer({ + output: 'Downloading wheel 25%\rDownloading wheel 75%\u001B[2Jdone', + }).render(120); + const raw = rendered.join('\n'); + + expect(raw).not.toContain('\r'); + expect(raw).not.toContain('\u001B[2J'); + expect(strip(raw)).toContain('Downloading wheel 25%Downloading wheel 75%done'); + }); }); describe('TaskOutputViewer — scrolling', () => { diff --git a/apps/kimi-code/test/tui/tasks-browser.test.ts b/apps/kimi-code/test/tui/tasks-browser.test.ts index 7d5a54c3bf..d173af25ff 100644 --- a/apps/kimi-code/test/tui/tasks-browser.test.ts +++ b/apps/kimi-code/test/tui/tasks-browser.test.ts @@ -243,6 +243,19 @@ describe('TasksBrowserApp — full-screen rendering', () => { expect(out).toContain('listening on :3000'); }); + it('does not pass terminal controls from tail output into the framed preview', () => { + const rendered = makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa' })], + selectedTaskId: 'bash-aaaaaaaa', + tailOutput: 'Downloading wheel 25%\rDownloading wheel 75%\u001B[2Jdone', + }).render(120); + const raw = rendered.join('\n'); + + expect(raw).not.toContain('\r'); + expect(raw).not.toContain('\u001B[2J'); + expect(strip(raw)).toContain('Downloading wheel 25%Downloading wheel 75%done'); + }); + it('shows a loading state when tail is loading', () => { const out = strip( makeApp({