mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -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) => (
|
||||
<Text
|
||||
key={tokenIndex}
|
||||
color={token.inverse ? token.bg : token.fg}
|
||||
backgroundColor={token.inverse ? token.fg : token.bg}
|
||||
color={token.fg}
|
||||
backgroundColor={token.bg}
|
||||
inverse={token.inverse}
|
||||
dimColor={token.dim}
|
||||
bold={token.bold}
|
||||
italic={token.italic}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user