mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
feat: add /quit-confirm
This commit is contained in:
@@ -25,7 +25,7 @@ import { initCommand } from '../ui/commands/initCommand.js';
|
|||||||
import { mcpCommand } from '../ui/commands/mcpCommand.js';
|
import { mcpCommand } from '../ui/commands/mcpCommand.js';
|
||||||
import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
||||||
import { privacyCommand } from '../ui/commands/privacyCommand.js';
|
import { privacyCommand } from '../ui/commands/privacyCommand.js';
|
||||||
import { quitCommand } from '../ui/commands/quitCommand.js';
|
import { quitCommand, quitConfirmCommand } from '../ui/commands/quitCommand.js';
|
||||||
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
||||||
import { statsCommand } from '../ui/commands/statsCommand.js';
|
import { statsCommand } from '../ui/commands/statsCommand.js';
|
||||||
import { themeCommand } from '../ui/commands/themeCommand.js';
|
import { themeCommand } from '../ui/commands/themeCommand.js';
|
||||||
@@ -70,6 +70,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
|||||||
memoryCommand,
|
memoryCommand,
|
||||||
privacyCommand,
|
privacyCommand,
|
||||||
quitCommand,
|
quitCommand,
|
||||||
|
quitConfirmCommand,
|
||||||
restoreCommand(this.config),
|
restoreCommand(this.config),
|
||||||
statsCommand,
|
statsCommand,
|
||||||
themeCommand,
|
themeCommand,
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import { useAuthCommand } from './hooks/useAuthCommand.js';
|
|||||||
import { useQwenAuth } from './hooks/useQwenAuth.js';
|
import { useQwenAuth } from './hooks/useQwenAuth.js';
|
||||||
import { useFolderTrust } from './hooks/useFolderTrust.js';
|
import { useFolderTrust } from './hooks/useFolderTrust.js';
|
||||||
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
import { useEditorSettings } from './hooks/useEditorSettings.js';
|
||||||
|
import { useQuitConfirmation } from './hooks/useQuitConfirmation.js';
|
||||||
|
import { useWelcomeBack } from './hooks/useWelcomeBack.js';
|
||||||
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
|
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
|
||||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||||
@@ -40,6 +42,7 @@ import { QwenOAuthProgress } from './components/QwenOAuthProgress.js';
|
|||||||
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
|
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
|
||||||
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
|
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
|
||||||
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
|
||||||
|
import { QuitConfirmationDialog } from './components/QuitConfirmationDialog.js';
|
||||||
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js';
|
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js';
|
||||||
import { Colors } from './colors.js';
|
import { Colors } from './colors.js';
|
||||||
import { loadHierarchicalGeminiMemory } from '../config/config.js';
|
import { loadHierarchicalGeminiMemory } from '../config/config.js';
|
||||||
@@ -103,6 +106,7 @@ import { SettingsDialog } from './components/SettingsDialog.js';
|
|||||||
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
||||||
import { appEvents, AppEvent } from '../utils/events.js';
|
import { appEvents, AppEvent } from '../utils/events.js';
|
||||||
import { isNarrowWidth } from './utils/isNarrowWidth.js';
|
import { isNarrowWidth } from './utils/isNarrowWidth.js';
|
||||||
|
import { WelcomeBackDialog } from './components/WelcomeBackDialog.js';
|
||||||
|
|
||||||
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
|
||||||
// Maximum number of queued messages to display in UI to prevent performance issues
|
// Maximum number of queued messages to display in UI to prevent performance issues
|
||||||
@@ -274,6 +278,9 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
setIsTrustedFolder,
|
setIsTrustedFolder,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { showQuitConfirmation, handleQuitConfirmationSelect } =
|
||||||
|
useQuitConfirmation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isAuthDialogOpen,
|
isAuthDialogOpen,
|
||||||
openAuthDialog,
|
openAuthDialog,
|
||||||
@@ -550,6 +557,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
commandContext,
|
commandContext,
|
||||||
shellConfirmationRequest,
|
shellConfirmationRequest,
|
||||||
confirmationRequest,
|
confirmationRequest,
|
||||||
|
quitConfirmationRequest,
|
||||||
} = useSlashCommandProcessor(
|
} = useSlashCommandProcessor(
|
||||||
config,
|
config,
|
||||||
settings,
|
settings,
|
||||||
@@ -568,6 +576,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
toggleVimEnabled,
|
toggleVimEnabled,
|
||||||
setIsProcessing,
|
setIsProcessing,
|
||||||
setGeminiMdFileCount,
|
setGeminiMdFileCount,
|
||||||
|
showQuitConfirmation,
|
||||||
);
|
);
|
||||||
|
|
||||||
const buffer = useTextBuffer({
|
const buffer = useTextBuffer({
|
||||||
@@ -608,6 +617,15 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
() => cancelHandlerRef.current(),
|
() => cancelHandlerRef.current(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Welcome back functionality
|
||||||
|
const {
|
||||||
|
welcomeBackInfo,
|
||||||
|
showWelcomeBackDialog,
|
||||||
|
welcomeBackChoice,
|
||||||
|
handleWelcomeBackSelection,
|
||||||
|
handleWelcomeBackClose,
|
||||||
|
} = useWelcomeBack(config, submitQuery);
|
||||||
|
|
||||||
// Message queue for handling input during streaming
|
// Message queue for handling input during streaming
|
||||||
const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } =
|
const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } =
|
||||||
useMessageQueue({
|
useMessageQueue({
|
||||||
@@ -687,6 +705,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
handleSlashCommand('/quit');
|
handleSlashCommand('/quit');
|
||||||
} else {
|
} else {
|
||||||
setPressedOnce(true);
|
setPressedOnce(true);
|
||||||
|
// Show quit confirmation dialog immediately on first Ctrl+C
|
||||||
|
handleSlashCommand('/quit-confirm');
|
||||||
timerRef.current = setTimeout(() => {
|
timerRef.current = setTimeout(() => {
|
||||||
setPressedOnce(false);
|
setPressedOnce(false);
|
||||||
timerRef.current = null;
|
timerRef.current = null;
|
||||||
@@ -816,7 +836,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
(streamingState === StreamingState.Idle ||
|
(streamingState === StreamingState.Idle ||
|
||||||
streamingState === StreamingState.Responding) &&
|
streamingState === StreamingState.Responding) &&
|
||||||
!initError &&
|
!initError &&
|
||||||
!isProcessing;
|
!isProcessing &&
|
||||||
|
!showWelcomeBackDialog;
|
||||||
|
|
||||||
const handleClearScreen = useCallback(() => {
|
const handleClearScreen = useCallback(() => {
|
||||||
clearItems();
|
clearItems();
|
||||||
@@ -895,6 +916,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
!isThemeDialogOpen &&
|
!isThemeDialogOpen &&
|
||||||
!isEditorDialogOpen &&
|
!isEditorDialogOpen &&
|
||||||
!showPrivacyNotice &&
|
!showPrivacyNotice &&
|
||||||
|
!showWelcomeBackDialog &&
|
||||||
|
welcomeBackChoice !== 'restart' &&
|
||||||
geminiClient?.isInitialized?.()
|
geminiClient?.isInitialized?.()
|
||||||
) {
|
) {
|
||||||
submitQuery(initialPrompt);
|
submitQuery(initialPrompt);
|
||||||
@@ -908,6 +931,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
isThemeDialogOpen,
|
isThemeDialogOpen,
|
||||||
isEditorDialogOpen,
|
isEditorDialogOpen,
|
||||||
showPrivacyNotice,
|
showPrivacyNotice,
|
||||||
|
showWelcomeBackDialog,
|
||||||
|
welcomeBackChoice,
|
||||||
geminiClient,
|
geminiClient,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1016,6 +1041,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{showWelcomeBackDialog && welcomeBackInfo?.hasHistory && (
|
||||||
|
<WelcomeBackDialog
|
||||||
|
welcomeBackInfo={welcomeBackInfo}
|
||||||
|
onSelect={handleWelcomeBackSelection}
|
||||||
|
onClose={handleWelcomeBackClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShowIdePrompt && currentIDE ? (
|
{shouldShowIdePrompt && currentIDE ? (
|
||||||
<IdeIntegrationNudge
|
<IdeIntegrationNudge
|
||||||
@@ -1024,6 +1056,17 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
|
|||||||
/>
|
/>
|
||||||
) : isFolderTrustDialogOpen ? (
|
) : isFolderTrustDialogOpen ? (
|
||||||
<FolderTrustDialog onSelect={handleFolderTrustSelect} />
|
<FolderTrustDialog onSelect={handleFolderTrustSelect} />
|
||||||
|
) : quitConfirmationRequest ? (
|
||||||
|
<QuitConfirmationDialog
|
||||||
|
onSelect={(choice) => {
|
||||||
|
const result = handleQuitConfirmationSelect(choice);
|
||||||
|
if (result?.shouldQuit) {
|
||||||
|
quitConfirmationRequest.onConfirm(true, result.action);
|
||||||
|
} else {
|
||||||
|
quitConfirmationRequest.onConfirm(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : shellConfirmationRequest ? (
|
) : shellConfirmationRequest ? (
|
||||||
<ShellConfirmationDialog request={shellConfirmationRequest} />
|
<ShellConfirmationDialog request={shellConfirmationRequest} />
|
||||||
) : confirmationRequest ? (
|
) : confirmationRequest ? (
|
||||||
|
|||||||
@@ -7,6 +7,33 @@
|
|||||||
import { formatDuration } from '../utils/formatters.js';
|
import { formatDuration } from '../utils/formatters.js';
|
||||||
import { CommandKind, type SlashCommand } from './types.js';
|
import { CommandKind, type SlashCommand } from './types.js';
|
||||||
|
|
||||||
|
export const quitConfirmCommand: SlashCommand = {
|
||||||
|
name: 'quit-confirm',
|
||||||
|
description: 'Show quit confirmation dialog',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
action: (context) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const { sessionStartTime } = context.session.stats;
|
||||||
|
const wallDuration = now - sessionStartTime.getTime();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'quit_confirmation',
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: 'user',
|
||||||
|
text: `/quit-confirm`,
|
||||||
|
id: now - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'quit_confirmation',
|
||||||
|
duration: formatDuration(wallDuration),
|
||||||
|
id: now,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const quitCommand: SlashCommand = {
|
export const quitCommand: SlashCommand = {
|
||||||
name: 'quit',
|
name: 'quit',
|
||||||
altNames: ['exit'],
|
altNames: ['exit'],
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ export interface QuitActionReturn {
|
|||||||
messages: HistoryItem[];
|
messages: HistoryItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The return type for a command action that requests quit confirmation. */
|
||||||
|
export interface QuitConfirmationActionReturn {
|
||||||
|
type: 'quit_confirmation';
|
||||||
|
messages: HistoryItem[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The return type for a command action that results in a simple message
|
* The return type for a command action that results in a simple message
|
||||||
* being displayed to the user.
|
* being displayed to the user.
|
||||||
@@ -154,6 +160,7 @@ export type SlashCommandActionReturn =
|
|||||||
| ToolActionReturn
|
| ToolActionReturn
|
||||||
| MessageActionReturn
|
| MessageActionReturn
|
||||||
| QuitActionReturn
|
| QuitActionReturn
|
||||||
|
| QuitConfirmationActionReturn
|
||||||
| OpenDialogActionReturn
|
| OpenDialogActionReturn
|
||||||
| LoadHistoryActionReturn
|
| LoadHistoryActionReturn
|
||||||
| SubmitPromptActionReturn
|
| SubmitPromptActionReturn
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
{item.type === 'model_stats' && <ModelStatsDisplay />}
|
{item.type === 'model_stats' && <ModelStatsDisplay />}
|
||||||
{item.type === 'tool_stats' && <ToolStatsDisplay />}
|
{item.type === 'tool_stats' && <ToolStatsDisplay />}
|
||||||
{item.type === 'quit' && <SessionSummaryDisplay duration={item.duration} />}
|
{item.type === 'quit' && <SessionSummaryDisplay duration={item.duration} />}
|
||||||
|
{item.type === 'quit_confirmation' && (
|
||||||
|
<SessionSummaryDisplay duration={item.duration} />
|
||||||
|
)}
|
||||||
{item.type === 'tool_group' && (
|
{item.type === 'tool_group' && (
|
||||||
<ToolGroupMessage
|
<ToolGroupMessage
|
||||||
toolCalls={item.tools}
|
toolCalls={item.tools}
|
||||||
|
|||||||
69
packages/cli/src/ui/components/QuitConfirmationDialog.tsx
Normal file
69
packages/cli/src/ui/components/QuitConfirmationDialog.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Box, Text } from 'ink';
|
||||||
|
import React from 'react';
|
||||||
|
import { Colors } from '../colors.js';
|
||||||
|
import {
|
||||||
|
RadioButtonSelect,
|
||||||
|
RadioSelectItem,
|
||||||
|
} from './shared/RadioButtonSelect.js';
|
||||||
|
import { useKeypress } from '../hooks/useKeypress.js';
|
||||||
|
|
||||||
|
export enum QuitChoice {
|
||||||
|
QUIT = 'quit',
|
||||||
|
SAVE_AND_QUIT = 'save_and_quit',
|
||||||
|
SUMMARY_AND_QUIT = 'summary_and_quit',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuitConfirmationDialogProps {
|
||||||
|
onSelect: (choice: QuitChoice) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuitConfirmationDialog: React.FC<QuitConfirmationDialogProps> = ({
|
||||||
|
onSelect,
|
||||||
|
}) => {
|
||||||
|
useKeypress(
|
||||||
|
(key) => {
|
||||||
|
if (key.name === 'escape') {
|
||||||
|
onSelect(QuitChoice.QUIT);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ isActive: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const options: Array<RadioSelectItem<QuitChoice>> = [
|
||||||
|
{
|
||||||
|
label: 'Quit immediately (/quit)',
|
||||||
|
value: QuitChoice.QUIT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Generate summary and quit (/chat summary)',
|
||||||
|
value: QuitChoice.SUMMARY_AND_QUIT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Save conversation and quit (/chat save)',
|
||||||
|
value: QuitChoice.SAVE_AND_QUIT,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
flexDirection="column"
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor={Colors.AccentYellow}
|
||||||
|
padding={1}
|
||||||
|
width="100%"
|
||||||
|
marginLeft={1}
|
||||||
|
>
|
||||||
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
|
<Text>What would you like to do before exiting?</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<RadioButtonSelect items={options} onSelect={onSelect} isFocused />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
ToolConfirmationOutcome,
|
ToolConfirmationOutcome,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} from '@qwen-code/qwen-code-core';
|
||||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||||
|
import { formatDuration } from '../utils/formatters.js';
|
||||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
@@ -54,6 +55,7 @@ export const useSlashCommandProcessor = (
|
|||||||
toggleVimEnabled: () => Promise<boolean>,
|
toggleVimEnabled: () => Promise<boolean>,
|
||||||
setIsProcessing: (isProcessing: boolean) => void,
|
setIsProcessing: (isProcessing: boolean) => void,
|
||||||
setGeminiMdFileCount: (count: number) => void,
|
setGeminiMdFileCount: (count: number) => void,
|
||||||
|
_showQuitConfirmation: () => void,
|
||||||
) => {
|
) => {
|
||||||
const session = useSessionStats();
|
const session = useSessionStats();
|
||||||
const [commands, setCommands] = useState<readonly SlashCommand[]>([]);
|
const [commands, setCommands] = useState<readonly SlashCommand[]>([]);
|
||||||
@@ -74,6 +76,10 @@ export const useSlashCommandProcessor = (
|
|||||||
prompt: React.ReactNode;
|
prompt: React.ReactNode;
|
||||||
onConfirm: (confirmed: boolean) => void;
|
onConfirm: (confirmed: boolean) => void;
|
||||||
}>(null);
|
}>(null);
|
||||||
|
const [quitConfirmationRequest, setQuitConfirmationRequest] =
|
||||||
|
useState<null | {
|
||||||
|
onConfirm: (shouldQuit: boolean, action?: string) => void;
|
||||||
|
}>(null);
|
||||||
|
|
||||||
const [sessionShellAllowlist, setSessionShellAllowlist] = useState(
|
const [sessionShellAllowlist, setSessionShellAllowlist] = useState(
|
||||||
new Set<string>(),
|
new Set<string>(),
|
||||||
@@ -141,6 +147,11 @@ export const useSlashCommandProcessor = (
|
|||||||
type: 'quit',
|
type: 'quit',
|
||||||
duration: message.duration,
|
duration: message.duration,
|
||||||
};
|
};
|
||||||
|
} else if (message.type === MessageType.QUIT_CONFIRMATION) {
|
||||||
|
historyItemContent = {
|
||||||
|
type: 'quit_confirmation',
|
||||||
|
duration: message.duration,
|
||||||
|
};
|
||||||
} else if (message.type === MessageType.COMPRESSION) {
|
} else if (message.type === MessageType.COMPRESSION) {
|
||||||
historyItemContent = {
|
historyItemContent = {
|
||||||
type: 'compression',
|
type: 'compression',
|
||||||
@@ -398,6 +409,70 @@ export const useSlashCommandProcessor = (
|
|||||||
});
|
});
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
}
|
}
|
||||||
|
case 'quit_confirmation':
|
||||||
|
// Show quit confirmation dialog instead of immediately quitting
|
||||||
|
setQuitConfirmationRequest({
|
||||||
|
onConfirm: (shouldQuit: boolean, action?: string) => {
|
||||||
|
setQuitConfirmationRequest(null);
|
||||||
|
if (shouldQuit) {
|
||||||
|
if (action === 'save_and_quit') {
|
||||||
|
// First save conversation with auto-generated tag, then quit
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[:.]/g, '-');
|
||||||
|
const autoSaveTag = `auto-save chat ${timestamp}`;
|
||||||
|
handleSlashCommand(`/chat save "${autoSaveTag}"`);
|
||||||
|
setTimeout(() => handleSlashCommand('/quit'), 100);
|
||||||
|
} else if (action === 'summary_and_quit') {
|
||||||
|
// Generate summary and then quit
|
||||||
|
handleSlashCommand('/chat summary')
|
||||||
|
.then(() => {
|
||||||
|
// Wait for summary to complete, then quit
|
||||||
|
handleSlashCommand('/quit');
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// If summary fails, still quit but show error
|
||||||
|
addItem(
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
text: `Failed to generate summary before quit: ${
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: String(error)
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
Date.now(),
|
||||||
|
);
|
||||||
|
handleSlashCommand('/quit');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Just quit immediately - trigger the actual quit action
|
||||||
|
const now = Date.now();
|
||||||
|
const { sessionStartTime } = session.stats;
|
||||||
|
const wallDuration = now - sessionStartTime.getTime();
|
||||||
|
|
||||||
|
setQuittingMessages([
|
||||||
|
{
|
||||||
|
type: 'user',
|
||||||
|
text: `/quit`,
|
||||||
|
id: now - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'quit',
|
||||||
|
duration: formatDuration(wallDuration),
|
||||||
|
id: now,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setTimeout(async () => {
|
||||||
|
await runExitCleanup();
|
||||||
|
process.exit(0);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return { type: 'handled' };
|
||||||
|
|
||||||
case 'quit':
|
case 'quit':
|
||||||
setQuittingMessages(result.messages);
|
setQuittingMessages(result.messages);
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
@@ -567,5 +642,6 @@ export const useSlashCommandProcessor = (
|
|||||||
commandContext,
|
commandContext,
|
||||||
shellConfirmationRequest,
|
shellConfirmationRequest,
|
||||||
confirmationRequest,
|
confirmationRequest,
|
||||||
|
quitConfirmationRequest,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
37
packages/cli/src/ui/hooks/useQuitConfirmation.ts
Normal file
37
packages/cli/src/ui/hooks/useQuitConfirmation.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { QuitChoice } from '../components/QuitConfirmationDialog.js';
|
||||||
|
|
||||||
|
export const useQuitConfirmation = () => {
|
||||||
|
const [isQuitConfirmationOpen, setIsQuitConfirmationOpen] = useState(false);
|
||||||
|
|
||||||
|
const showQuitConfirmation = useCallback(() => {
|
||||||
|
setIsQuitConfirmationOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleQuitConfirmationSelect = useCallback((choice: QuitChoice) => {
|
||||||
|
setIsQuitConfirmationOpen(false);
|
||||||
|
|
||||||
|
if (choice === QuitChoice.QUIT) {
|
||||||
|
return { shouldQuit: true, action: 'quit' };
|
||||||
|
} else if (choice === QuitChoice.SAVE_AND_QUIT) {
|
||||||
|
return { shouldQuit: true, action: 'save_and_quit' };
|
||||||
|
} else if (choice === QuitChoice.SUMMARY_AND_QUIT) {
|
||||||
|
return { shouldQuit: true, action: 'summary_and_quit' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to quit if unknown choice
|
||||||
|
return { shouldQuit: true, action: 'quit' };
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isQuitConfirmationOpen,
|
||||||
|
showQuitConfirmation,
|
||||||
|
handleQuitConfirmationSelect,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -121,6 +121,11 @@ export type HistoryItemQuit = HistoryItemBase & {
|
|||||||
duration: string;
|
duration: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type HistoryItemQuitConfirmation = HistoryItemBase & {
|
||||||
|
type: 'quit_confirmation';
|
||||||
|
duration: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type HistoryItemToolGroup = HistoryItemBase & {
|
export type HistoryItemToolGroup = HistoryItemBase & {
|
||||||
type: 'tool_group';
|
type: 'tool_group';
|
||||||
tools: IndividualToolCallDisplay[];
|
tools: IndividualToolCallDisplay[];
|
||||||
@@ -154,6 +159,7 @@ export type HistoryItemWithoutId =
|
|||||||
| HistoryItemModelStats
|
| HistoryItemModelStats
|
||||||
| HistoryItemToolStats
|
| HistoryItemToolStats
|
||||||
| HistoryItemQuit
|
| HistoryItemQuit
|
||||||
|
| HistoryItemQuitConfirmation
|
||||||
| HistoryItemCompression;
|
| HistoryItemCompression;
|
||||||
|
|
||||||
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
export type HistoryItem = HistoryItemWithoutId & { id: number };
|
||||||
@@ -169,6 +175,7 @@ export enum MessageType {
|
|||||||
MODEL_STATS = 'model_stats',
|
MODEL_STATS = 'model_stats',
|
||||||
TOOL_STATS = 'tool_stats',
|
TOOL_STATS = 'tool_stats',
|
||||||
QUIT = 'quit',
|
QUIT = 'quit',
|
||||||
|
QUIT_CONFIRMATION = 'quit_confirmation',
|
||||||
GEMINI = 'gemini',
|
GEMINI = 'gemini',
|
||||||
COMPRESSION = 'compression',
|
COMPRESSION = 'compression',
|
||||||
}
|
}
|
||||||
@@ -219,6 +226,12 @@ export type Message =
|
|||||||
duration: string;
|
duration: string;
|
||||||
content?: string;
|
content?: string;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: MessageType.QUIT_CONFIRMATION;
|
||||||
|
timestamp: Date;
|
||||||
|
duration: string;
|
||||||
|
content?: string;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: MessageType.COMPRESSION;
|
type: MessageType.COMPRESSION;
|
||||||
compression: CompressionProps;
|
compression: CompressionProps;
|
||||||
|
|||||||
Reference in New Issue
Block a user