mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
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:
@@ -34,8 +34,9 @@ export const AnsiOutputText: React.FC<AnsiOutputProps> = ({
|
|||||||
? line.map((token: AnsiToken, tokenIndex: number) => (
|
? line.map((token: AnsiToken, tokenIndex: number) => (
|
||||||
<Text
|
<Text
|
||||||
key={tokenIndex}
|
key={tokenIndex}
|
||||||
color={token.inverse ? token.bg : token.fg}
|
color={token.fg}
|
||||||
backgroundColor={token.inverse ? token.fg : token.bg}
|
backgroundColor={token.bg}
|
||||||
|
inverse={token.inverse}
|
||||||
dimColor={token.dim}
|
dimColor={token.dim}
|
||||||
bold={token.bold}
|
bold={token.bold}
|
||||||
italic={token.italic}
|
italic={token.italic}
|
||||||
|
|||||||
@@ -72,6 +72,28 @@ const shellExecutionConfig = {
|
|||||||
disableDynamicLineTrimming: true,
|
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 createExpectedAnsiOutput = (text: string | string[]): AnsiOutput => {
|
||||||
const lines = Array.isArray(text) ? text : text.split('\n');
|
const lines = Array.isArray(text) ? text : text.split('\n');
|
||||||
const expected: AnsiOutput = Array.from(
|
const expected: AnsiOutput = Array.from(
|
||||||
@@ -114,7 +136,7 @@ describe('ShellExecutionService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
mockSerializeTerminalToObject.mockReturnValue([]);
|
||||||
mockIsBinary.mockReturnValue(false);
|
mockIsBinary.mockReturnValue(false);
|
||||||
mockPlatform.mockReturnValue('linux');
|
mockPlatform.mockReturnValue('linux');
|
||||||
mockGetPty.mockResolvedValue({
|
mockGetPty.mockResolvedValue({
|
||||||
@@ -179,6 +201,9 @@ describe('ShellExecutionService', () => {
|
|||||||
|
|
||||||
describe('Successful Execution', () => {
|
describe('Successful Execution', () => {
|
||||||
it('should execute a command and capture output', async () => {
|
it('should execute a command and capture output', async () => {
|
||||||
|
mockSerializeTerminalToObject.mockReturnValue(
|
||||||
|
createMockSerializeTerminalToObjectReturnValue('file1.txt'),
|
||||||
|
);
|
||||||
const { result, handle } = await simulateExecution('ls -l', (pty) => {
|
const { result, handle } = await simulateExecution('ls -l', (pty) => {
|
||||||
pty.onData.mock.calls[0][0]('file1.txt\n');
|
pty.onData.mock.calls[0][0]('file1.txt\n');
|
||||||
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
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) => {
|
const { result } = await simulateExecution('ls --color=auto', (pty) => {
|
||||||
pty.onData.mock.calls[0][0]('a\u001b[31mred\u001b[0mword');
|
pty.onData.mock.calls[0][0]('a\u001b[31mred\u001b[0mword');
|
||||||
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
||||||
@@ -231,6 +259,9 @@ describe('ShellExecutionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle commands with no output', async () => {
|
it('should handle commands with no output', async () => {
|
||||||
|
mockSerializeTerminalToObject.mockReturnValue(
|
||||||
|
createMockSerializeTerminalToObjectReturnValue(''),
|
||||||
|
);
|
||||||
await simulateExecution('touch file', (pty) => {
|
await simulateExecution('touch file', (pty) => {
|
||||||
pty.onExit.mock.calls[0][0]({ exitCode: 0, signal: null });
|
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 () => {
|
it('should call onOutputEvent with AnsiOutput when showColor is false', async () => {
|
||||||
|
mockSerializeTerminalToObject.mockReturnValue(
|
||||||
|
createMockSerializeTerminalToObjectReturnValue('aredword'),
|
||||||
|
);
|
||||||
await simulateExecution(
|
await simulateExecution(
|
||||||
'ls --color=auto',
|
'ls --color=auto',
|
||||||
(pty) => {
|
(pty) => {
|
||||||
@@ -628,6 +662,13 @@ describe('ShellExecutionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle multi-line output correctly when showColor is false', async () => {
|
it('should handle multi-line output correctly when showColor is false', async () => {
|
||||||
|
mockSerializeTerminalToObject.mockReturnValue(
|
||||||
|
createMockSerializeTerminalToObjectReturnValue([
|
||||||
|
'line 1',
|
||||||
|
'line 2',
|
||||||
|
'line 3',
|
||||||
|
]),
|
||||||
|
);
|
||||||
await simulateExecution(
|
await simulateExecution(
|
||||||
'ls --color=auto',
|
'ls --color=auto',
|
||||||
(pty) => {
|
(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) => {
|
const { result } = await simulateExecution('ls --color=auto', (cp) => {
|
||||||
cp.stdout?.emit('data', Buffer.from('a\u001b[31mred\u001b[0mword'));
|
cp.stdout?.emit('data', Buffer.from('a\u001b[31mred\u001b[0mword'));
|
||||||
cp.emit('exit', 0, null);
|
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 () => {
|
it('should use node-pty when shouldUseNodePty is true and pty is available', async () => {
|
||||||
|
mockSerializeTerminalToObject.mockReturnValue([]);
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
const handle = await ShellExecutionService.execute(
|
const handle = await ShellExecutionService.execute(
|
||||||
'test command',
|
'test command',
|
||||||
|
|||||||
@@ -486,24 +486,14 @@ export class ShellExecutionService {
|
|||||||
if (shellExecutionConfig.showColor) {
|
if (shellExecutionConfig.showColor) {
|
||||||
newOutput = serializeTerminalToObject(headlessTerminal);
|
newOutput = serializeTerminalToObject(headlessTerminal);
|
||||||
} else {
|
} else {
|
||||||
const lines: AnsiOutput = [];
|
newOutput = (serializeTerminalToObject(headlessTerminal) || []).map(
|
||||||
for (let y = 0; y < headlessTerminal.rows; y++) {
|
(line) =>
|
||||||
const line = buffer.getLine(buffer.viewportY + y);
|
line.map((token) => {
|
||||||
const lineContent = line ? line.translateToString(true) : '';
|
token.fg = '';
|
||||||
lines.push([
|
token.bg = '';
|
||||||
{
|
return token;
|
||||||
text: lineContent,
|
}),
|
||||||
bold: false,
|
);
|
||||||
italic: false,
|
|
||||||
underline: false,
|
|
||||||
dim: false,
|
|
||||||
inverse: false,
|
|
||||||
fg: '',
|
|
||||||
bg: '',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
newOutput = lines;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastNonEmptyLine = -1;
|
let lastNonEmptyLine = -1;
|
||||||
|
|||||||
@@ -171,6 +171,26 @@ describe('terminalSerializer', () => {
|
|||||||
expect(result[0][0].bg).toBe('#008000');
|
expect(result[0][0].bg).toBe('#008000');
|
||||||
expect(result[0][0].text).toBe('Styled text');
|
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', () => {
|
describe('convertColorToHex', () => {
|
||||||
it('should convert RGB color to hex', () => {
|
it('should convert RGB color to hex', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user