mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
chore: finish truncation and stream logging logic
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
ExecutionLifecycleService,
|
ExecutionLifecycleService,
|
||||||
CoreToolCallStatus,
|
CoreToolCallStatus,
|
||||||
moveToolOutputToFile,
|
moveToolOutputToFile,
|
||||||
|
debugLogger,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { type PartListUnion } from '@google/genai';
|
import { type PartListUnion } from '@google/genai';
|
||||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||||
@@ -378,11 +379,6 @@ export const useExecutionLifecycle = (
|
|||||||
let cumulativeStdout: string | AnsiOutput = '';
|
let cumulativeStdout: string | AnsiOutput = '';
|
||||||
let isBinaryStream = false;
|
let isBinaryStream = false;
|
||||||
let binaryBytesReceived = 0;
|
let binaryBytesReceived = 0;
|
||||||
let totalBytesWritten = 0;
|
|
||||||
|
|
||||||
const outputFileName = `gemini_shell_output_${crypto.randomBytes(6).toString('hex')}.log`;
|
|
||||||
const outputFilePath = path.join(os.tmpdir(), outputFileName);
|
|
||||||
const outputStream = fs.createWriteStream(outputFilePath);
|
|
||||||
|
|
||||||
const initialToolDisplay: IndividualToolCallDisplay = {
|
const initialToolDisplay: IndividualToolCallDisplay = {
|
||||||
callId,
|
callId,
|
||||||
@@ -400,7 +396,6 @@ export const useExecutionLifecycle = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
let executionPid: number | undefined;
|
let executionPid: number | undefined;
|
||||||
let fullOutputReturned = false;
|
|
||||||
|
|
||||||
const abortHandler = () => {
|
const abortHandler = () => {
|
||||||
onDebugMessage(
|
onDebugMessage(
|
||||||
@@ -430,25 +425,13 @@ export const useExecutionLifecycle = (
|
|||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'raw_data':
|
case 'raw_data':
|
||||||
// We rely on 'file_data' for the clean output stream.
|
|
||||||
break;
|
|
||||||
case 'file_data':
|
case 'file_data':
|
||||||
if (!isBinaryStream) {
|
|
||||||
outputStream.write(event.chunk);
|
|
||||||
totalBytesWritten += Buffer.byteLength(event.chunk);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case 'data':
|
case 'data':
|
||||||
if (isBinaryStream) break;
|
if (isBinaryStream) break;
|
||||||
if (typeof event.chunk === 'string') {
|
if (typeof event.chunk === 'string') {
|
||||||
if (typeof cumulativeStdout === 'string') {
|
if (typeof cumulativeStdout === 'string') {
|
||||||
cumulativeStdout += event.chunk;
|
cumulativeStdout += event.chunk;
|
||||||
// Keep a small buffer for the UI to prevent memory spikes and Ink lagging
|
|
||||||
const MAX_UI_LENGTH = 100000; // 100KB
|
|
||||||
if (cumulativeStdout.length > MAX_UI_LENGTH) {
|
|
||||||
cumulativeStdout =
|
|
||||||
cumulativeStdout.slice(-MAX_UI_LENGTH);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
cumulativeStdout = event.chunk;
|
cumulativeStdout = event.chunk;
|
||||||
}
|
}
|
||||||
@@ -534,9 +517,6 @@ export const useExecutionLifecycle = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await resultPromise;
|
const result = await resultPromise;
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
outputStream.end(resolve);
|
|
||||||
});
|
|
||||||
setPendingHistoryItem(null);
|
setPendingHistoryItem(null);
|
||||||
|
|
||||||
if (result.backgrounded && result.pid) {
|
if (result.backgrounded && result.pid) {
|
||||||
@@ -556,10 +536,9 @@ export const useExecutionLifecycle = (
|
|||||||
} else {
|
} else {
|
||||||
mainContent =
|
mainContent =
|
||||||
result.output.trim() || '(Command produced no output)';
|
result.output.trim() || '(Command produced no output)';
|
||||||
const threshold = config.getTruncateToolOutputThreshold();
|
if (result.fullOutputFilePath) {
|
||||||
if (threshold > 0 && totalBytesWritten >= threshold) {
|
|
||||||
const { outputFile: savedPath } = await moveToolOutputToFile(
|
const { outputFile: savedPath } = await moveToolOutputToFile(
|
||||||
outputFilePath,
|
result.fullOutputFilePath,
|
||||||
SHELL_COMMAND_NAME,
|
SHELL_COMMAND_NAME,
|
||||||
callId,
|
callId,
|
||||||
config.storage.getProjectTempDir(),
|
config.storage.getProjectTempDir(),
|
||||||
@@ -574,7 +553,6 @@ export const useExecutionLifecycle = (
|
|||||||
warning,
|
warning,
|
||||||
)
|
)
|
||||||
: `${mainContent}\n\n${warning}`;
|
: `${mainContent}\n\n${warning}`;
|
||||||
fullOutputReturned = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -678,19 +656,17 @@ export const useExecutionLifecycle = (
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
abortSignal.removeEventListener('abort', abortHandler);
|
abortSignal.removeEventListener('abort', abortHandler);
|
||||||
if (!outputStream.closed) {
|
|
||||||
outputStream.destroy();
|
|
||||||
}
|
|
||||||
if (pwdFilePath) {
|
if (pwdFilePath) {
|
||||||
fs.promises.unlink(pwdFilePath).catch(() => {});
|
fs.promises.unlink(pwdFilePath).catch((err) => {
|
||||||
|
debugLogger.warn(
|
||||||
|
`Failed to cleanup pwd file: ${pwdFilePath}`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: 'SET_ACTIVE_PTY', pid: null });
|
dispatch({ type: 'SET_ACTIVE_PTY', pid: null });
|
||||||
setShellInputFocused(false);
|
setShellInputFocused(false);
|
||||||
|
|
||||||
if (!fullOutputReturned) {
|
|
||||||
fs.promises.unlink(outputFilePath).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface ExecutionResult {
|
|||||||
pid: number | undefined;
|
pid: number | undefined;
|
||||||
executionMethod: ExecutionMethod;
|
executionMethod: ExecutionMethod;
|
||||||
backgrounded?: boolean;
|
backgrounded?: boolean;
|
||||||
|
fullOutputFilePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecutionHandle {
|
export interface ExecutionHandle {
|
||||||
|
|||||||
@@ -33,7 +33,16 @@ const mockIsBinary = vi.hoisted(() => vi.fn());
|
|||||||
const mockPlatform = vi.hoisted(() => vi.fn());
|
const mockPlatform = vi.hoisted(() => vi.fn());
|
||||||
const mockHomedir = vi.hoisted(() => vi.fn());
|
const mockHomedir = vi.hoisted(() => vi.fn());
|
||||||
const mockMkdirSync = vi.hoisted(() => vi.fn());
|
const mockMkdirSync = vi.hoisted(() => vi.fn());
|
||||||
const mockCreateWriteStream = vi.hoisted(() => vi.fn());
|
const mockCreateWriteStream = vi.hoisted(() =>
|
||||||
|
vi.fn().mockReturnValue({
|
||||||
|
write: vi.fn(),
|
||||||
|
end: vi.fn().mockImplementation((cb?: () => void) => {
|
||||||
|
if (cb) cb();
|
||||||
|
}),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
closed: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
const mockGetPty = vi.hoisted(() => vi.fn());
|
const mockGetPty = vi.hoisted(() => vi.fn());
|
||||||
const mockSerializeTerminalToObject = vi.hoisted(() => vi.fn());
|
const mockSerializeTerminalToObject = vi.hoisted(() => vi.fn());
|
||||||
const mockResolveExecutable = vi.hoisted(() => vi.fn());
|
const mockResolveExecutable = vi.hoisted(() => vi.fn());
|
||||||
@@ -91,6 +100,7 @@ vi.mock('node:os', () => ({
|
|||||||
default: {
|
default: {
|
||||||
platform: mockPlatform,
|
platform: mockPlatform,
|
||||||
homedir: mockHomedir,
|
homedir: mockHomedir,
|
||||||
|
tmpdir: () => '/tmp',
|
||||||
constants: {
|
constants: {
|
||||||
signals: {
|
signals: {
|
||||||
SIGTERM: 15,
|
SIGTERM: 15,
|
||||||
@@ -207,6 +217,15 @@ describe('ShellExecutionService', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
mockCreateWriteStream.mockReturnValue({
|
||||||
|
write: vi.fn(),
|
||||||
|
end: vi.fn().mockImplementation((cb?: () => void) => {
|
||||||
|
if (cb) cb();
|
||||||
|
}),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
closed: false,
|
||||||
|
on: vi.fn(),
|
||||||
|
});
|
||||||
ExecutionLifecycleService.resetForTest();
|
ExecutionLifecycleService.resetForTest();
|
||||||
mockSerializeTerminalToObject.mockReturnValue([]);
|
mockSerializeTerminalToObject.mockReturnValue([]);
|
||||||
mockIsBinary.mockReturnValue(false);
|
mockIsBinary.mockReturnValue(false);
|
||||||
@@ -619,7 +638,7 @@ describe('ShellExecutionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a synchronous spawn error', async () => {
|
it('should handle a synchronous spawn error', async () => {
|
||||||
mockGetPty.mockImplementation(() => null);
|
mockGetPty.mockResolvedValue(null);
|
||||||
|
|
||||||
mockCpSpawn.mockImplementation(() => {
|
mockCpSpawn.mockImplementation(() => {
|
||||||
throw new Error('Simulated PTY spawn error');
|
throw new Error('Simulated PTY spawn error');
|
||||||
@@ -723,7 +742,13 @@ describe('ShellExecutionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Backgrounding', () => {
|
describe('Backgrounding', () => {
|
||||||
let mockWriteStream: { write: Mock; end: Mock; on: Mock };
|
let mockWriteStream: {
|
||||||
|
write: Mock;
|
||||||
|
end: Mock;
|
||||||
|
on: Mock;
|
||||||
|
destroy: Mock;
|
||||||
|
closed: boolean;
|
||||||
|
};
|
||||||
let mockBgChildProcess: EventEmitter & Partial<ChildProcess>;
|
let mockBgChildProcess: EventEmitter & Partial<ChildProcess>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -731,6 +756,8 @@ describe('ShellExecutionService', () => {
|
|||||||
write: vi.fn(),
|
write: vi.fn(),
|
||||||
end: vi.fn().mockImplementation((cb) => cb?.()),
|
end: vi.fn().mockImplementation((cb) => cb?.()),
|
||||||
on: vi.fn(),
|
on: vi.fn(),
|
||||||
|
destroy: vi.fn(),
|
||||||
|
closed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
mockMkdirSync.mockReturnValue(undefined);
|
mockMkdirSync.mockReturnValue(undefined);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type { Writable } from 'node:stream';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import fs, { mkdirSync } from 'node:fs';
|
import fs, { mkdirSync } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
import type { IPty } from '@lydell/node-pty';
|
import type { IPty } from '@lydell/node-pty';
|
||||||
import { getCachedEncodingForBuffer } from '../utils/systemEncoding.js';
|
import { getCachedEncodingForBuffer } from '../utils/systemEncoding.js';
|
||||||
import {
|
import {
|
||||||
@@ -368,32 +369,114 @@ export class ShellExecutionService {
|
|||||||
shouldUseNodePty: boolean,
|
shouldUseNodePty: boolean,
|
||||||
shellExecutionConfig: ShellExecutionConfig,
|
shellExecutionConfig: ShellExecutionConfig,
|
||||||
): Promise<ShellExecutionHandle> {
|
): Promise<ShellExecutionHandle> {
|
||||||
|
const outputFileName = `gemini_shell_output_${crypto.randomBytes(6).toString('hex')}.log`;
|
||||||
|
const outputFilePath = path.join(os.tmpdir(), outputFileName);
|
||||||
|
const outputStream = fs.createWriteStream(outputFilePath);
|
||||||
|
|
||||||
|
let isBinaryStream = false;
|
||||||
|
let totalBytesWritten = 0;
|
||||||
|
|
||||||
|
const interceptedOnOutputEvent = (event: ShellOutputEvent) => {
|
||||||
|
switch (event.type) {
|
||||||
|
case 'raw_data':
|
||||||
|
break;
|
||||||
|
case 'file_data':
|
||||||
|
if (!isBinaryStream) {
|
||||||
|
outputStream.write(event.chunk);
|
||||||
|
totalBytesWritten += Buffer.byteLength(event.chunk);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'binary_detected':
|
||||||
|
case 'binary_progress':
|
||||||
|
isBinaryStream = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
onOutputEvent(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
let handlePromise: Promise<ShellExecutionHandle>;
|
||||||
|
|
||||||
if (shouldUseNodePty) {
|
if (shouldUseNodePty) {
|
||||||
const ptyInfo = await getPty();
|
handlePromise = getPty().then((ptyInfo) => {
|
||||||
if (ptyInfo) {
|
if (ptyInfo) {
|
||||||
try {
|
return this.executeWithPty(
|
||||||
return await this.executeWithPty(
|
|
||||||
commandToExecute,
|
commandToExecute,
|
||||||
cwd,
|
cwd,
|
||||||
onOutputEvent,
|
interceptedOnOutputEvent,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
shellExecutionConfig,
|
shellExecutionConfig,
|
||||||
ptyInfo,
|
ptyInfo,
|
||||||
|
).catch(() =>
|
||||||
|
this.childProcessFallback(
|
||||||
|
commandToExecute,
|
||||||
|
cwd,
|
||||||
|
interceptedOnOutputEvent,
|
||||||
|
abortSignal,
|
||||||
|
shellExecutionConfig,
|
||||||
|
shouldUseNodePty,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch {
|
|
||||||
// Fallback to child_process
|
|
||||||
}
|
}
|
||||||
}
|
return this.childProcessFallback(
|
||||||
|
commandToExecute,
|
||||||
|
cwd,
|
||||||
|
interceptedOnOutputEvent,
|
||||||
|
abortSignal,
|
||||||
|
shellExecutionConfig,
|
||||||
|
shouldUseNodePty,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handlePromise = this.childProcessFallback(
|
||||||
|
commandToExecute,
|
||||||
|
cwd,
|
||||||
|
interceptedOnOutputEvent,
|
||||||
|
abortSignal,
|
||||||
|
shellExecutionConfig,
|
||||||
|
shouldUseNodePty,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.childProcessFallback(
|
const handle = await handlePromise;
|
||||||
commandToExecute,
|
|
||||||
cwd,
|
const wrappedResultPromise = handle.result
|
||||||
onOutputEvent,
|
.then(async (result) => {
|
||||||
abortSignal,
|
await new Promise<void>((resolve) => {
|
||||||
shellExecutionConfig,
|
outputStream.end(resolve);
|
||||||
shouldUseNodePty,
|
});
|
||||||
);
|
// The threshold logic is handled later by ToolExecutor/caller, so we just return the full file path if anything was written
|
||||||
|
if (
|
||||||
|
totalBytesWritten > 0 &&
|
||||||
|
!result.backgrounded &&
|
||||||
|
!abortSignal.aborted &&
|
||||||
|
!result.error
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
fullOutputFilePath: outputFilePath,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!outputStream.closed) {
|
||||||
|
outputStream.destroy();
|
||||||
|
}
|
||||||
|
await fs.promises.unlink(outputFilePath).catch(() => undefined);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(async (err) => {
|
||||||
|
if (!outputStream.closed) {
|
||||||
|
outputStream.destroy();
|
||||||
|
}
|
||||||
|
await fs.promises.unlink(outputFilePath).catch(() => undefined);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
pid: handle.pid,
|
||||||
|
result: wrappedResultPromise,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static appendAndTruncate(
|
private static appendAndTruncate(
|
||||||
|
|||||||
Reference in New Issue
Block a user