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.",