From 524679d23c8bad7dacb29bea7e05aa9e0985c20d Mon Sep 17 00:00:00 2001 From: Gal Zahavi <38544478+galz10@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:13:20 -0700 Subject: [PATCH] feat: implement background process logging and cleanup (#21189) --- packages/cli/src/gemini.tsx | 2 + packages/cli/src/ui/AppContainer.tsx | 8 +- .../BackgroundShellDisplay.test.tsx | 8 +- .../ui/components/BackgroundShellDisplay.tsx | 39 ++- .../cli/src/ui/components/Footer.test.tsx | 71 ++--- .../ui/components/ModelStatsDisplay.test.tsx | 1 + .../BackgroundShellDisplay.test.tsx.snap | 6 + .../ModelStatsDisplay.test.tsx.snap | 23 ++ .../cli/src/ui/contexts/UIActionsContext.tsx | 2 +- .../ui/hooks/shellCommandProcessor.test.tsx | 8 +- .../cli/src/ui/hooks/shellCommandProcessor.ts | 4 +- packages/cli/src/utils/logCleanup.test.ts | 116 ++++++++ packages/cli/src/utils/logCleanup.ts | 66 +++++ .../services/shellExecutionService.test.ts | 232 ++++++++++++++- .../src/services/shellExecutionService.ts | 279 +++++++++++++----- 15 files changed, 724 insertions(+), 141 deletions(-) create mode 100644 packages/cli/src/utils/logCleanup.test.ts create mode 100644 packages/cli/src/utils/logCleanup.ts diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 331ec0c018..b1898ba8ef 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -109,6 +109,7 @@ import { OverflowProvider } from './ui/contexts/OverflowContext.js'; import { setupTerminalAndTheme } from './utils/terminalTheme.js'; import { profiler } from './ui/components/DebugProfiler.js'; import { runDeferredCommand } from './deferred.js'; +import { cleanupBackgroundLogs } from './utils/logCleanup.js'; import { SlashCommandConflictHandler } from './services/SlashCommandConflictHandler.js'; const SLOW_RENDER_MS = 200; @@ -370,6 +371,7 @@ export async function main() { await Promise.all([ cleanupCheckpoints(), cleanupToolOutputFiles(settings.merged), + cleanupBackgroundLogs(), ]); const parseArgsHandle = startupProfiler.start('parse_arguments'); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 97d821850a..2e5e4554dd 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -473,9 +473,11 @@ export const AppContainer = (props: AppContainerProps) => { disableMouseEvents(); // Kill all background shells - for (const pid of backgroundShellsRef.current.keys()) { - ShellExecutionService.kill(pid); - } + await Promise.all( + Array.from(backgroundShellsRef.current.keys()).map((pid) => + ShellExecutionService.kill(pid), + ), + ); const ideClient = await IdeClient.getInstance(); await ideClient.disconnect(); diff --git a/packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx b/packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx index 4d37de24c3..847dcd9a87 100644 --- a/packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx +++ b/packages/cli/src/ui/components/BackgroundShellDisplay.test.tsx @@ -35,6 +35,10 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { ShellExecutionService: { resizePty: vi.fn(), subscribe: vi.fn(() => vi.fn()), + getLogFilePath: vi.fn( + (pid) => `~/.gemini/tmp/background-processes/background-${pid}.log`, + ), + getLogDir: vi.fn(() => '~/.gemini/tmp/background-processes'), }, }; }); @@ -222,7 +226,7 @@ describe('', () => { expect(ShellExecutionService.resizePty).toHaveBeenCalledWith( shell1.pid, 76, - 21, + 20, ); rerender( @@ -242,7 +246,7 @@ describe('', () => { expect(ShellExecutionService.resizePty).toHaveBeenCalledWith( shell1.pid, 96, - 27, + 26, ); unmount(); }); diff --git a/packages/cli/src/ui/components/BackgroundShellDisplay.tsx b/packages/cli/src/ui/components/BackgroundShellDisplay.tsx index a2187fc2f3..bb4c1f26da 100644 --- a/packages/cli/src/ui/components/BackgroundShellDisplay.tsx +++ b/packages/cli/src/ui/components/BackgroundShellDisplay.tsx @@ -10,6 +10,8 @@ import { useUIActions } from '../contexts/UIActionsContext.js'; import { theme } from '../semantic-colors.js'; import { ShellExecutionService, + shortenPath, + tildeifyPath, type AnsiOutput, type AnsiLine, type AnsiToken, @@ -43,8 +45,14 @@ interface BackgroundShellDisplayProps { const CONTENT_PADDING_X = 1; const BORDER_WIDTH = 2; // Left and Right border -const HEADER_HEIGHT = 3; // 2 for border, 1 for header +const MAIN_BORDER_HEIGHT = 2; // Top and Bottom border +const HEADER_HEIGHT = 1; +const FOOTER_HEIGHT = 1; +const TOTAL_OVERHEAD_HEIGHT = + MAIN_BORDER_HEIGHT + HEADER_HEIGHT + FOOTER_HEIGHT; +const PROCESS_LIST_HEADER_HEIGHT = 3; // 1 padding top, 1 text, 1 margin bottom const TAB_DISPLAY_HORIZONTAL_PADDING = 4; +const LOG_PATH_OVERHEAD = 7; // "Log: " (5) + paddingX (2) const formatShellCommandForDisplay = (command: string, maxWidth: number) => { const commandFirstLine = command.split('\n')[0]; @@ -81,7 +89,7 @@ export const BackgroundShellDisplay = ({ if (!activePid) return; const ptyWidth = Math.max(1, width - BORDER_WIDTH - CONTENT_PADDING_X * 2); - const ptyHeight = Math.max(1, height - HEADER_HEIGHT); + const ptyHeight = Math.max(1, height - TOTAL_OVERHEAD_HEIGHT); ShellExecutionService.resizePty(activePid, ptyWidth, ptyHeight); }, [activePid, width, height]); @@ -150,7 +158,7 @@ export const BackgroundShellDisplay = ({ if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) { if (highlightedPid) { - dismissBackgroundShell(highlightedPid); + void dismissBackgroundShell(highlightedPid); // If we killed the active one, the list might update via props } return true; @@ -171,7 +179,7 @@ export const BackgroundShellDisplay = ({ } if (keyMatchers[Command.KILL_BACKGROUND_SHELL](key)) { - dismissBackgroundShell(activeShell.pid); + void dismissBackgroundShell(activeShell.pid); return true; } @@ -336,7 +344,10 @@ export const BackgroundShellDisplay = ({ }} onHighlight={(pid) => setHighlightedPid(pid)} isFocused={isFocused} - maxItemsToShow={Math.max(1, height - HEADER_HEIGHT - 3)} // Adjust for header + maxItemsToShow={Math.max( + 1, + height - TOTAL_OVERHEAD_HEIGHT - PROCESS_LIST_HEADER_HEIGHT, + )} renderItem={( item, { isSelected: _isSelected, titleColor: _titleColor }, @@ -383,6 +394,23 @@ export const BackgroundShellDisplay = ({ ); }; + const renderFooter = () => { + const pidToDisplay = isListOpenProp + ? (highlightedPid ?? activePid) + : activePid; + if (!pidToDisplay) return null; + const logPath = ShellExecutionService.getLogFilePath(pidToDisplay); + const displayPath = shortenPath( + tildeifyPath(logPath), + width - LOG_PATH_OVERHEAD, + ); + return ( + + Log: {displayPath} + + ); + }; + const renderOutput = () => { const lines = typeof output === 'string' ? output.split('\n') : output; @@ -454,6 +482,7 @@ export const BackgroundShellDisplay = ({ {isListOpenProp ? renderProcessList() : renderOutput()} + {renderFooter()} ); }; diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx index 21aa6ee5c0..ab487a440f 100644 --- a/packages/cli/src/ui/components/Footer.test.tsx +++ b/packages/cli/src/ui/components/Footer.test.tsx @@ -101,6 +101,12 @@ describe('