diff --git a/.changeset/fix-approval-long-command-wrap.md b/.changeset/fix-approval-long-command-wrap.md new file mode 100644 index 0000000000..79025b6a12 --- /dev/null +++ b/.changeset/fix-approval-long-command-wrap.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": patch +--- + +Wrap long single-line shell commands in approval prompts so the full command remains visible. diff --git a/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts b/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts index 51aea329a7..82d5339f26 100644 --- a/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts +++ b/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts @@ -13,6 +13,7 @@ import { type Focusable, truncateToWidth, visibleWidth, + wrapTextWithAnsi, } from '@earendil-works/pi-tui'; import chalk from 'chalk'; @@ -59,10 +60,53 @@ function makeBlockStyles(colors: ColorPalette): BlockStyles { }; } +function appendWrappedLine( + lines: string[], + firstPrefix: string, + continuationPrefix: string, + content: string, + width: number, +): void { + const prefixWidth = Math.max(visibleWidth(firstPrefix), visibleWidth(continuationPrefix)); + const wrapped = wrapTextWithAnsi(content, Math.max(1, width - prefixWidth)); + if (wrapped.length === 0) { + lines.push(firstPrefix); + return; + } + lines.push(`${firstPrefix}${wrapped[0] ?? ''}`); + for (let i = 1; i < wrapped.length; i++) { + lines.push(`${continuationPrefix}${wrapped[i] ?? ''}`); + } +} + +function renderShellDisplayBlock( + block: Extract, + s: BlockStyles, + width: number, +): string[] { + const lines: string[] = []; + if (block.cwd !== undefined && block.cwd.length > 0) { + lines.push(s.dim(`cwd: ${block.cwd}`)); + } + if (block.danger !== undefined) { + lines.push(s.errorBold(`Dangerous: ${block.danger}`)); + } + const cmdLines = block.command.length > 0 ? block.command.split('\n') : ['']; + cmdLines.forEach((cmdLine, idx) => { + const prefix = idx === 0 ? `${s.accent('$')} ` : `${s.dim('·')} `; + appendWrappedLine(lines, prefix, ' ', s.strong(cmdLine), width); + }); + if (block.description !== undefined && block.description.length > 0) { + lines.push(` ${s.dim(block.description)}`); + } + return lines; +} + function renderDisplayBlock( block: DisplayBlock, s: BlockStyles, colors: ColorPalette, + contentWidth: number, ): string[] { switch (block.type) { case 'diff': @@ -89,24 +133,8 @@ function renderDisplayBlock( } return lines; } - case 'shell': { - const lines: string[] = []; - if (block.cwd !== undefined && block.cwd.length > 0) { - lines.push(s.dim(`cwd: ${block.cwd}`)); - } - if (block.danger !== undefined) { - lines.push(s.errorBold(`Dangerous: ${block.danger}`)); - } - const cmdLines = block.command.length > 0 ? block.command.split('\n') : ['']; - cmdLines.forEach((cmdLine, idx) => { - const prefix = idx === 0 ? s.accent('$') : s.dim('·'); - lines.push(`${prefix} ${s.strong(cmdLine)}`); - }); - if (block.description !== undefined && block.description.length > 0) { - lines.push(` ${s.dim(block.description)}`); - } - return lines; - } + case 'shell': + return renderShellDisplayBlock(block, s, contentWidth); case 'file_op': { const op = s.accent(block.operation.padEnd(5)); const lines = [`${op} ${s.strong(block.path)}`]; @@ -331,7 +359,12 @@ export class ApprovalPanelComponent extends Container implements Focusable { if (visibleBlocks.length > 0) { lines.push(''); for (const block of visibleBlocks) { - const blockLines = renderDisplayBlock(block, blockStyles, this.colors); + const blockLines = renderDisplayBlock( + block, + blockStyles, + this.colors, + Math.max(1, width - 2), + ); for (const line of blockLines) { lines.push(indent(line)); } diff --git a/apps/kimi-code/test/tui/components/dialogs/approval-panel.test.ts b/apps/kimi-code/test/tui/components/dialogs/approval-panel.test.ts index 0b844ee7c5..d6b7b8dda4 100644 --- a/apps/kimi-code/test/tui/components/dialogs/approval-panel.test.ts +++ b/apps/kimi-code/test/tui/components/dialogs/approval-panel.test.ts @@ -92,6 +92,38 @@ describe('ApprovalPanelComponent', () => { expect(out).not.toContain('⚠'); }); + it('wraps a long single-line shell command instead of truncating it', () => { + const head = 'approve-long-command-head'; + const tail = 'approve-long-command-tail'; + const command = `printf ${head}_${'x'.repeat(220)}_${tail}`; + const pending: PendingApproval = { + data: { + id: 'approval_long_command', + tool_call_id: 'tool_long_command', + tool_name: 'Bash', + action: 'run', + description: '', + display: [ + { + type: 'shell', + language: 'bash', + command, + }, + ], + choices: [{ label: 'Approve once', response: 'approved' }], + }, + }; + const dialog = new ApprovalPanelComponent(pending, () => {}, COLORS); + + const rendered = dialog.render(60); + const out = strip(rendered.join('\n')); + expect(rendered.length).toBeGreaterThan(8); + expect(out).toContain(head); + expect(out).toContain(tail); + expect(out).not.toContain('...'); + expect(out).not.toContain('…'); + }); + it('numeric shortcuts still drive approval actions', () => { const { dialog, responses } = makeDialog(); dialog.handleInput('2');