Merge branch 'main' into pr4-cli

This commit is contained in:
Michael Bleigh
2026-03-27 11:08:15 -07:00
committed by GitHub
398 changed files with 18947 additions and 6535 deletions
+8
View File
@@ -11,6 +11,7 @@ import {
shutdownTelemetry,
isTelemetrySdkInitialized,
ExitCodes,
resetBrowserSession,
} from '@google/gemini-cli-core';
import type { Config } from '@google/gemini-cli-core';
@@ -72,6 +73,13 @@ export async function runExitCleanup() {
}
cleanupFunctions.length = 0; // Clear the array
// Close persistent browser sessions before disposing config
try {
await resetBrowserSession();
} catch (_) {
// Ignore errors during browser cleanup
}
if (configForTelemetry) {
try {
await configForTelemetry.dispose();
+3 -2
View File
@@ -20,6 +20,7 @@ import {
coreEvents,
getErrorType,
getErrorMessage,
getErrorType,
} from '@google/gemini-cli-core';
import { runSyncCleanup } from './cleanup.js';
@@ -178,7 +179,7 @@ export function handleCancellationError(config: Config): never {
timestamp: new Date().toISOString(),
status: 'error',
error: {
type: 'FatalCancellationError',
type: getErrorType(cancellationError),
message: cancellationError.message,
},
stats: streamFormatter.convertToStreamStats(metrics, 0),
@@ -219,7 +220,7 @@ export function handleMaxTurnsExceededError(config: Config): never {
timestamp: new Date().toISOString(),
status: 'error',
error: {
type: 'FatalTurnLimitedError',
type: getErrorType(maxTurnsError),
message: maxTurnsError.message,
},
stats: streamFormatter.convertToStreamStats(metrics, 0),
@@ -197,7 +197,9 @@ describe('handleAutoUpdate', () => {
expect(updateEventEmitter.emit).toHaveBeenCalledTimes(1);
expect(updateEventEmitter.emit).toHaveBeenCalledWith('update-received', {
...mockUpdateInfo,
message: 'An update is available!\nPlease update manually.',
isUpdating: false,
});
expect(mockSpawn).not.toHaveBeenCalled();
});
@@ -236,7 +238,9 @@ describe('handleAutoUpdate', () => {
expect(updateEventEmitter.emit).toHaveBeenCalledTimes(1);
expect(updateEventEmitter.emit).toHaveBeenCalledWith('update-received', {
...mockUpdateInfo,
message: 'An update is available!\nCannot determine update command.',
isUpdating: false,
});
expect(mockSpawn).not.toHaveBeenCalled();
});
@@ -253,7 +257,9 @@ describe('handleAutoUpdate', () => {
expect(updateEventEmitter.emit).toHaveBeenCalledTimes(1);
expect(updateEventEmitter.emit).toHaveBeenCalledWith('update-received', {
...mockUpdateInfo,
message: 'An update is available!\nThis is an additional message.',
isUpdating: false,
});
});
+10 -5
View File
@@ -102,17 +102,22 @@ export function handleAutoUpdate(
combinedMessage += `\n${installationInfo.updateMessage}`;
}
updateEventEmitter.emit('update-received', {
message: combinedMessage,
});
if (
!installationInfo.updateCommand ||
!settings.merged.general.enableAutoUpdate
) {
updateEventEmitter.emit('update-received', {
...info,
message: combinedMessage,
isUpdating: false,
});
return;
}
updateEventEmitter.emit('update-received', {
...info,
message: combinedMessage,
isUpdating: true,
});
if (_updateInProgress) {
return;
}
@@ -106,6 +106,8 @@ describe('Session Cleanup (Refactored)', () => {
);
// Session directory
await fs.mkdir(path.join(testTempDir, sessionId), { recursive: true });
// Subagent chats directory
await fs.mkdir(path.join(chatsDir, sessionId), { recursive: true });
}
async function seedSessions() {
@@ -274,6 +276,7 @@ describe('Session Cleanup (Refactored)', () => {
existsSync(path.join(toolOutputsDir, `session-${sessions[1].id}`)),
).toBe(false);
expect(existsSync(path.join(testTempDir, sessions[1].id))).toBe(false); // Session directory should be deleted
expect(existsSync(path.join(chatsDir, sessions[1].id))).toBe(false); // Subagent chats directory should be deleted
});
it('should NOT delete sessions within the cutoff date', async () => {
+8 -36
View File
@@ -13,6 +13,8 @@ import {
Storage,
TOOL_OUTPUTS_DIR,
type Config,
deleteSessionArtifactsAsync,
deleteSubagentSessionDirAndArtifactsAsync,
} from '@google/gemini-cli-core';
import type { Settings, SessionRetentionSettings } from '../config/settings.js';
import { getAllSessionFiles, type SessionFileEntry } from './sessionUtils.js';
@@ -59,48 +61,18 @@ function deriveShortIdFromFileName(fileName: string): string | null {
return null;
}
/**
* Gets the log path for a session ID.
*/
function getSessionLogPath(tempDir: string, safeSessionId: string): string {
return path.join(tempDir, 'logs', `session-${safeSessionId}.jsonl`);
}
/**
* Cleans up associated artifacts (logs, tool-outputs, directory) for a session.
*/
async function deleteSessionArtifactsAsync(
async function cleanupSessionAndSubagentsAsync(
sessionId: string,
config: Config,
): Promise<void> {
const tempDir = config.storage.getProjectTempDir();
const chatsDir = path.join(tempDir, 'chats');
// Cleanup logs
const logsDir = path.join(tempDir, 'logs');
const safeSessionId = sanitizeFilenamePart(sessionId);
const logPath = getSessionLogPath(tempDir, safeSessionId);
if (logPath.startsWith(logsDir)) {
await fs.unlink(logPath).catch(() => {});
}
// Cleanup tool outputs
const toolOutputDir = path.join(
tempDir,
TOOL_OUTPUTS_DIR,
`session-${safeSessionId}`,
);
const toolOutputsBase = path.join(tempDir, TOOL_OUTPUTS_DIR);
if (toolOutputDir.startsWith(toolOutputsBase)) {
await fs
.rm(toolOutputDir, { recursive: true, force: true })
.catch(() => {});
}
// Cleanup session directory
const sessionDir = path.join(tempDir, safeSessionId);
if (safeSessionId && sessionDir.startsWith(tempDir + path.sep)) {
await fs.rm(sessionDir, { recursive: true, force: true }).catch(() => {});
}
await deleteSessionArtifactsAsync(sessionId, tempDir);
await deleteSubagentSessionDirAndArtifactsAsync(sessionId, chatsDir, tempDir);
}
/**
@@ -201,7 +173,7 @@ export async function cleanupExpiredSessions(
await fs.unlink(filePath);
if (fullSessionId) {
await deleteSessionArtifactsAsync(fullSessionId, config);
await cleanupSessionAndSubagentsAsync(fullSessionId, config);
}
result.deleted++;
} else {
@@ -230,7 +202,7 @@ export async function cleanupExpiredSessions(
const sessionId = sessionToDelete.sessionInfo?.id;
if (sessionId) {
await deleteSessionArtifactsAsync(sessionId, config);
await cleanupSessionAndSubagentsAsync(sessionId, config);
}
if (config.getDebugMode()) {
+1 -1
View File
@@ -97,7 +97,7 @@ export async function deleteSession(
try {
// Use ChatRecordingService to delete the session
const chatRecordingService = new ChatRecordingService(config);
chatRecordingService.deleteSession(sessionToDelete.file);
await chatRecordingService.deleteSession(sessionToDelete.file);
const time = formatRelativeTime(sessionToDelete.lastUpdated);
writeToStdout(
@@ -43,7 +43,7 @@ describe('terminal notifications', () => {
});
});
it('returns false without writing on non-macOS platforms', async () => {
it('emits notification on non-macOS platforms', async () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
configurable: true,
@@ -54,8 +54,8 @@ describe('terminal notifications', () => {
body: 'b',
});
expect(shown).toBe(false);
expect(writeToStdout).not.toHaveBeenCalled();
expect(shown).toBe(true);
expect(writeToStdout).toHaveBeenCalled();
});
it('returns false without writing when disabled', async () => {
@@ -69,6 +69,7 @@ describe('terminal notifications', () => {
});
it('emits OSC 9 notification when supported terminal is detected', async () => {
vi.stubEnv('WT_SESSION', '');
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
const shown = await notifyViaTerminal(true, {
@@ -126,6 +127,7 @@ describe('terminal notifications', () => {
});
it('strips terminal control sequences and newlines from payload text', async () => {
vi.stubEnv('WT_SESSION', '');
vi.stubEnv('TERM_PROGRAM', 'iTerm.app');
const shown = await notifyViaTerminal(true, {
@@ -75,17 +75,10 @@ export function buildRunEventNotificationContent(
export function isNotificationsEnabled(settings: LoadedSettings): boolean {
const general = settings.merged.general as
| {
enableNotifications?: boolean;
enableMacOsNotifications?: boolean;
}
| { enableNotifications?: boolean }
| undefined;
return (
process.platform === 'darwin' &&
(general?.enableNotifications === true ||
general?.enableMacOsNotifications === true)
);
return general?.enableNotifications === true;
}
function buildTerminalNotificationMessage(
@@ -112,7 +105,7 @@ export async function notifyViaTerminal(
notificationsEnabled: boolean,
content: RunEventNotificationContent,
): Promise<boolean> {
if (!notificationsEnabled || process.platform !== 'darwin') {
if (!notificationsEnabled) {
return false;
}