diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts
index 7304d912..7b955cdf 100644
--- a/packages/cli/src/services/BuiltinCommandLoader.ts
+++ b/packages/cli/src/services/BuiltinCommandLoader.ts
@@ -25,7 +25,7 @@ import { initCommand } from '../ui/commands/initCommand.js';
import { mcpCommand } from '../ui/commands/mcpCommand.js';
import { memoryCommand } from '../ui/commands/memoryCommand.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 { statsCommand } from '../ui/commands/statsCommand.js';
import { themeCommand } from '../ui/commands/themeCommand.js';
@@ -70,6 +70,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
memoryCommand,
privacyCommand,
quitCommand,
+ quitConfirmCommand,
restoreCommand(this.config),
statsCommand,
themeCommand,
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 11536b75..7e65aff0 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -23,6 +23,8 @@ import { useAuthCommand } from './hooks/useAuthCommand.js';
import { useQwenAuth } from './hooks/useQwenAuth.js';
import { useFolderTrust } from './hooks/useFolderTrust.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 { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
@@ -40,6 +42,7 @@ import { QwenOAuthProgress } from './components/QwenOAuthProgress.js';
import { EditorSettingsDialog } from './components/EditorSettingsDialog.js';
import { FolderTrustDialog } from './components/FolderTrustDialog.js';
import { ShellConfirmationDialog } from './components/ShellConfirmationDialog.js';
+import { QuitConfirmationDialog } from './components/QuitConfirmationDialog.js';
import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js';
import { Colors } from './colors.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
@@ -103,6 +106,7 @@ import { SettingsDialog } from './components/SettingsDialog.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { appEvents, AppEvent } from '../utils/events.js';
import { isNarrowWidth } from './utils/isNarrowWidth.js';
+import { WelcomeBackDialog } from './components/WelcomeBackDialog.js';
const CTRL_EXIT_PROMPT_DURATION_MS = 1000;
// 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,
);
+ const { showQuitConfirmation, handleQuitConfirmationSelect } =
+ useQuitConfirmation();
+
const {
isAuthDialogOpen,
openAuthDialog,
@@ -550,6 +557,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
commandContext,
shellConfirmationRequest,
confirmationRequest,
+ quitConfirmationRequest,
} = useSlashCommandProcessor(
config,
settings,
@@ -568,6 +576,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
toggleVimEnabled,
setIsProcessing,
setGeminiMdFileCount,
+ showQuitConfirmation,
);
const buffer = useTextBuffer({
@@ -608,6 +617,15 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
() => cancelHandlerRef.current(),
);
+ // Welcome back functionality
+ const {
+ welcomeBackInfo,
+ showWelcomeBackDialog,
+ welcomeBackChoice,
+ handleWelcomeBackSelection,
+ handleWelcomeBackClose,
+ } = useWelcomeBack(config, submitQuery);
+
// Message queue for handling input during streaming
const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } =
useMessageQueue({
@@ -687,6 +705,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
handleSlashCommand('/quit');
} else {
setPressedOnce(true);
+ // Show quit confirmation dialog immediately on first Ctrl+C
+ handleSlashCommand('/quit-confirm');
timerRef.current = setTimeout(() => {
setPressedOnce(false);
timerRef.current = null;
@@ -816,7 +836,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
(streamingState === StreamingState.Idle ||
streamingState === StreamingState.Responding) &&
!initError &&
- !isProcessing;
+ !isProcessing &&
+ !showWelcomeBackDialog;
const handleClearScreen = useCallback(() => {
clearItems();
@@ -895,6 +916,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
!isThemeDialogOpen &&
!isEditorDialogOpen &&
!showPrivacyNotice &&
+ !showWelcomeBackDialog &&
+ welcomeBackChoice !== 'restart' &&
geminiClient?.isInitialized?.()
) {
submitQuery(initialPrompt);
@@ -908,6 +931,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
isThemeDialogOpen,
isEditorDialogOpen,
showPrivacyNotice,
+ showWelcomeBackDialog,
+ welcomeBackChoice,
geminiClient,
]);
@@ -1016,6 +1041,13 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
))}
)}
+ {showWelcomeBackDialog && welcomeBackInfo?.hasHistory && (
+
+ )}
{shouldShowIdePrompt && currentIDE ? (
{
/>
) : isFolderTrustDialogOpen ? (
+ ) : quitConfirmationRequest ? (
+ {
+ const result = handleQuitConfirmationSelect(choice);
+ if (result?.shouldQuit) {
+ quitConfirmationRequest.onConfirm(true, result.action);
+ } else {
+ quitConfirmationRequest.onConfirm(false);
+ }
+ }}
+ />
) : shellConfirmationRequest ? (
) : confirmationRequest ? (
diff --git a/packages/cli/src/ui/commands/quitCommand.ts b/packages/cli/src/ui/commands/quitCommand.ts
index 36f15c71..3e175d9c 100644
--- a/packages/cli/src/ui/commands/quitCommand.ts
+++ b/packages/cli/src/ui/commands/quitCommand.ts
@@ -7,6 +7,33 @@
import { formatDuration } from '../utils/formatters.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 = {
name: 'quit',
altNames: ['exit'],
diff --git a/packages/cli/src/ui/commands/types.ts b/packages/cli/src/ui/commands/types.ts
index bf0457be..a8c3ac75 100644
--- a/packages/cli/src/ui/commands/types.ts
+++ b/packages/cli/src/ui/commands/types.ts
@@ -88,6 +88,12 @@ export interface QuitActionReturn {
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
* being displayed to the user.
@@ -154,6 +160,7 @@ export type SlashCommandActionReturn =
| ToolActionReturn
| MessageActionReturn
| QuitActionReturn
+ | QuitConfirmationActionReturn
| OpenDialogActionReturn
| LoadHistoryActionReturn
| SubmitPromptActionReturn
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index 170a5ee6..627a04f7 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -81,6 +81,9 @@ export const HistoryItemDisplay: React.FC = ({
{item.type === 'model_stats' && }
{item.type === 'tool_stats' && }
{item.type === 'quit' && }
+ {item.type === 'quit_confirmation' && (
+
+ )}
{item.type === 'tool_group' && (
void;
+}
+
+export const QuitConfirmationDialog: React.FC = ({
+ onSelect,
+}) => {
+ useKeypress(
+ (key) => {
+ if (key.name === 'escape') {
+ onSelect(QuitChoice.QUIT);
+ }
+ },
+ { isActive: true },
+ );
+
+ const options: Array> = [
+ {
+ 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 (
+
+
+ What would you like to do before exiting?
+
+
+
+
+ );
+};
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index 014bba61..6eb06960 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -18,6 +18,7 @@ import {
ToolConfirmationOutcome,
} from '@qwen-code/qwen-code-core';
import { useSessionStats } from '../contexts/SessionContext.js';
+import { formatDuration } from '../utils/formatters.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import {
Message,
@@ -54,6 +55,7 @@ export const useSlashCommandProcessor = (
toggleVimEnabled: () => Promise,
setIsProcessing: (isProcessing: boolean) => void,
setGeminiMdFileCount: (count: number) => void,
+ _showQuitConfirmation: () => void,
) => {
const session = useSessionStats();
const [commands, setCommands] = useState([]);
@@ -74,6 +76,10 @@ export const useSlashCommandProcessor = (
prompt: React.ReactNode;
onConfirm: (confirmed: boolean) => void;
}>(null);
+ const [quitConfirmationRequest, setQuitConfirmationRequest] =
+ useState void;
+ }>(null);
const [sessionShellAllowlist, setSessionShellAllowlist] = useState(
new Set(),
@@ -141,6 +147,11 @@ export const useSlashCommandProcessor = (
type: 'quit',
duration: message.duration,
};
+ } else if (message.type === MessageType.QUIT_CONFIRMATION) {
+ historyItemContent = {
+ type: 'quit_confirmation',
+ duration: message.duration,
+ };
} else if (message.type === MessageType.COMPRESSION) {
historyItemContent = {
type: 'compression',
@@ -398,6 +409,70 @@ export const useSlashCommandProcessor = (
});
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':
setQuittingMessages(result.messages);
setTimeout(async () => {
@@ -567,5 +642,6 @@ export const useSlashCommandProcessor = (
commandContext,
shellConfirmationRequest,
confirmationRequest,
+ quitConfirmationRequest,
};
};
diff --git a/packages/cli/src/ui/hooks/useQuitConfirmation.ts b/packages/cli/src/ui/hooks/useQuitConfirmation.ts
new file mode 100644
index 00000000..39d680cb
--- /dev/null
+++ b/packages/cli/src/ui/hooks/useQuitConfirmation.ts
@@ -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,
+ };
+};
diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts
index c8568da8..9e7b603b 100644
--- a/packages/cli/src/ui/types.ts
+++ b/packages/cli/src/ui/types.ts
@@ -121,6 +121,11 @@ export type HistoryItemQuit = HistoryItemBase & {
duration: string;
};
+export type HistoryItemQuitConfirmation = HistoryItemBase & {
+ type: 'quit_confirmation';
+ duration: string;
+};
+
export type HistoryItemToolGroup = HistoryItemBase & {
type: 'tool_group';
tools: IndividualToolCallDisplay[];
@@ -154,6 +159,7 @@ export type HistoryItemWithoutId =
| HistoryItemModelStats
| HistoryItemToolStats
| HistoryItemQuit
+ | HistoryItemQuitConfirmation
| HistoryItemCompression;
export type HistoryItem = HistoryItemWithoutId & { id: number };
@@ -169,6 +175,7 @@ export enum MessageType {
MODEL_STATS = 'model_stats',
TOOL_STATS = 'tool_stats',
QUIT = 'quit',
+ QUIT_CONFIRMATION = 'quit_confirmation',
GEMINI = 'gemini',
COMPRESSION = 'compression',
}
@@ -219,6 +226,12 @@ export type Message =
duration: string;
content?: string;
}
+ | {
+ type: MessageType.QUIT_CONFIRMATION;
+ timestamp: Date;
+ duration: string;
+ content?: string;
+ }
| {
type: MessageType.COMPRESSION;
compression: CompressionProps;