[ide-mode] Close all open diffs when the CLI gets closed (#5792)

This commit is contained in:
christine betts 2025-08-08 15:38:30 +00:00 committed by GitHub
parent 5ec4ea9b4d
commit 3af4913ef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 126 additions and 7 deletions

View File

@ -122,6 +122,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const [idePromptAnswered, setIdePromptAnswered] = useState(false); const [idePromptAnswered, setIdePromptAnswered] = useState(false);
const currentIDE = config.getIdeClient().getCurrentIde(); const currentIDE = config.getIdeClient().getCurrentIde();
useEffect(() => {
registerCleanup(() => config.getIdeClient().disconnect());
}, [config]);
const shouldShowIdePrompt = const shouldShowIdePrompt =
config.getIdeModeFeature() && config.getIdeModeFeature() &&
currentIDE && currentIDE &&

View File

@ -60,6 +60,14 @@ vi.mock('../contexts/SessionContext.js', () => ({
useSessionStats: vi.fn(() => ({ stats: {} })), useSessionStats: vi.fn(() => ({ stats: {} })),
})); }));
const { mockRunExitCleanup } = vi.hoisted(() => ({
mockRunExitCleanup: vi.fn(),
}));
vi.mock('../../utils/cleanup.js', () => ({
runExitCleanup: mockRunExitCleanup,
}));
import { act, renderHook, waitFor } from '@testing-library/react'; import { act, renderHook, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest'; import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest';
import { useSlashCommandProcessor } from './slashCommandProcessor.js'; import { useSlashCommandProcessor } from './slashCommandProcessor.js';
@ -405,6 +413,37 @@ describe('useSlashCommandProcessor', () => {
vi.useRealTimers(); vi.useRealTimers();
} }
}); });
it('should call runExitCleanup when handling a "quit" action', async () => {
const quitAction = vi
.fn()
.mockResolvedValue({ type: 'quit', messages: [] });
const command = createTestCommand({
name: 'exit',
action: quitAction,
});
const result = setupProcessorHook([command]);
await waitFor(() =>
expect(result.current.slashCommands).toHaveLength(1),
);
vi.useFakeTimers();
try {
await act(async () => {
await result.current.handleSlashCommand('/exit');
});
await act(async () => {
await vi.advanceTimersByTimeAsync(200);
});
expect(mockRunExitCleanup).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
}
});
}); });
it('should handle "submit_prompt" action returned from a file-based command', async () => { it('should handle "submit_prompt" action returned from a file-based command', async () => {

View File

@ -18,6 +18,7 @@ import {
ToolConfirmationOutcome, ToolConfirmationOutcome,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import { useSessionStats } from '../contexts/SessionContext.js'; import { useSessionStats } from '../contexts/SessionContext.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { import {
Message, Message,
MessageType, MessageType,
@ -370,7 +371,8 @@ export const useSlashCommandProcessor = (
} }
case 'quit': case 'quit':
setQuittingMessages(result.messages); setQuittingMessages(result.messages);
setTimeout(() => { setTimeout(async () => {
await runExitCleanup();
process.exit(0); process.exit(0);
}, 100); }, 100);
return { type: 'handled' }; return { type: 'handled' };

View File

@ -0,0 +1,68 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { vi } from 'vitest';
import { registerCleanup, runExitCleanup } from './cleanup';
describe('cleanup', () => {
const originalCleanupFunctions = global['cleanupFunctions'];
beforeEach(() => {
// Isolate cleanup functions for each test
global['cleanupFunctions'] = [];
});
afterAll(() => {
// Restore original cleanup functions
global['cleanupFunctions'] = originalCleanupFunctions;
});
it('should run a registered synchronous function', async () => {
const cleanupFn = vi.fn();
registerCleanup(cleanupFn);
await runExitCleanup();
expect(cleanupFn).toHaveBeenCalledTimes(1);
});
it('should run a registered asynchronous function', async () => {
const cleanupFn = vi.fn().mockResolvedValue(undefined);
registerCleanup(cleanupFn);
await runExitCleanup();
expect(cleanupFn).toHaveBeenCalledTimes(1);
});
it('should run multiple registered functions', async () => {
const syncFn = vi.fn();
const asyncFn = vi.fn().mockResolvedValue(undefined);
registerCleanup(syncFn);
registerCleanup(asyncFn);
await runExitCleanup();
expect(syncFn).toHaveBeenCalledTimes(1);
expect(asyncFn).toHaveBeenCalledTimes(1);
});
it('should continue running cleanup functions even if one throws an error', async () => {
const errorFn = vi.fn(() => {
throw new Error('Test Error');
});
const successFn = vi.fn();
registerCleanup(errorFn);
registerCleanup(successFn);
await runExitCleanup();
expect(errorFn).toHaveBeenCalledTimes(1);
expect(successFn).toHaveBeenCalledTimes(1);
});
});

View File

@ -8,16 +8,16 @@ import { promises as fs } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { getProjectTempDir } from '@google/gemini-cli-core'; import { getProjectTempDir } from '@google/gemini-cli-core';
const cleanupFunctions: Array<() => void> = []; const cleanupFunctions: Array<(() => void) | (() => Promise<void>)> = [];
export function registerCleanup(fn: () => void) { export function registerCleanup(fn: (() => void) | (() => Promise<void>)) {
cleanupFunctions.push(fn); cleanupFunctions.push(fn);
} }
export function runExitCleanup() { export async function runExitCleanup() {
for (const fn of cleanupFunctions) { for (const fn of cleanupFunctions) {
try { try {
fn(); await fn();
} catch (_) { } catch (_) {
// Ignore errors during cleanup. // Ignore errors during cleanup.
} }

View File

@ -684,7 +684,7 @@ export class Config {
await this.ideClient.connect(); await this.ideClient.connect();
logIdeConnection(this, new IdeConnectionEvent(IdeConnectionType.SESSION)); logIdeConnection(this, new IdeConnectionEvent(IdeConnectionType.SESSION));
} else { } else {
this.ideClient.disconnect(); await this.ideClient.disconnect();
} }
} }

View File

@ -175,7 +175,14 @@ export class IdeClient {
} }
} }
disconnect() { async disconnect() {
if (this.state.status === IDEConnectionStatus.Disconnected) {
return;
}
for (const filePath of this.diffResponses.keys()) {
await this.closeDiff(filePath);
}
this.diffResponses.clear();
this.setState( this.setState(
IDEConnectionStatus.Disconnected, IDEConnectionStatus.Disconnected,
'IDE integration disabled. To enable it again, run /ide enable.', 'IDE integration disabled. To enable it again, run /ide enable.',