diff --git a/packages/cli/src/ui/components/AnsiOutput.tsx b/packages/cli/src/ui/components/AnsiOutput.tsx index 8b54eb6453..d31ae62b28 100644 --- a/packages/cli/src/ui/components/AnsiOutput.tsx +++ b/packages/cli/src/ui/components/AnsiOutput.tsx @@ -34,8 +34,9 @@ export const AnsiOutputText: React.FC = ({ ? line.map((token: AnsiToken, tokenIndex: number) => ( { + const lines = Array.isArray(text) ? text : text.split('\n'); + const expected: AnsiOutput = Array.from( + { length: shellExecutionConfig.terminalHeight }, + (_, i) => [ + { + text: (lines[i] || '').trim(), + bold: false, + italic: false, + underline: false, + dim: false, + inverse: false, + fg: '#ffffff', + bg: '#000000', + }, + ], + ); + return expected; +}; + const createExpectedAnsiOutput = (text: string | string[]): AnsiOutput => { const lines = Array.isArray(text) ? text : text.split('\n'); const expected: AnsiOutput = Array.from( @@ -114,7 +136,7 @@ describe('ShellExecutionService', () => { beforeEach(() => { vi.clearAllMocks(); - + mockSerializeTerminalToObject.mockReturnValue([]); mockIsBinary.mockReturnValue(false); mockPlatform.mockReturnValue('linux'); mockGetPty.mockResolvedValue({ @@ -179,6 +201,9 @@ describe('ShellExecutionService', () => { describe('Successful Execution', () => { it('should execute a command and capture output', async () => { + mockSerializeTerminalToObject.mockReturnValue( + createMockSerializeTerminalToObjectReturnValue('file1.txt'), + ); const { result, handle } = await simulateExecution('ls -l', (pty) => { pty.onData.mock.calls[0][0]('file1.txt\n'); pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }); @@ -205,7 +230,10 @@ describe('ShellExecutionService', () => { }); }); - it('should strip ANSI codes from output', async () => { + it('should strip ANSI color codes from output', async () => { + mockSerializeTerminalToObject.mockReturnValue( + createMockSerializeTerminalToObjectReturnValue('aredword'), + ); const { result } = await simulateExecution('ls --color=auto', (pty) => { pty.onData.mock.calls[0][0]('a\u001b[31mred\u001b[0mword'); pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }); @@ -231,6 +259,9 @@ describe('ShellExecutionService', () => { }); it('should handle commands with no output', async () => { + mockSerializeTerminalToObject.mockReturnValue( + createMockSerializeTerminalToObjectReturnValue(''), + ); await simulateExecution('touch file', (pty) => { pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null }); }); @@ -604,6 +635,9 @@ describe('ShellExecutionService', () => { }); it('should call onOutputEvent with AnsiOutput when showColor is false', async () => { + mockSerializeTerminalToObject.mockReturnValue( + createMockSerializeTerminalToObjectReturnValue('aredword'), + ); await simulateExecution( 'ls --color=auto', (pty) => { @@ -628,6 +662,13 @@ describe('ShellExecutionService', () => { }); it('should handle multi-line output correctly when showColor is false', async () => { + mockSerializeTerminalToObject.mockReturnValue( + createMockSerializeTerminalToObjectReturnValue([ + 'line 1', + 'line 2', + 'line 3', + ]), + ); await simulateExecution( 'ls --color=auto', (pty) => { @@ -733,7 +774,7 @@ describe('ShellExecutionService child_process fallback', () => { }); }); - it('should strip ANSI codes from output', async () => { + it('should strip ANSI color codes from output', async () => { const { result } = await simulateExecution('ls --color=auto', (cp) => { cp.stdout?.emit('data', Buffer.from('a\u001b[31mred\u001b[0mword')); cp.emit('exit', 0, null); @@ -1072,6 +1113,7 @@ describe('ShellExecutionService execution method selection', () => { }); it('should use node-pty when shouldUseNodePty is true and pty is available', async () => { + mockSerializeTerminalToObject.mockReturnValue([]); const abortController = new AbortController(); const handle = await ShellExecutionService.execute( 'test command', diff --git a/packages/core/src/services/shellExecutionService.ts b/packages/core/src/services/shellExecutionService.ts index df40c4b1a3..7705337a35 100644 --- a/packages/core/src/services/shellExecutionService.ts +++ b/packages/core/src/services/shellExecutionService.ts @@ -486,24 +486,14 @@ export class ShellExecutionService { if (shellExecutionConfig.showColor) { newOutput = serializeTerminalToObject(headlessTerminal); } else { - const lines: AnsiOutput = []; - for (let y = 0; y < headlessTerminal.rows; y++) { - const line = buffer.getLine(buffer.viewportY + y); - const lineContent = line ? line.translateToString(true) : ''; - lines.push([ - { - text: lineContent, - bold: false, - italic: false, - underline: false, - dim: false, - inverse: false, - fg: '', - bg: '', - }, - ]); - } - newOutput = lines; + newOutput = (serializeTerminalToObject(headlessTerminal) || []).map( + (line) => + line.map((token) => { + token.fg = ''; + token.bg = ''; + return token; + }), + ); } let lastNonEmptyLine = -1; diff --git a/packages/core/src/utils/terminalSerializer.test.ts b/packages/core/src/utils/terminalSerializer.test.ts index fd6241d04d..cfc8032141 100644 --- a/packages/core/src/utils/terminalSerializer.test.ts +++ b/packages/core/src/utils/terminalSerializer.test.ts @@ -171,6 +171,26 @@ describe('terminalSerializer', () => { expect(result[0][0].bg).toBe('#008000'); expect(result[0][0].text).toBe('Styled text'); }); + + it('should set inverse for the cursor position', async () => { + const terminal = new Terminal({ + cols: 80, + rows: 24, + allowProposedApi: true, + }); + await writeToTerminal(terminal, 'Cursor test'); + // Move cursor to the start of the line (0,0) using ANSI escape code + await writeToTerminal(terminal, '\x1b[H'); + + const result = serializeTerminalToObject(terminal); + // The character at (0,0) should have inverse: true due to cursor + expect(result[0][0].text).toBe('C'); + expect(result[0][0].inverse).toBe(true); + + // The rest of the text should not have inverse: true (unless explicitly set) + expect(result[0][1].text.trim()).toBe('ursor test'); + expect(result[0][1].inverse).toBe(false); + }); }); describe('convertColorToHex', () => { it('should convert RGB color to hex', () => {