mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Merge pull request #1194 from afarber/add-audio-notification-bell
feat: add terminal bell setting to enable/disable audio notifications
This commit is contained in:
@@ -194,6 +194,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
{ value: 'ru', label: 'Русский (Russian)' },
|
{ value: 'ru', label: 'Русский (Russian)' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
terminalBell: {
|
||||||
|
type: 'boolean',
|
||||||
|
label: 'Terminal Bell',
|
||||||
|
category: 'General',
|
||||||
|
requiresRestart: false,
|
||||||
|
default: true,
|
||||||
|
description:
|
||||||
|
'Play terminal bell sound when response completes or needs approval.',
|
||||||
|
showInDialog: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -942,6 +942,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
isFocused,
|
isFocused,
|
||||||
streamingState,
|
streamingState,
|
||||||
elapsedTime,
|
elapsedTime,
|
||||||
|
settings,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dialog close functionality
|
// Dialog close functionality
|
||||||
|
|||||||
@@ -15,6 +15,23 @@ import {
|
|||||||
LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS,
|
LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS,
|
||||||
useAttentionNotifications,
|
useAttentionNotifications,
|
||||||
} from './useAttentionNotifications.js';
|
} from './useAttentionNotifications.js';
|
||||||
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
|
|
||||||
|
const mockSettings: LoadedSettings = {
|
||||||
|
merged: {
|
||||||
|
general: {
|
||||||
|
terminalBell: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as LoadedSettings;
|
||||||
|
|
||||||
|
const mockSettingsDisabled: LoadedSettings = {
|
||||||
|
merged: {
|
||||||
|
general: {
|
||||||
|
terminalBell: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as LoadedSettings;
|
||||||
|
|
||||||
vi.mock('../../utils/attentionNotification.js', () => ({
|
vi.mock('../../utils/attentionNotification.js', () => ({
|
||||||
notifyTerminalAttention: vi.fn(),
|
notifyTerminalAttention: vi.fn(),
|
||||||
@@ -40,6 +57,7 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: true,
|
isFocused: true,
|
||||||
streamingState: StreamingState.Idle,
|
streamingState: StreamingState.Idle,
|
||||||
elapsedTime: 0,
|
elapsedTime: 0,
|
||||||
|
settings: mockSettings,
|
||||||
...props,
|
...props,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -53,11 +71,13 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: false,
|
isFocused: false,
|
||||||
streamingState: StreamingState.WaitingForConfirmation,
|
streamingState: StreamingState.WaitingForConfirmation,
|
||||||
elapsedTime: 0,
|
elapsedTime: 0,
|
||||||
|
settings: mockSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedNotify).toHaveBeenCalledWith(
|
expect(mockedNotify).toHaveBeenCalledWith(
|
||||||
AttentionNotificationReason.ToolApproval,
|
AttentionNotificationReason.ToolApproval,
|
||||||
|
{ enabled: true },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,6 +92,7 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: false,
|
isFocused: false,
|
||||||
streamingState: StreamingState.WaitingForConfirmation,
|
streamingState: StreamingState.WaitingForConfirmation,
|
||||||
elapsedTime: 0,
|
elapsedTime: 0,
|
||||||
|
settings: mockSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,6 +107,7 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: false,
|
isFocused: false,
|
||||||
streamingState: StreamingState.Responding,
|
streamingState: StreamingState.Responding,
|
||||||
elapsedTime: LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS + 5,
|
elapsedTime: LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS + 5,
|
||||||
|
settings: mockSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -94,11 +116,13 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: false,
|
isFocused: false,
|
||||||
streamingState: StreamingState.Idle,
|
streamingState: StreamingState.Idle,
|
||||||
elapsedTime: 0,
|
elapsedTime: 0,
|
||||||
|
settings: mockSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedNotify).toHaveBeenCalledWith(
|
expect(mockedNotify).toHaveBeenCalledWith(
|
||||||
AttentionNotificationReason.LongTaskComplete,
|
AttentionNotificationReason.LongTaskComplete,
|
||||||
|
{ enabled: true },
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -110,6 +134,7 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: true,
|
isFocused: true,
|
||||||
streamingState: StreamingState.Responding,
|
streamingState: StreamingState.Responding,
|
||||||
elapsedTime: LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS + 2,
|
elapsedTime: LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS + 2,
|
||||||
|
settings: mockSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -118,6 +143,7 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: true,
|
isFocused: true,
|
||||||
streamingState: StreamingState.Idle,
|
streamingState: StreamingState.Idle,
|
||||||
elapsedTime: 0,
|
elapsedTime: 0,
|
||||||
|
settings: mockSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,6 +161,7 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: false,
|
isFocused: false,
|
||||||
streamingState: StreamingState.Responding,
|
streamingState: StreamingState.Responding,
|
||||||
elapsedTime: 5,
|
elapsedTime: 5,
|
||||||
|
settings: mockSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -143,9 +170,30 @@ describe('useAttentionNotifications', () => {
|
|||||||
isFocused: false,
|
isFocused: false,
|
||||||
streamingState: StreamingState.Idle,
|
streamingState: StreamingState.Idle,
|
||||||
elapsedTime: 0,
|
elapsedTime: 0,
|
||||||
|
settings: mockSettings,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockedNotify).not.toHaveBeenCalled();
|
expect(mockedNotify).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not notify when terminalBell setting is disabled', () => {
|
||||||
|
const { rerender } = render({
|
||||||
|
settings: mockSettingsDisabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
rerender({
|
||||||
|
hookProps: {
|
||||||
|
isFocused: false,
|
||||||
|
streamingState: StreamingState.WaitingForConfirmation,
|
||||||
|
elapsedTime: 0,
|
||||||
|
settings: mockSettingsDisabled,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockedNotify).toHaveBeenCalledWith(
|
||||||
|
AttentionNotificationReason.ToolApproval,
|
||||||
|
{ enabled: false },
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
notifyTerminalAttention,
|
notifyTerminalAttention,
|
||||||
AttentionNotificationReason,
|
AttentionNotificationReason,
|
||||||
} from '../../utils/attentionNotification.js';
|
} from '../../utils/attentionNotification.js';
|
||||||
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
|
|
||||||
export const LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS = 20;
|
export const LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS = 20;
|
||||||
|
|
||||||
@@ -17,13 +18,16 @@ interface UseAttentionNotificationsOptions {
|
|||||||
isFocused: boolean;
|
isFocused: boolean;
|
||||||
streamingState: StreamingState;
|
streamingState: StreamingState;
|
||||||
elapsedTime: number;
|
elapsedTime: number;
|
||||||
|
settings: LoadedSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAttentionNotifications = ({
|
export const useAttentionNotifications = ({
|
||||||
isFocused,
|
isFocused,
|
||||||
streamingState,
|
streamingState,
|
||||||
elapsedTime,
|
elapsedTime,
|
||||||
|
settings,
|
||||||
}: UseAttentionNotificationsOptions) => {
|
}: UseAttentionNotificationsOptions) => {
|
||||||
|
const terminalBellEnabled = settings?.merged?.general?.terminalBell ?? true;
|
||||||
const awaitingNotificationSentRef = useRef(false);
|
const awaitingNotificationSentRef = useRef(false);
|
||||||
const respondingElapsedRef = useRef(0);
|
const respondingElapsedRef = useRef(0);
|
||||||
|
|
||||||
@@ -33,14 +37,16 @@ export const useAttentionNotifications = ({
|
|||||||
!isFocused &&
|
!isFocused &&
|
||||||
!awaitingNotificationSentRef.current
|
!awaitingNotificationSentRef.current
|
||||||
) {
|
) {
|
||||||
notifyTerminalAttention(AttentionNotificationReason.ToolApproval);
|
notifyTerminalAttention(AttentionNotificationReason.ToolApproval, {
|
||||||
|
enabled: terminalBellEnabled,
|
||||||
|
});
|
||||||
awaitingNotificationSentRef.current = true;
|
awaitingNotificationSentRef.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (streamingState !== StreamingState.WaitingForConfirmation || isFocused) {
|
if (streamingState !== StreamingState.WaitingForConfirmation || isFocused) {
|
||||||
awaitingNotificationSentRef.current = false;
|
awaitingNotificationSentRef.current = false;
|
||||||
}
|
}
|
||||||
}, [isFocused, streamingState]);
|
}, [isFocused, streamingState, terminalBellEnabled]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (streamingState === StreamingState.Responding) {
|
if (streamingState === StreamingState.Responding) {
|
||||||
@@ -53,11 +59,13 @@ export const useAttentionNotifications = ({
|
|||||||
respondingElapsedRef.current >=
|
respondingElapsedRef.current >=
|
||||||
LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS;
|
LONG_TASK_NOTIFICATION_THRESHOLD_SECONDS;
|
||||||
if (wasLongTask && !isFocused) {
|
if (wasLongTask && !isFocused) {
|
||||||
notifyTerminalAttention(AttentionNotificationReason.LongTaskComplete);
|
notifyTerminalAttention(AttentionNotificationReason.LongTaskComplete, {
|
||||||
|
enabled: terminalBellEnabled,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// Reset tracking for next task
|
// Reset tracking for next task
|
||||||
respondingElapsedRef.current = 0;
|
respondingElapsedRef.current = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}, [streamingState, elapsedTime, isFocused]);
|
}, [streamingState, elapsedTime, isFocused, terminalBellEnabled]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export enum AttentionNotificationReason {
|
|||||||
|
|
||||||
export interface TerminalNotificationOptions {
|
export interface TerminalNotificationOptions {
|
||||||
stream?: Pick<NodeJS.WriteStream, 'write' | 'isTTY'>;
|
stream?: Pick<NodeJS.WriteStream, 'write' | 'isTTY'>;
|
||||||
|
enabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TERMINAL_BELL = '\u0007';
|
const TERMINAL_BELL = '\u0007';
|
||||||
@@ -28,6 +29,11 @@ export function notifyTerminalAttention(
|
|||||||
_reason: AttentionNotificationReason,
|
_reason: AttentionNotificationReason,
|
||||||
options: TerminalNotificationOptions = {},
|
options: TerminalNotificationOptions = {},
|
||||||
): boolean {
|
): boolean {
|
||||||
|
// Check if terminal bell is enabled (default true for backwards compatibility)
|
||||||
|
if (options.enabled === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const stream = options.stream ?? process.stdout;
|
const stream = options.stream ?? process.stdout;
|
||||||
if (!stream?.write || stream.isTTY === false) {
|
if (!stream?.write || stream.isTTY === false) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
Reference in New Issue
Block a user