Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/cli/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ they appear in the UI.
| Vim Mode | `general.vimMode` | Enable Vim keybindings | `false` |
| Default Approval Mode | `general.defaultApprovalMode` | The default approval mode for tool execution. 'default' prompts for approval, 'auto_edit' auto-approves edit tools, and 'plan' is read-only mode. 'yolo' is not supported yet. | `"default"` |
| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` |
| Enable Notifications | `general.enableNotifications` | Enable run-event notifications for action-required prompts and session completion. Currently macOS only. | `false` |
| Enable Prompt Completion | `general.enablePromptCompletion` | Enable AI-powered prompt completion suggestions while typing. | `false` |
| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` |
| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `false` |
Expand Down
5 changes: 5 additions & 0 deletions docs/get-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Enable update notification prompts.
- **Default:** `true`

- **`general.enableNotifications`** (boolean):
- **Description:** Enable run-event notifications for action-required prompts
and session completion. Currently macOS only.
- **Default:** `false`

- **`general.checkpointing.enabled`** (boolean):
- **Description:** Enable session checkpointing for recovery
- **Default:** `false`
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/config/settingsSchema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,17 @@ describe('SettingsSchema', () => {
).toBe('Show the "? for shortcuts" hint above the input.');
});

it('should have enableNotifications setting in schema', () => {
const setting =
getSettingsSchema().general.properties.enableNotifications;
expect(setting).toBeDefined();
expect(setting.type).toBe('boolean');
expect(setting.category).toBe('General');
expect(setting.default).toBe(false);
expect(setting.requiresRestart).toBe(false);
expect(setting.showInDialog).toBe(true);
});

it('should have enableAgents setting in schema', () => {
const setting = getSettingsSchema().experimental.properties.enableAgents;
expect(setting).toBeDefined();
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,16 @@ const SETTINGS_SCHEMA = {
description: 'Enable update notification prompts.',
showInDialog: false,
},
enableNotifications: {
type: 'boolean',
label: 'Enable Notifications',
category: 'General',
requiresRestart: false,
default: false,
description:
'Enable run-event notifications for action-required prompts and session completion. Currently macOS only.',
showInDialog: true,
},
checkpointing: {
type: 'object',
label: 'Checkpointing',
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/gemini.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,20 @@ vi.mock('./nonInteractiveCli.js', () => ({
runNonInteractive: runNonInteractiveSpy,
}));

const terminalNotificationMocks = vi.hoisted(() => ({
notifyViaTerminal: vi.fn().mockResolvedValue(true),
buildRunEventNotificationContent: vi.fn(() => ({
title: 'Session complete',
body: 'done',
subtitle: 'Run finished',
})),
}));
vi.mock('./utils/terminalNotifications.js', () => ({
notifyViaTerminal: terminalNotificationMocks.notifyViaTerminal,
buildRunEventNotificationContent:
terminalNotificationMocks.buildRunEventNotificationContent,
}));

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
Expand Down Expand Up @@ -837,6 +851,10 @@ describe('gemini.tsx main function kitty protocol', () => {
expect(runNonInteractive).toHaveBeenCalled();
const callArgs = vi.mocked(runNonInteractive).mock.calls[0][0];
expect(callArgs.input).toBe('stdin-data\n\ntest-question');
expect(
terminalNotificationMocks.buildRunEventNotificationContent,
).not.toHaveBeenCalled();
expect(terminalNotificationMocks.notifyViaTerminal).not.toHaveBeenCalled();
expect(processExitSpy).toHaveBeenCalledWith(0);
processExitSpy.mockRestore();
});
Expand Down
53 changes: 31 additions & 22 deletions packages/cli/src/gemini_cleanup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,6 @@ import { main } from './gemini.js';
import { debugLogger } from '@google/gemini-cli-core';
import { type Config } from '@google/gemini-cli-core';

// Custom error to identify mock process.exit calls
class MockProcessExitError extends Error {
constructor(readonly code?: string | number | null | undefined) {
super('PROCESS_EXIT_MOCKED');
this.name = 'MockProcessExitError';
}
}

vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
Expand Down Expand Up @@ -124,10 +116,39 @@ vi.mock('./validateNonInterActiveAuth.js', () => ({
validateNonInteractiveAuth: vi.fn().mockResolvedValue({}),
}));

vi.mock('./core/initializer.js', () => ({
initializeApp: vi.fn().mockResolvedValue({
authError: null,
themeError: null,
shouldOpenAuthDialog: false,
geminiMdFileCount: 0,
}),
}));

vi.mock('./nonInteractiveCli.js', () => ({
runNonInteractive: vi.fn().mockResolvedValue(undefined),
}));

vi.mock('./utils/cleanup.js', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils/cleanup.js')>();
return {
...actual,
cleanupCheckpoints: vi.fn().mockResolvedValue(undefined),
registerCleanup: vi.fn(),
registerSyncCleanup: vi.fn(),
registerTelemetryConfig: vi.fn(),
runExitCleanup: vi.fn().mockResolvedValue(undefined),
};
});

vi.mock('./zed-integration/zedIntegration.js', () => ({
runZedIntegration: vi.fn().mockResolvedValue(undefined),
}));

vi.mock('./utils/readStdin.js', () => ({
readStdin: vi.fn().mockResolvedValue(''),
}));

const { cleanupMockState } = vi.hoisted(() => ({
cleanupMockState: { shouldThrow: false, called: false },
}));
Expand Down Expand Up @@ -169,12 +190,6 @@ describe('gemini.tsx main function cleanup', () => {
const debugLoggerErrorSpy = vi
.spyOn(debugLogger, 'error')
.mockImplementation(() => {});
const processExitSpy = vi
.spyOn(process, 'exit')
.mockImplementation((code) => {
throw new MockProcessExitError(code);
});

vi.mocked(loadSettings).mockReturnValue({
merged: { advanced: {}, security: { auth: {} }, ui: {} },
workspace: { settings: {} },
Expand All @@ -201,7 +216,7 @@ describe('gemini.tsx main function cleanup', () => {
getMcpServers: () => ({}),
getMcpClientManager: vi.fn(),
getIdeMode: vi.fn(() => false),
getExperimentalZedIntegration: vi.fn(() => false),
getExperimentalZedIntegration: vi.fn(() => true),
getScreenReader: vi.fn(() => false),
getGeminiMdFileCount: vi.fn(() => 0),
getProjectRoot: vi.fn(() => '/'),
Expand All @@ -224,18 +239,12 @@ describe('gemini.tsx main function cleanup', () => {
getRemoteAdminSettings: vi.fn(() => undefined),
} as any); // eslint-disable-line @typescript-eslint/no-explicit-any

try {
await main();
} catch (e) {
if (!(e instanceof MockProcessExitError)) throw e;
}
await main();

expect(cleanupMockState.called).toBe(true);
expect(debugLoggerErrorSpy).toHaveBeenCalledWith(
'Failed to cleanup expired sessions:',
expect.objectContaining({ message: 'Cleanup failed' }),
);
expect(processExitSpy).toHaveBeenCalledWith(0); // Should not exit on cleanup failure
processExitSpy.mockRestore();
});
});
Loading
Loading