Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/vscode-ide-companion-borading

This commit is contained in:
yiliang114
2025-12-13 17:05:40 +08:00
23 changed files with 152 additions and 98 deletions

View File

@@ -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: {

View File

@@ -867,6 +867,7 @@ export default {
// Exit Screen / Stats // Exit Screen / Stats
// ============================================================================ // ============================================================================
'Agent powering down. Goodbye!': 'Agent powering down. Goodbye!', 'Agent powering down. Goodbye!': 'Agent powering down. Goodbye!',
'To continue this session, run': 'To continue this session, run',
'Interaction Summary': 'Interaction Summary', 'Interaction Summary': 'Interaction Summary',
'Session ID:': 'Session ID:', 'Session ID:': 'Session ID:',
'Tool Calls:': 'Tool Calls:', 'Tool Calls:': 'Tool Calls:',

View File

@@ -820,6 +820,7 @@ export default {
// Exit Screen / Stats // Exit Screen / Stats
// ============================================================================ // ============================================================================
'Agent powering down. Goodbye!': 'Qwen Code 正在关闭,再见!', 'Agent powering down. Goodbye!': 'Qwen Code 正在关闭,再见!',
'To continue this session, run': '要继续此会话,请运行',
'Interaction Summary': '交互摘要', 'Interaction Summary': '交互摘要',
'Session ID:': '会话 ID', 'Session ID:': '会话 ID',
'Tool Calls:': '工具调用:', 'Tool Calls:': '工具调用:',

View File

@@ -58,7 +58,6 @@ vi.mock('../ui/commands/authCommand.js', () => ({ authCommand: {} }));
vi.mock('../ui/commands/bugCommand.js', () => ({ bugCommand: {} })); vi.mock('../ui/commands/bugCommand.js', () => ({ bugCommand: {} }));
vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: {} })); vi.mock('../ui/commands/clearCommand.js', () => ({ clearCommand: {} }));
vi.mock('../ui/commands/compressCommand.js', () => ({ compressCommand: {} })); vi.mock('../ui/commands/compressCommand.js', () => ({ compressCommand: {} }));
vi.mock('../ui/commands/corgiCommand.js', () => ({ corgiCommand: {} }));
vi.mock('../ui/commands/docsCommand.js', () => ({ docsCommand: {} })); vi.mock('../ui/commands/docsCommand.js', () => ({ docsCommand: {} }));
vi.mock('../ui/commands/editorCommand.js', () => ({ editorCommand: {} })); vi.mock('../ui/commands/editorCommand.js', () => ({ editorCommand: {} }));
vi.mock('../ui/commands/extensionsCommand.js', () => ({ vi.mock('../ui/commands/extensionsCommand.js', () => ({

View File

@@ -15,7 +15,6 @@ import { bugCommand } from '../ui/commands/bugCommand.js';
import { clearCommand } from '../ui/commands/clearCommand.js'; import { clearCommand } from '../ui/commands/clearCommand.js';
import { compressCommand } from '../ui/commands/compressCommand.js'; import { compressCommand } from '../ui/commands/compressCommand.js';
import { copyCommand } from '../ui/commands/copyCommand.js'; import { copyCommand } from '../ui/commands/copyCommand.js';
import { corgiCommand } from '../ui/commands/corgiCommand.js';
import { docsCommand } from '../ui/commands/docsCommand.js'; import { docsCommand } from '../ui/commands/docsCommand.js';
import { directoryCommand } from '../ui/commands/directoryCommand.js'; import { directoryCommand } from '../ui/commands/directoryCommand.js';
import { editorCommand } from '../ui/commands/editorCommand.js'; import { editorCommand } from '../ui/commands/editorCommand.js';
@@ -63,7 +62,6 @@ export class BuiltinCommandLoader implements ICommandLoader {
clearCommand, clearCommand,
compressCommand, compressCommand,
copyCommand, copyCommand,
corgiCommand,
docsCommand, docsCommand,
directoryCommand, directoryCommand,
editorCommand, editorCommand,

View File

@@ -56,7 +56,6 @@ export const createMockCommandContext = (
pendingItem: null, pendingItem: null,
setPendingItem: vi.fn(), setPendingItem: vi.fn(),
loadHistory: vi.fn(), loadHistory: vi.fn(),
toggleCorgiMode: vi.fn(),
toggleVimEnabled: vi.fn(), toggleVimEnabled: vi.fn(),
extensionsUpdateState: new Map(), extensionsUpdateState: new Map(),
setExtensionsUpdateState: vi.fn(), setExtensionsUpdateState: vi.fn(),

View File

@@ -136,7 +136,6 @@ export const AppContainer = (props: AppContainerProps) => {
const { settings, config, initializationResult } = props; const { settings, config, initializationResult } = props;
const historyManager = useHistory(); const historyManager = useHistory();
useMemoryMonitor(historyManager); useMemoryMonitor(historyManager);
const [corgiMode, setCorgiMode] = useState(false);
const [debugMessage, setDebugMessage] = useState<string>(''); const [debugMessage, setDebugMessage] = useState<string>('');
const [quittingMessages, setQuittingMessages] = useState< const [quittingMessages, setQuittingMessages] = useState<
HistoryItem[] | null HistoryItem[] | null
@@ -485,7 +484,6 @@ export const AppContainer = (props: AppContainerProps) => {
}, 100); }, 100);
}, },
setDebugMessage, setDebugMessage,
toggleCorgiMode: () => setCorgiMode((prev) => !prev),
dispatchExtensionStateUpdate, dispatchExtensionStateUpdate,
addConfirmUpdateExtensionRequest, addConfirmUpdateExtensionRequest,
openSubagentCreateDialog, openSubagentCreateDialog,
@@ -498,7 +496,6 @@ export const AppContainer = (props: AppContainerProps) => {
openSettingsDialog, openSettingsDialog,
openModelDialog, openModelDialog,
setDebugMessage, setDebugMessage,
setCorgiMode,
dispatchExtensionStateUpdate, dispatchExtensionStateUpdate,
openPermissionsDialog, openPermissionsDialog,
openApprovalModeDialog, openApprovalModeDialog,
@@ -945,6 +942,7 @@ export const AppContainer = (props: AppContainerProps) => {
isFocused, isFocused,
streamingState, streamingState,
elapsedTime, elapsedTime,
settings,
}); });
// Dialog close functionality // Dialog close functionality
@@ -1218,7 +1216,6 @@ export const AppContainer = (props: AppContainerProps) => {
qwenAuthState, qwenAuthState,
editorError, editorError,
isEditorDialogOpen, isEditorDialogOpen,
corgiMode,
debugMessage, debugMessage,
quittingMessages, quittingMessages,
isSettingsDialogOpen, isSettingsDialogOpen,
@@ -1309,7 +1306,6 @@ export const AppContainer = (props: AppContainerProps) => {
qwenAuthState, qwenAuthState,
editorError, editorError,
isEditorDialogOpen, isEditorDialogOpen,
corgiMode,
debugMessage, debugMessage,
quittingMessages, quittingMessages,
isSettingsDialogOpen, isSettingsDialogOpen,

View File

@@ -1,34 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { corgiCommand } from './corgiCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
describe('corgiCommand', () => {
let mockContext: CommandContext;
beforeEach(() => {
mockContext = createMockCommandContext();
vi.spyOn(mockContext.ui, 'toggleCorgiMode');
});
it('should call the toggleCorgiMode function on the UI context', async () => {
if (!corgiCommand.action) {
throw new Error('The corgi command must have an action.');
}
await corgiCommand.action(mockContext, '');
expect(mockContext.ui.toggleCorgiMode).toHaveBeenCalledTimes(1);
});
it('should have the correct name and description', () => {
expect(corgiCommand.name).toBe('corgi');
expect(corgiCommand.description).toBe('Toggles corgi mode.');
});
});

View File

@@ -1,17 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { CommandKind, type SlashCommand } from './types.js';
export const corgiCommand: SlashCommand = {
name: 'corgi',
description: 'Toggles corgi mode.',
hidden: true,
kind: CommandKind.BUILT_IN,
action: (context, _args) => {
context.ui.toggleCorgiMode();
},
};

View File

@@ -64,8 +64,6 @@ export interface CommandContext {
* @param history The array of history items to load. * @param history The array of history items to load.
*/ */
loadHistory: UseHistoryManagerReturn['loadHistory']; loadHistory: UseHistoryManagerReturn['loadHistory'];
/** Toggles a special display mode. */
toggleCorgiMode: () => void;
toggleVimEnabled: () => Promise<boolean>; toggleVimEnabled: () => Promise<boolean>;
setGeminiMdFileCount: (count: number) => void; setGeminiMdFileCount: (count: number) => void;
reloadCommands: () => void; reloadCommands: () => void;

View File

@@ -120,7 +120,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
}, },
branchName: 'main', branchName: 'main',
debugMessage: '', debugMessage: '',
corgiMode: false,
errorCount: 0, errorCount: 0,
nightly: false, nightly: false,
isTrustedFolder: true, isTrustedFolder: true,
@@ -183,6 +182,7 @@ describe('Composer', () => {
const { lastFrame } = renderComposer(uiState, settings); const { lastFrame } = renderComposer(uiState, settings);
// Smoke check that the Footer renders when enabled.
expect(lastFrame()).toContain('Footer'); expect(lastFrame()).toContain('Footer');
}); });
@@ -200,7 +200,6 @@ describe('Composer', () => {
it('passes correct props to Footer including vim mode when enabled', async () => { it('passes correct props to Footer including vim mode when enabled', async () => {
const uiState = createMockUIState({ const uiState = createMockUIState({
branchName: 'feature-branch', branchName: 'feature-branch',
corgiMode: true,
errorCount: 2, errorCount: 2,
sessionStats: { sessionStats: {
sessionId: 'test-session', sessionId: 'test-session',

View File

@@ -33,7 +33,6 @@ export const Footer: React.FC = () => {
debugMode, debugMode,
branchName, branchName,
debugMessage, debugMessage,
corgiMode,
errorCount, errorCount,
showErrorDetails, showErrorDetails,
promptTokenCount, promptTokenCount,
@@ -45,7 +44,6 @@ export const Footer: React.FC = () => {
debugMode: config.getDebugMode(), debugMode: config.getDebugMode(),
branchName: uiState.branchName, branchName: uiState.branchName,
debugMessage: uiState.debugMessage, debugMessage: uiState.debugMessage,
corgiMode: uiState.corgiMode,
errorCount: uiState.errorCount, errorCount: uiState.errorCount,
showErrorDetails: uiState.showErrorDetails, showErrorDetails: uiState.showErrorDetails,
promptTokenCount: uiState.sessionStats.lastPromptTokenCount, promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
@@ -153,16 +151,6 @@ export const Footer: React.FC = () => {
{showMemoryUsage && <MemoryUsageDisplay />} {showMemoryUsage && <MemoryUsageDisplay />}
</Box> </Box>
<Box alignItems="center" paddingLeft={2}> <Box alignItems="center" paddingLeft={2}>
{corgiMode && (
<Text>
<Text color={theme.ui.symbol}>| </Text>
<Text color={theme.status.error}></Text>
<Text color={theme.text.primary}>(´</Text>
<Text color={theme.status.error}></Text>
<Text color={theme.text.primary}>`)</Text>
<Text color={theme.status.error}>▼ </Text>
</Text>
)}
{!showErrorDetails && errorCount > 0 && ( {!showErrorDetails && errorCount > 0 && (
<Box> <Box>
<Text color={theme.ui.symbol}>| </Text> <Text color={theme.ui.symbol}>| </Text>

View File

@@ -20,16 +20,21 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats); const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
const renderWithMockedStats = (metrics: SessionMetrics) => { const renderWithMockedStats = (
metrics: SessionMetrics,
sessionId: string = 'test-session-id-12345',
promptCount: number = 5,
) => {
useSessionStatsMock.mockReturnValue({ useSessionStatsMock.mockReturnValue({
stats: { stats: {
sessionId,
sessionStartTime: new Date(), sessionStartTime: new Date(),
metrics, metrics,
lastPromptTokenCount: 0, lastPromptTokenCount: 0,
promptCount: 5, promptCount,
}, },
getPromptCount: () => 5, getPromptCount: () => promptCount,
startNewPrompt: vi.fn(), startNewPrompt: vi.fn(),
}); });
@@ -70,6 +75,38 @@ describe('<SessionSummaryDisplay />', () => {
const output = lastFrame(); const output = lastFrame();
expect(output).toContain('Agent powering down. Goodbye!'); expect(output).toContain('Agent powering down. Goodbye!');
expect(output).toContain('To continue this session, run');
expect(output).toContain('qwen --resume test-session-id-12345');
expect(output).toMatchSnapshot(); expect(output).toMatchSnapshot();
}); });
it('does not show resume message when there are no messages', () => {
const metrics: SessionMetrics = {
models: {},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: { accept: 0, reject: 0, modify: 0 },
byName: {},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
};
// Pass promptCount = 0 to simulate no messages
const { lastFrame } = renderWithMockedStats(
metrics,
'test-session-id-12345',
0,
);
const output = lastFrame();
expect(output).toContain('Agent powering down. Goodbye!');
expect(output).not.toContain('To continue this session, run');
expect(output).not.toContain('qwen --resume');
});
}); });

View File

@@ -5,7 +5,10 @@
*/ */
import type React from 'react'; import type React from 'react';
import { Box, Text } from 'ink';
import { StatsDisplay } from './StatsDisplay.js'; import { StatsDisplay } from './StatsDisplay.js';
import { useSessionStats } from '../contexts/SessionContext.js';
import { theme } from '../semantic-colors.js';
import { t } from '../../i18n/index.js'; import { t } from '../../i18n/index.js';
interface SessionSummaryDisplayProps { interface SessionSummaryDisplayProps {
@@ -14,9 +17,28 @@ interface SessionSummaryDisplayProps {
export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({ export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({
duration, duration,
}) => ( }) => {
<StatsDisplay const { stats } = useSessionStats();
title={t('Agent powering down. Goodbye!')}
duration={duration} // Only show the resume message if there were messages in the session
/> const hasMessages = stats.promptCount > 0;
);
return (
<>
<StatsDisplay
title={t('Agent powering down. Goodbye!')}
duration={duration}
/>
{hasMessages && (
<Box marginTop={1}>
<Text color={theme.text.secondary}>
{t('To continue this session, run')}{' '}
<Text color={theme.text.accent}>
qwen --resume {stats.sessionId}
</Text>
</Text>
</Box>
)}
</>
);
};

View File

@@ -6,7 +6,7 @@ exports[`<SessionSummaryDisplay /> > renders the summary display with a title 1`
│ Agent powering down. Goodbye! │ │ Agent powering down. Goodbye! │
│ │ │ │
│ Interaction Summary │ │ Interaction Summary │
│ Session ID: │ Session ID: test-session-id-12345
│ Tool Calls: 0 ( ✓ 0 x 0 ) │ │ Tool Calls: 0 ( ✓ 0 x 0 ) │
│ Success Rate: 0.0% │ │ Success Rate: 0.0% │
│ Code Changes: +42 -15 │ │ Code Changes: +42 -15 │
@@ -26,5 +26,7 @@ exports[`<SessionSummaryDisplay /> > renders the summary display with a title 1`
│ │ │ │
│ » Tip: For a full token breakdown, run \`/stats model\`. │ │ » Tip: For a full token breakdown, run \`/stats model\`. │
│ │ │ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
To continue this session, run qwen --resume test-session-id-12345"
`; `;

View File

@@ -54,7 +54,6 @@ export interface UIState {
qwenAuthState: QwenAuthState; qwenAuthState: QwenAuthState;
editorError: string | null; editorError: string | null;
isEditorDialogOpen: boolean; isEditorDialogOpen: boolean;
corgiMode: boolean;
debugMessage: string; debugMessage: string;
quittingMessages: HistoryItem[] | null; quittingMessages: HistoryItem[] | null;
isSettingsDialogOpen: boolean; isSettingsDialogOpen: boolean;

View File

@@ -153,7 +153,6 @@ describe('useSlashCommandProcessor', () => {
openModelDialog: mockOpenModelDialog, openModelDialog: mockOpenModelDialog,
quit: mockSetQuittingMessages, quit: mockSetQuittingMessages,
setDebugMessage: vi.fn(), setDebugMessage: vi.fn(),
toggleCorgiMode: vi.fn(),
}, },
), ),
); );
@@ -909,7 +908,6 @@ describe('useSlashCommandProcessor', () => {
vi.fn(), // openThemeDialog vi.fn(), // openThemeDialog
mockOpenAuthDialog, mockOpenAuthDialog,
vi.fn(), // openEditorDialog vi.fn(), // openEditorDialog
vi.fn(), // toggleCorgiMode
mockSetQuittingMessages, mockSetQuittingMessages,
vi.fn(), // openSettingsDialog vi.fn(), // openSettingsDialog
vi.fn(), // openModelSelectionDialog vi.fn(), // openModelSelectionDialog

View File

@@ -68,7 +68,6 @@ interface SlashCommandProcessorActions {
openApprovalModeDialog: () => void; openApprovalModeDialog: () => void;
quit: (messages: HistoryItem[]) => void; quit: (messages: HistoryItem[]) => void;
setDebugMessage: (message: string) => void; setDebugMessage: (message: string) => void;
toggleCorgiMode: () => void;
dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void; dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void; addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void;
openSubagentCreateDialog: () => void; openSubagentCreateDialog: () => void;
@@ -206,7 +205,6 @@ export const useSlashCommandProcessor = (
setDebugMessage: actions.setDebugMessage, setDebugMessage: actions.setDebugMessage,
pendingItem, pendingItem,
setPendingItem, setPendingItem,
toggleCorgiMode: actions.toggleCorgiMode,
toggleVimEnabled, toggleVimEnabled,
setGeminiMdFileCount, setGeminiMdFileCount,
reloadCommands, reloadCommands,

View File

@@ -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 },
);
});
}); });

View File

@@ -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]);
}; };

View File

@@ -20,7 +20,6 @@ export function createNonInteractiveUI(): CommandContext['ui'] {
loadHistory: (_newHistory) => {}, loadHistory: (_newHistory) => {},
pendingItem: null, pendingItem: null,
setPendingItem: (_item) => {}, setPendingItem: (_item) => {},
toggleCorgiMode: () => {},
toggleVimEnabled: async () => false, toggleVimEnabled: async () => false,
setGeminiMdFileCount: (_count) => {}, setGeminiMdFileCount: (_count) => {},
reloadCommands: () => {}, reloadCommands: () => {},

View File

@@ -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;

View File

@@ -38,7 +38,6 @@
"src/ui/commands/clearCommand.test.ts", "src/ui/commands/clearCommand.test.ts",
"src/ui/commands/compressCommand.test.ts", "src/ui/commands/compressCommand.test.ts",
"src/ui/commands/copyCommand.test.ts", "src/ui/commands/copyCommand.test.ts",
"src/ui/commands/corgiCommand.test.ts",
"src/ui/commands/docsCommand.test.ts", "src/ui/commands/docsCommand.test.ts",
"src/ui/commands/editorCommand.test.ts", "src/ui/commands/editorCommand.test.ts",
"src/ui/commands/extensionsCommand.test.ts", "src/ui/commands/extensionsCommand.test.ts",