diff --git a/docs/cli/settings.md b/docs/cli/settings.md index 88a5d2ff83..6717bcce9a 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -24,20 +24,21 @@ they appear in the UI. ### General -| UI Label | Setting | Description | Default | -| ----------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -| 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 mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"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. | `false` | -| Enable Plan Mode | `general.plan.enabled` | Enable Plan Mode for read-only safety during planning. | `true` | -| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` | -| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` | -| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` | -| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` | -| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` | -| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` | -| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` | +| UI Label | Setting | Description | Default | +| ----------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| 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 mode (auto-approve all actions) can only be enabled via command line (--yolo or --approval-mode=yolo). | `"default"` | +| Enable Auto Update | `general.enableAutoUpdate` | Enable automatic updates. | `true` | +| Enable Terminal Notifications | `general.enableNotifications` | Enable terminal run-event notifications for action-required prompts and session completion. | `false` | +| Terminal Notification Method | `general.notificationMethod` | How to send terminal notifications. | `"auto"` | +| Enable Plan Mode | `general.plan.enabled` | Enable Plan Mode for read-only safety during planning. | `true` | +| Plan Directory | `general.plan.directory` | The directory where planning artifacts are stored. If not specified, defaults to the system temporary directory. A custom directory requires a policy to allow write access in Plan Mode. | `undefined` | +| Plan Model Routing | `general.plan.modelRouting` | Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pro for the planning phase and Flash for the implementation phase. | `true` | +| Retry Fetch Errors | `general.retryFetchErrors` | Retry on "exception TypeError: fetch failed sending request" errors. | `true` | +| Max Chat Model Attempts | `general.maxAttempts` | Maximum number of attempts for requests to the main chat model. Cannot exceed 10. | `10` | +| Debug Keystroke Logging | `general.debugKeystrokeLogging` | Enable debug logging of keystrokes to the console. | `false` | +| Enable Session Cleanup | `general.sessionRetention.enabled` | Enable automatic session cleanup | `true` | +| Keep chat history | `general.sessionRetention.maxAge` | Automatically delete chats older than this time period (e.g., "30d", "7d", "24h", "1w") | `"30d"` | ### Output diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 2047a9b09d..40eb9d11ca 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -134,10 +134,15 @@ their corresponding top-level category object in your `settings.json` file. - **Default:** `true` - **`general.enableNotifications`** (boolean): - - **Description:** Enable run-event notifications for action-required prompts - and session completion. + - **Description:** Enable terminal run-event notifications for action-required + prompts and session completion. - **Default:** `false` +- **`general.notificationMethod`** (enum): + - **Description:** How to send terminal notifications. + - **Default:** `"auto"` + - **Values:** `"auto"`, `"osc9"`, `"osc777"`, `"bell"` + - **`general.checkpointing.enabled`** (boolean): - **Description:** Enable session checkpointing for recovery - **Default:** `false` diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index fcfd604e3a..ae7d1ebdf5 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -256,14 +256,29 @@ const SETTINGS_SCHEMA = { }, enableNotifications: { type: 'boolean', - label: 'Enable Notifications', + label: 'Enable Terminal Notifications', category: 'General', requiresRestart: false, default: false, description: - 'Enable run-event notifications for action-required prompts and session completion.', + 'Enable terminal run-event notifications for action-required prompts and session completion.', showInDialog: true, }, + notificationMethod: { + type: 'enum', + label: 'Terminal Notification Method', + category: 'General', + requiresRestart: false, + default: 'auto', + description: 'How to send terminal notifications.', + showInDialog: true, + options: [ + { value: 'auto', label: 'Auto' }, + { value: 'osc9', label: 'OSC 9' }, + { value: 'osc777', label: 'OSC 777' }, + { value: 'bell', label: 'Bell' }, + ], + }, checkpointing: { type: 'object', label: 'Checkpointing', diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 8f05b996dc..92a519856a 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -53,6 +53,7 @@ const mocks = vi.hoisted(() => ({ const terminalNotificationsMocks = vi.hoisted(() => ({ notifyViaTerminal: vi.fn().mockResolvedValue(true), isNotificationsEnabled: vi.fn(() => true), + getNotificationMethod: vi.fn(() => 'auto'), buildRunEventNotificationContent: vi.fn((event) => ({ title: 'Mock Notification', subtitle: 'Mock Subtitle', @@ -194,6 +195,7 @@ vi.mock('./hooks/useShellInactivityStatus.js', () => ({ vi.mock('../utils/terminalNotifications.js', () => ({ notifyViaTerminal: terminalNotificationsMocks.notifyViaTerminal, isNotificationsEnabled: terminalNotificationsMocks.isNotificationsEnabled, + getNotificationMethod: terminalNotificationsMocks.getNotificationMethod, buildRunEventNotificationContent: terminalNotificationsMocks.buildRunEventNotificationContent, })); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index f17ac0d756..c680c5d463 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -181,7 +181,10 @@ import { useTimedMessage } from './hooks/useTimedMessage.js'; import { useIsHelpDismissKey } from './utils/shortcutsHelp.js'; import { useSuspend } from './hooks/useSuspend.js'; import { useRunEventNotifications } from './hooks/useRunEventNotifications.js'; -import { isNotificationsEnabled } from '../utils/terminalNotifications.js'; +import { + isNotificationsEnabled, + getNotificationMethod, +} from '../utils/terminalNotifications.js'; import { getLastTurnToolCallIds, isToolExecuting, @@ -225,6 +228,7 @@ export const AppContainer = (props: AppContainerProps) => { const settings = useSettings(); const { reset } = useOverflowActions()!; const notificationsEnabled = isNotificationsEnabled(settings); + const notificationMethod = getNotificationMethod(settings); const { setOptions, dumpCurrentFrame, startRecording, stopRecording } = useContext(InkAppContext); @@ -2284,6 +2288,7 @@ Logging in with Google... Restarting Gemini CLI to continue. useRunEventNotifications({ notificationsEnabled, + notificationMethod, isFocused, hasReceivedFocusEvent, streamingState, diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg index a9673bc3b7..a07095d37b 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg @@ -67,47 +67,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg index 72a11cad81..e90fa4363e 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg @@ -67,47 +67,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg index 8f4daa80ae..2a30e9f212 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg @@ -67,47 +67,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg index a9673bc3b7..a07095d37b 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg @@ -67,47 +67,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg index a9673bc3b7..a07095d37b 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg @@ -67,47 +67,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg index 4068847a9c..31b81aa227 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg @@ -56,47 +56,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg index 93ba308209..926eaef5e7 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg @@ -67,47 +67,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg index a9673bc3b7..a07095d37b 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg @@ -67,47 +67,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg index b49d53d02c..54288835ea 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg @@ -67,47 +67,47 @@ - Enable Notifications + Enable Terminal Notifications false - Enable run-event notifications for action-required prompts and session completion. + Enable terminal run-event notifications for action-required prompts and session com… - Enable Plan Mode - true + Terminal Notification Method + Auto - Enable Plan Mode for read-only safety during planning. + How to send terminal notifications. - Plan Directory - undefined + Enable Plan Mode + true - The directory where planning artifacts are stored. If not specified, defaults t… + Enable Plan Mode for read-only safety during planning. - Plan Model Routing - true + Plan Directory + undefined - Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… + The directory where planning artifacts are stored. If not specified, defaults t… - Retry Fetch Errors + Plan Model Routing true - Retry on "exception TypeError: fetch failed sending request" errors. + Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap index d585c9a918..a7f994ed68 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog.test.tsx.snap @@ -19,8 +19,11 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v │ Enable Auto Update true │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -31,9 +34,6 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ Apply To │ @@ -65,8 +65,11 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings │ Enable Auto Update true │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -77,9 +80,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ Apply To │ @@ -111,8 +111,11 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d │ Enable Auto Update true* │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -123,9 +126,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ Apply To │ @@ -157,8 +157,11 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct │ Enable Auto Update true │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -169,9 +172,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ Apply To │ @@ -203,8 +203,11 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting │ Enable Auto Update true │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -215,9 +218,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ Apply To │ @@ -249,8 +249,11 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec │ Enable Auto Update true │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -261,9 +264,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ > Apply To │ @@ -295,8 +295,11 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb │ Enable Auto Update false* │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -307,9 +310,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ Apply To │ @@ -341,8 +341,11 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set │ Enable Auto Update true │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -353,9 +356,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ Apply To │ @@ -387,8 +387,11 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ Enable Auto Update false* │ │ Enable automatic updates. │ │ │ -│ Enable Notifications false │ -│ Enable run-event notifications for action-required prompts and session completion. │ +│ Enable Terminal Notifications false │ +│ Enable terminal run-event notifications for action-required prompts and session com… │ +│ │ +│ Terminal Notification Method Auto │ +│ How to send terminal notifications. │ │ │ │ Enable Plan Mode true │ │ Enable Plan Mode for read-only safety during planning. │ @@ -399,9 +402,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin │ Plan Model Routing true │ │ Automatically switch between Pro and Flash models based on Plan Mode status. Uses Pr… │ │ │ -│ Retry Fetch Errors true │ -│ Retry on "exception TypeError: fetch failed sending request" errors. │ -│ │ │ ▼ │ │ │ │ Apply To │ diff --git a/packages/cli/src/ui/hooks/useRunEventNotifications.ts b/packages/cli/src/ui/hooks/useRunEventNotifications.ts index 3051847afb..8dea01f0d2 100644 --- a/packages/cli/src/ui/hooks/useRunEventNotifications.ts +++ b/packages/cli/src/ui/hooks/useRunEventNotifications.ts @@ -15,12 +15,14 @@ import { getPendingAttentionNotification } from '../utils/pendingAttentionNotifi import { buildRunEventNotificationContent, notifyViaTerminal, + type TerminalNotificationMethod, } from '../../utils/terminalNotifications.js'; const ATTENTION_NOTIFICATION_COOLDOWN_MS = 20_000; interface RunEventNotificationParams { notificationsEnabled: boolean; + notificationMethod: TerminalNotificationMethod; isFocused: boolean; hasReceivedFocusEvent: boolean; streamingState: StreamingState; @@ -36,6 +38,7 @@ interface RunEventNotificationParams { export function useRunEventNotifications({ notificationsEnabled, + notificationMethod, isFocused, hasReceivedFocusEvent, streamingState, @@ -124,11 +127,13 @@ export function useRunEventNotifications({ void notifyViaTerminal( notificationsEnabled, buildRunEventNotificationContent(pendingAttentionNotification.event), + notificationMethod, ); }, [ isFocused, hasReceivedFocusEvent, notificationsEnabled, + notificationMethod, pendingAttentionNotification, ]); @@ -159,12 +164,14 @@ export function useRunEventNotifications({ type: 'session_complete', detail: 'Gemini CLI finished responding.', }), + notificationMethod, ); }, [ streamingState, isFocused, hasReceivedFocusEvent, notificationsEnabled, + notificationMethod, hasPendingActionRequired, ]); } diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts index 4cc25586b5..261b0e6ca9 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.test.ts @@ -365,76 +365,123 @@ describe('TerminalCapabilityManager', () => { ); }); - describe('supportsOsc9Notifications', () => { + describe('isTmux', () => { const manager = TerminalCapabilityManager.getInstance(); - it.each([ - { - name: 'WezTerm (terminal name)', - terminalName: 'WezTerm', - env: {}, - expected: true, - }, - { - name: 'iTerm.app (terminal name)', - terminalName: 'iTerm.app', - env: {}, - expected: true, - }, - { - name: 'ghostty (terminal name)', - terminalName: 'ghostty', - env: {}, - expected: true, - }, - { - name: 'kitty (terminal name)', - terminalName: 'kitty', - env: {}, - expected: true, - }, - { - name: 'some-other-term (terminal name)', - terminalName: 'some-other-term', - env: {}, - expected: false, - }, - { - name: 'iTerm.app (TERM_PROGRAM)', - terminalName: undefined, - env: { TERM_PROGRAM: 'iTerm.app' }, - expected: true, - }, - { - name: 'vscode (TERM_PROGRAM)', - terminalName: undefined, - env: { TERM_PROGRAM: 'vscode' }, - expected: false, - }, - { - name: 'xterm-kitty (TERM)', - terminalName: undefined, - env: { TERM: 'xterm-kitty' }, - expected: true, - }, - { - name: 'xterm-256color (TERM)', - terminalName: undefined, - env: { TERM: 'xterm-256color' }, - expected: false, - }, - { - name: 'Windows Terminal (WT_SESSION)', - terminalName: 'iTerm.app', - env: { WT_SESSION: 'some-guid' }, - expected: false, - }, - ])( - 'should return $expected for $name', - ({ terminalName, env, expected }) => { - vi.spyOn(manager, 'getTerminalName').mockReturnValue(terminalName); - expect(manager.supportsOsc9Notifications(env)).toBe(expected); - }, - ); + it('returns true when TMUX is set', () => { + expect(manager.isTmux({ TMUX: '1' })).toBe(true); + expect(manager.isTmux({ TMUX: 'tmux-1234' })).toBe(true); + }); + + it('returns false when TMUX is not set', () => { + expect(manager.isTmux({})).toBe(false); + expect(manager.isTmux({ STY: '1' })).toBe(false); + }); + }); + + describe('isScreen', () => { + const manager = TerminalCapabilityManager.getInstance(); + + it('returns true when STY is set', () => { + expect(manager.isScreen({ STY: '1' })).toBe(true); + expect(manager.isScreen({ STY: 'screen.1234' })).toBe(true); + }); + + it('returns false when STY is not set', () => { + expect(manager.isScreen({})).toBe(false); + expect(manager.isScreen({ TMUX: '1' })).toBe(false); + }); + }); + + describe('isITerm2', () => { + const manager = TerminalCapabilityManager.getInstance(); + + it('returns true when iTerm is in terminal name', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue('iTerm.app'); + expect(manager.isITerm2({})).toBe(true); + }); + + it('returns true when TERM_PROGRAM is iTerm.app', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined); + expect(manager.isITerm2({ TERM_PROGRAM: 'iTerm.app' })).toBe(true); + }); + + it('returns false otherwise', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue('xterm'); + expect(manager.isITerm2({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false); + }); + }); + + describe('isAlacritty', () => { + const manager = TerminalCapabilityManager.getInstance(); + + it('returns true when ALACRITTY_WINDOW_ID is set', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined); + expect(manager.isAlacritty({ ALACRITTY_WINDOW_ID: '123' })).toBe(true); + }); + + it('returns true when TERM is alacritty', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined); + expect(manager.isAlacritty({ TERM: 'alacritty' })).toBe(true); + }); + + it('returns true when terminal name contains alacritty', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue('alacritty'); + expect(manager.isAlacritty({})).toBe(true); + }); + + it('returns false otherwise', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined); + expect(manager.isAlacritty({ TERM: 'xterm' })).toBe(false); + }); + }); + + describe('isAppleTerminal', () => { + const manager = TerminalCapabilityManager.getInstance(); + + it('returns true when apple_terminal is in terminal name', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue('apple_terminal'); + expect(manager.isAppleTerminal({})).toBe(true); + }); + + it('returns true when TERM_PROGRAM is Apple_Terminal', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue(undefined); + expect(manager.isAppleTerminal({ TERM_PROGRAM: 'Apple_Terminal' })).toBe( + true, + ); + }); + + it('returns false otherwise', () => { + vi.spyOn(manager, 'getTerminalName').mockReturnValue('xterm'); + expect(manager.isAppleTerminal({ TERM_PROGRAM: 'iTerm.app' })).toBe( + false, + ); + }); + }); + + describe('isVSCodeTerminal', () => { + const manager = TerminalCapabilityManager.getInstance(); + + it('returns true when TERM_PROGRAM is vscode', () => { + expect(manager.isVSCodeTerminal({ TERM_PROGRAM: 'vscode' })).toBe(true); + }); + + it('returns false otherwise', () => { + expect(manager.isVSCodeTerminal({ TERM_PROGRAM: 'iTerm.app' })).toBe( + false, + ); + }); + }); + + describe('isWindowsTerminal', () => { + const manager = TerminalCapabilityManager.getInstance(); + + it('returns true when WT_SESSION is set', () => { + expect(manager.isWindowsTerminal({ WT_SESSION: 'some-guid' })).toBe(true); + }); + + it('returns false otherwise', () => { + expect(manager.isWindowsTerminal({})).toBe(false); + }); }); }); diff --git a/packages/cli/src/ui/utils/terminalCapabilityManager.ts b/packages/cli/src/ui/utils/terminalCapabilityManager.ts index ddbbad4ce8..e0fc6c01b8 100644 --- a/packages/cli/src/ui/utils/terminalCapabilityManager.ts +++ b/packages/cli/src/ui/utils/terminalCapabilityManager.ts @@ -284,31 +284,43 @@ export class TerminalCapabilityManager { ); } - supportsOsc9Notifications(env: NodeJS.ProcessEnv = process.env): boolean { - if (env['WT_SESSION']) { - return false; - } + isTmux(env: NodeJS.ProcessEnv = process.env): boolean { + return !!env['TMUX']; + } - return ( - this.hasOsc9TerminalSignature(this.getTerminalName()) || - this.hasOsc9TerminalSignature(env['TERM_PROGRAM']) || - this.hasOsc9TerminalSignature(env['TERM']) + isScreen(env: NodeJS.ProcessEnv = process.env): boolean { + return !!env['STY']; + } + + isITerm2(env: NodeJS.ProcessEnv = process.env): boolean { + return !!( + this.getTerminalName()?.toLowerCase().includes('iterm') || + env['TERM_PROGRAM']?.toLowerCase().includes('iterm') ); } - private hasOsc9TerminalSignature(value: string | undefined): boolean { - if (!value) { - return false; - } - - const normalized = value.toLowerCase(); - return ( - normalized.includes('wezterm') || - normalized.includes('ghostty') || - normalized.includes('iterm') || - normalized.includes('kitty') + isAlacritty(env: NodeJS.ProcessEnv = process.env): boolean { + return !!( + this.getTerminalName()?.toLowerCase().includes('alacritty') || + env['ALACRITTY_WINDOW_ID'] || + env['TERM']?.toLowerCase().includes('alacritty') ); } + + isAppleTerminal(env: NodeJS.ProcessEnv = process.env): boolean { + return !!( + this.getTerminalName()?.toLowerCase().includes('apple_terminal') || + env['TERM_PROGRAM']?.toLowerCase().includes('apple_terminal') + ); + } + + isVSCodeTerminal(env: NodeJS.ProcessEnv = process.env): boolean { + return !!env['TERM_PROGRAM']?.toLowerCase().includes('vscode'); + } + + isWindowsTerminal(env: NodeJS.ProcessEnv = process.env): boolean { + return !!env['WT_SESSION']; + } } export const terminalCapabilityManager = diff --git a/packages/cli/src/utils/terminalNotifications.test.ts b/packages/cli/src/utils/terminalNotifications.test.ts index f05e650325..4abd3dddc4 100644 --- a/packages/cli/src/utils/terminalNotifications.test.ts +++ b/packages/cli/src/utils/terminalNotifications.test.ts @@ -11,6 +11,7 @@ import { MAX_NOTIFICATION_SUBTITLE_CHARS, MAX_NOTIFICATION_TITLE_CHARS, notifyViaTerminal, + TerminalNotificationMethod, } from './terminalNotifications.js'; const writeToStdout = vi.hoisted(() => vi.fn()); @@ -24,38 +25,19 @@ vi.mock('@google/gemini-cli-core', () => ({ })); describe('terminal notifications', () => { - const originalPlatform = process.platform; - beforeEach(() => { vi.resetAllMocks(); vi.unstubAllEnvs(); - Object.defineProperty(process, 'platform', { - value: 'darwin', - configurable: true, - }); + vi.stubEnv('TMUX', ''); + vi.stubEnv('STY', ''); + vi.stubEnv('WT_SESSION', ''); + vi.stubEnv('TERM_PROGRAM', ''); + vi.stubEnv('TERM', ''); + vi.stubEnv('ALACRITTY_WINDOW_ID', ''); }); afterEach(() => { vi.unstubAllEnvs(); - Object.defineProperty(process, 'platform', { - value: originalPlatform, - configurable: true, - }); - }); - - it('emits notification on non-macOS platforms', async () => { - Object.defineProperty(process, 'platform', { - value: 'linux', - configurable: true, - }); - - const shown = await notifyViaTerminal(true, { - title: 't', - body: 'b', - }); - - expect(shown).toBe(true); - expect(writeToStdout).toHaveBeenCalled(); }); it('returns false without writing when disabled', async () => { @@ -68,8 +50,7 @@ describe('terminal notifications', () => { expect(writeToStdout).not.toHaveBeenCalled(); }); - it('emits OSC 9 notification when supported terminal is detected', async () => { - vi.stubEnv('WT_SESSION', ''); + it('emits OSC 9 notification when iTerm2 is detected', async () => { vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); const shown = await notifyViaTerminal(true, { @@ -85,23 +66,57 @@ describe('terminal notifications', () => { expect(emitted.endsWith('\x07')).toBe(true); }); - it('emits BEL fallback when OSC 9 is not supported', async () => { - vi.stubEnv('TERM_PROGRAM', ''); - vi.stubEnv('TERM', ''); - + it('emits OSC 777 for unknown terminals', async () => { const shown = await notifyViaTerminal(true, { title: 'Title', subtitle: 'Subtitle', body: 'Body', }); + expect(shown).toBe(true); + expect(writeToStdout).toHaveBeenCalledTimes(1); + const emitted = String(writeToStdout.mock.calls[0][0]); + expect(emitted.startsWith('\x1b]777;notify;')).toBe(true); + }); + + it('uses BEL when Windows Terminal is detected', async () => { + vi.stubEnv('WT_SESSION', '1'); + + const shown = await notifyViaTerminal(true, { + title: 'Title', + body: 'Body', + }); + expect(shown).toBe(true); expect(writeToStdout).toHaveBeenCalledWith('\x07'); }); - it('uses BEL fallback when WT_SESSION is set', async () => { - vi.stubEnv('WT_SESSION', '1'); - vi.stubEnv('TERM_PROGRAM', 'WezTerm'); + it('uses BEL when Alacritty is detected', async () => { + vi.stubEnv('ALACRITTY_WINDOW_ID', '1'); + + const shown = await notifyViaTerminal(true, { + title: 'Title', + body: 'Body', + }); + + expect(shown).toBe(true); + expect(writeToStdout).toHaveBeenCalledWith('\x07'); + }); + + it('uses BEL when Apple Terminal is detected', async () => { + vi.stubEnv('TERM_PROGRAM', 'Apple_Terminal'); + + const shown = await notifyViaTerminal(true, { + title: 'Title', + body: 'Body', + }); + + expect(shown).toBe(true); + expect(writeToStdout).toHaveBeenCalledWith('\x07'); + }); + + it('uses BEL when VSCode Terminal is detected', async () => { + vi.stubEnv('TERM_PROGRAM', 'vscode'); const shown = await notifyViaTerminal(true, { title: 'Title', @@ -127,7 +142,6 @@ 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, { @@ -162,4 +176,124 @@ describe('terminal notifications', () => { MAX_NOTIFICATION_BODY_CHARS, ); }); + + it('emits OSC 9 notification when method is explicitly set to osc9', async () => { + // Explicitly set terminal to something that would normally use BEL + vi.stubEnv('WT_SESSION', '1'); + + const shown = await notifyViaTerminal( + true, + { + title: 'Explicit OSC 9', + body: 'Body', + }, + TerminalNotificationMethod.Osc9, + ); + + expect(shown).toBe(true); + expect(writeToStdout).toHaveBeenCalledTimes(1); + const emitted = String(writeToStdout.mock.calls[0][0]); + expect(emitted.startsWith('\x1b]9;')).toBe(true); + expect(emitted.endsWith('\x07')).toBe(true); + expect(emitted).toContain('Explicit OSC 9'); + }); + + it('emits OSC 777 notification when method is explicitly set to osc777', async () => { + // Explicitly set terminal to something that would normally use BEL + vi.stubEnv('WT_SESSION', '1'); + const shown = await notifyViaTerminal( + true, + { + title: 'Explicit OSC 777', + body: 'Body', + }, + TerminalNotificationMethod.Osc777, + ); + + expect(shown).toBe(true); + expect(writeToStdout).toHaveBeenCalledTimes(1); + const emitted = String(writeToStdout.mock.calls[0][0]); + expect(emitted.startsWith('\x1b]777;notify;')).toBe(true); + expect(emitted.endsWith('\x07')).toBe(true); + expect(emitted).toContain('Explicit OSC 777'); + }); + + it('emits BEL notification when method is explicitly set to bell', async () => { + // Explicitly set terminal to something that supports OSC 9 + vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); + + const shown = await notifyViaTerminal( + true, + { + title: 'Explicit BEL', + body: 'Body', + }, + TerminalNotificationMethod.Bell, + ); + + expect(shown).toBe(true); + expect(writeToStdout).toHaveBeenCalledTimes(1); + expect(writeToStdout).toHaveBeenCalledWith('\x07'); + }); + + it('replaces semicolons with colons in OSC 777 to avoid breaking the sequence', async () => { + const shown = await notifyViaTerminal( + true, + { + title: 'Title; with; semicolons', + subtitle: 'Sub;title', + body: 'Body; with; semicolons', + }, + TerminalNotificationMethod.Osc777, + ); + + expect(shown).toBe(true); + const emitted = String(writeToStdout.mock.calls[0][0]); + + // Format: \x1b]777;notify;title;body\x07 + expect(emitted).toContain('Title: with: semicolons'); + expect(emitted).toContain('Sub:title'); + expect(emitted).toContain('Body: with: semicolons'); + expect(emitted).not.toContain('Title; with; semicolons'); + expect(emitted).not.toContain('Body; with; semicolons'); + + // Extract everything after '\x1b]777;notify;' and before '\x07' + const payload = emitted.slice('\x1b]777;notify;'.length, -1); + + // There should be exactly one semicolon separating title and body + const semicolonsCount = (payload.match(/;/g) || []).length; + expect(semicolonsCount).toBe(1); + }); + + it('wraps OSC sequence in tmux passthrough when TMUX env var is set', async () => { + vi.stubEnv('TMUX', '1'); + vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); + + const shown = await notifyViaTerminal(true, { + title: 'Title', + body: 'Body', + }); + + expect(shown).toBe(true); + expect(writeToStdout).toHaveBeenCalledTimes(1); + const emitted = String(writeToStdout.mock.calls[0][0]); + expect(emitted.startsWith('\x1bPtmux;\x1b\x1b]9;')).toBe(true); + expect(emitted.endsWith('\x1b\\')).toBe(true); + }); + + it('wraps OSC sequence in GNU screen passthrough when STY env var is set', async () => { + vi.stubEnv('STY', '1'); + vi.stubEnv('TERM_PROGRAM', 'iTerm.app'); + + const shown = await notifyViaTerminal(true, { + title: 'Title', + body: 'Body', + }); + + expect(shown).toBe(true); + expect(writeToStdout).toHaveBeenCalledTimes(1); + const emitted = String(writeToStdout.mock.calls[0][0]); + expect(emitted.startsWith('\x1bP\x1b]9;')).toBe(true); + expect(emitted.endsWith('\x1b\\')).toBe(true); + }); }); diff --git a/packages/cli/src/utils/terminalNotifications.ts b/packages/cli/src/utils/terminalNotifications.ts index c0ad259a4b..ea2539ff0f 100644 --- a/packages/cli/src/utils/terminalNotifications.ts +++ b/packages/cli/src/utils/terminalNotifications.ts @@ -15,12 +15,8 @@ export const MAX_NOTIFICATION_BODY_CHARS = 180; const BEL = '\x07'; const OSC9_PREFIX = '\x1b]9;'; -const OSC9_SEPARATOR = ' | '; -const MAX_OSC9_MESSAGE_CHARS = - MAX_NOTIFICATION_TITLE_CHARS + - MAX_NOTIFICATION_SUBTITLE_CHARS + - MAX_NOTIFICATION_BODY_CHARS + - OSC9_SEPARATOR.length * 2; +const OSC777_PREFIX = '\x1b]777;notify;'; +const OSC_TEXT_SEPARATOR = ' | '; export interface RunEventNotificationContent { title: string; @@ -81,36 +77,100 @@ export function isNotificationsEnabled(settings: LoadedSettings): boolean { return general?.enableNotifications === true; } -function buildTerminalNotificationMessage( - content: RunEventNotificationContent, -): string { - const pieces = [content.title, content.subtitle, content.body].filter( - Boolean, - ); - const combined = pieces.join(OSC9_SEPARATOR); - return sanitizeForDisplay(combined, MAX_OSC9_MESSAGE_CHARS); +export enum TerminalNotificationMethod { + Auto = 'auto', + Osc9 = 'osc9', + Osc777 = 'osc777', + Bell = 'bell', +} + +export function getNotificationMethod( + settings: LoadedSettings, +): TerminalNotificationMethod { + switch (settings.merged.general?.notificationMethod) { + case TerminalNotificationMethod.Osc9: + return TerminalNotificationMethod.Osc9; + case TerminalNotificationMethod.Osc777: + return TerminalNotificationMethod.Osc777; + case TerminalNotificationMethod.Bell: + return TerminalNotificationMethod.Bell; + default: + return TerminalNotificationMethod.Auto; + } +} + +function wrapWithPassthrough(sequence: string): string { + const capabilityManager = TerminalCapabilityManager.getInstance(); + if (capabilityManager.isTmux()) { + // eslint-disable-next-line no-control-regex + return `\x1bPtmux;${sequence.replace(/\x1b/g, '\x1b\x1b')}\x1b\\`; + } else if (capabilityManager.isScreen()) { + return `\x1bP${sequence}\x1b\\`; + } + return sequence; } function emitOsc9Notification(content: RunEventNotificationContent): void { - const message = buildTerminalNotificationMessage(content); - if (!TerminalCapabilityManager.getInstance().supportsOsc9Notifications()) { - writeToStdout(BEL); - return; - } + const sanitized = sanitizeNotificationContent(content); + const pieces = [sanitized.title, sanitized.subtitle, sanitized.body].filter( + Boolean, + ); + const combined = pieces.join(OSC_TEXT_SEPARATOR); - writeToStdout(`${OSC9_PREFIX}${message}${BEL}`); + writeToStdout(wrapWithPassthrough(`${OSC9_PREFIX}${combined}${BEL}`)); +} + +function emitOsc777Notification(content: RunEventNotificationContent): void { + const sanitized = sanitizeNotificationContent(content); + const bodyParts = [sanitized.subtitle, sanitized.body].filter(Boolean); + const body = bodyParts.join(OSC_TEXT_SEPARATOR); + + // Replace ';' with ':' to avoid breaking the OSC 777 sequence + const safeTitle = sanitized.title.replace(/;/g, ':'); + const safeBody = body.replace(/;/g, ':'); + + writeToStdout( + wrapWithPassthrough(`${OSC777_PREFIX}${safeTitle};${safeBody}${BEL}`), + ); +} + +function emitBellNotification(): void { + writeToStdout(BEL); } export async function notifyViaTerminal( notificationsEnabled: boolean, content: RunEventNotificationContent, + method: TerminalNotificationMethod = TerminalNotificationMethod.Auto, ): Promise { if (!notificationsEnabled) { return false; } try { - emitOsc9Notification(sanitizeNotificationContent(content)); + if (method === TerminalNotificationMethod.Osc9) { + emitOsc9Notification(content); + } else if (method === TerminalNotificationMethod.Osc777) { + emitOsc777Notification(content); + } else if (method === TerminalNotificationMethod.Bell) { + emitBellNotification(); + } else { + // auto + const capabilityManager = TerminalCapabilityManager.getInstance(); + if (capabilityManager.isITerm2()) { + emitOsc9Notification(content); + } else if ( + capabilityManager.isAlacritty() || + capabilityManager.isAppleTerminal() || + capabilityManager.isVSCodeTerminal() || + capabilityManager.isWindowsTerminal() + ) { + emitBellNotification(); + } else { + emitOsc777Notification(content); + } + } + return true; } catch (error) { debugLogger.debug('Failed to emit terminal notification:', error); diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 98bc786410..c285846cf6 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -92,12 +92,20 @@ "type": "boolean" }, "enableNotifications": { - "title": "Enable Notifications", - "description": "Enable run-event notifications for action-required prompts and session completion.", - "markdownDescription": "Enable run-event notifications for action-required prompts and session completion.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `false`", + "title": "Enable Terminal Notifications", + "description": "Enable terminal run-event notifications for action-required prompts and session completion.", + "markdownDescription": "Enable terminal run-event notifications for action-required prompts and session completion.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `false`", "default": false, "type": "boolean" }, + "notificationMethod": { + "title": "Terminal Notification Method", + "description": "How to send terminal notifications.", + "markdownDescription": "How to send terminal notifications.\n\n- Category: `General`\n- Requires restart: `no`\n- Default: `auto`", + "default": "auto", + "type": "string", + "enum": ["auto", "osc9", "osc777", "bell"] + }, "checkpointing": { "title": "Checkpointing", "description": "Session checkpointing settings.",