mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 08:47:44 +00:00
Merge branch 'main' of https://github.com/QwenLM/qwen-code into feat/vscode-ide-companion-borading
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: {
|
||||||
|
|||||||
@@ -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:',
|
||||||
|
|||||||
@@ -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:': '工具调用:',
|
||||||
|
|||||||
@@ -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', () => ({
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -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"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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: () => {},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user