fix(shell): cursor visibility when using interactive mode (#14095)

Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
Aswin Ashok
2025-12-04 22:28:03 +05:30
committed by GitHub
parent 1040c246f5
commit 84f521b1c6
4 changed files with 76 additions and 23 deletions
@@ -72,6 +72,28 @@ const shellExecutionConfig = {
disableDynamicLineTrimming: true,
};
const createMockSerializeTerminalToObjectReturnValue = (
text: string | string[],
): AnsiOutput => {
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',