Skip to content

Commit 01154db

Browse files
committed
Merge Commit '94c3eec': feat(sessions): add /resume slash command to open the session browser (google-gemini#13621)
2 parents 14ec6b0 + 94c3eec commit 01154db

16 files changed

+164
-47
lines changed

packages/cli/src/services/BuiltinCommandLoader.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ vi.mock('../ui/commands/modelCommand.js', () => ({
7575
}));
7676
vi.mock('../ui/commands/privacyCommand.js', () => ({ privacyCommand: {} }));
7777
vi.mock('../ui/commands/quitCommand.js', () => ({ quitCommand: {} }));
78+
vi.mock('../ui/commands/resumeCommand.js', () => ({ resumeCommand: {} }));
7879
vi.mock('../ui/commands/statsCommand.js', () => ({ statsCommand: {} }));
7980
vi.mock('../ui/commands/themeCommand.js', () => ({ themeCommand: {} }));
8081
vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} }));

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { policiesCommand } from '../ui/commands/policiesCommand.js';
3232
import { profileCommand } from '../ui/commands/profileCommand.js';
3333
import { quitCommand } from '../ui/commands/quitCommand.js';
3434
import { restoreCommand } from '../ui/commands/restoreCommand.js';
35+
import { resumeCommand } from '../ui/commands/resumeCommand.js';
3536
import { statsCommand } from '../ui/commands/statsCommand.js';
3637
import { themeCommand } from '../ui/commands/themeCommand.js';
3738
import { toolsCommand } from '../ui/commands/toolsCommand.js';
@@ -90,6 +91,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
9091
...(isDevelopment ? [profileCommand] : []),
9192
quitCommand,
9293
restoreCommand(this.config),
94+
resumeCommand,
9395
statsCommand,
9496
themeCommand,
9597
toolsCommand,

packages/cli/src/test-utils/render.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ const mockUIActions: UIActions = {
148148
closeSettingsDialog: vi.fn(),
149149
closeModelDialog: vi.fn(),
150150
openPermissionsDialog: vi.fn(),
151+
openSessionBrowser: vi.fn(),
152+
closeSessionBrowser: vi.fn(),
153+
handleResumeSession: vi.fn(),
154+
handleDeleteSession: vi.fn(),
151155
closePermissionsDialog: vi.fn(),
152156
setShellModeActive: vi.fn(),
153157
vimHandleInput: vi.fn(),

packages/cli/src/ui/AppContainer.tsx

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import { type UpdateObject } from './utils/updateCheck.js';
102102
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
103103
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
104104
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
105+
import type { SessionInfo } from '../utils/sessionUtils.js';
105106
import { useMessageQueue } from './hooks/useMessageQueue.js';
106107
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
107108
import { useSessionStats } from './contexts/SessionContext.js';
@@ -121,9 +122,10 @@ import {
121122
useExtensionUpdates,
122123
} from './hooks/useExtensionUpdates.js';
123124
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
124-
import { useSessionResume } from './hooks/useSessionResume.js';
125125
import { type ExtensionManager } from '../config/extension-manager.js';
126126
import { requestConsentInteractive } from '../config/extensions/consent.js';
127+
import { useSessionBrowser } from './hooks/useSessionBrowser.js';
128+
import { useSessionResume } from './hooks/useSessionResume.js';
127129
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
128130
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
129131
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
@@ -449,7 +451,7 @@ export const AppContainer = (props: AppContainerProps) => {
449451
// Session browser and resume functionality
450452
const isGeminiClientInitialized = config.getGeminiClient()?.isInitialized();
451453

452-
useSessionResume({
454+
const { loadHistoryForResume } = useSessionResume({
453455
config,
454456
historyManager,
455457
refreshStatic,
@@ -458,6 +460,20 @@ export const AppContainer = (props: AppContainerProps) => {
458460
resumedSessionData,
459461
isAuthenticating,
460462
});
463+
const {
464+
isSessionBrowserOpen,
465+
openSessionBrowser,
466+
closeSessionBrowser,
467+
handleResumeSession,
468+
handleDeleteSession: handleDeleteSessionSync,
469+
} = useSessionBrowser(config, loadHistoryForResume);
470+
// Wrap handleDeleteSession to return a Promise for UIActions interface
471+
const handleDeleteSession = useCallback(
472+
async (session: SessionInfo): Promise<void> => {
473+
handleDeleteSessionSync(session);
474+
},
475+
[handleDeleteSessionSync],
476+
);
461477

462478
// Create handleAuthSelect wrapper for backward compatibility
463479
const handleAuthSelect = useCallback(
@@ -593,6 +609,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
593609
openLanguageDialog,
594610
openPrivacyNotice: () => setShowPrivacyNotice(true),
595611
openSettingsDialog,
612+
openSessionBrowser,
596613
openModelDialog,
597614
openPermissionsDialog,
598615
quit: (messages: HistoryItem[]) => {
@@ -614,6 +631,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
614631
openEditorDialog,
615632
openLanguageDialog,
616633
openSettingsDialog,
634+
openSessionBrowser,
617635
openModelDialog,
618636
setQuittingMessages,
619637
setDebugMessage,
@@ -1356,6 +1374,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
13561374
showPrivacyNotice ||
13571375
showIdeRestartPrompt ||
13581376
!!proQuotaRequest ||
1377+
isSessionBrowserOpen ||
13591378
isAuthDialogOpen ||
13601379
authState === AuthState.AwaitingApiKeyInput;
13611380

@@ -1586,7 +1605,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
15861605
let reason = 'general';
15871606

15881607
if (shouldShowIdePrompt) {
1589-
message = 'IDE integration prompt is displayed. Please respond to connect your editor to Auditaria CLI in the terminal.';
1608+
message =
1609+
'IDE integration prompt is displayed. Please respond to connect your editor to Auditaria CLI in the terminal.';
15901610
reason = 'ide_integration';
15911611
} else if (isAuthenticating || isAuthDialogOpen) {
15921612
const authMessage = isAuthenticating
@@ -1595,37 +1615,47 @@ Logging in with Google... Restarting Gemini CLI to continue.
15951615
message = authMessage;
15961616
reason = 'authentication';
15971617
} else if (isThemeDialogOpen) {
1598-
message = 'Theme selection is open. Please choose a theme in the CLI terminal.';
1618+
message =
1619+
'Theme selection is open. Please choose a theme in the CLI terminal.';
15991620
reason = 'theme_selection';
16001621
} else if (isEditorDialogOpen) {
1601-
message = 'Editor settings are open. Please configure your editor in the CLI terminal.';
1622+
message =
1623+
'Editor settings are open. Please configure your editor in the CLI terminal.';
16021624
reason = 'editor_settings';
16031625
} else if (isLanguageDialogOpen) {
1604-
message = 'Language selection is open. Please choose a language in the CLI terminal.';
1626+
message =
1627+
'Language selection is open. Please choose a language in the CLI terminal.';
16051628
reason = 'language_selection';
16061629
} else if (isSettingsDialogOpen) {
1607-
message = 'Settings dialog is open. Please configure settings in the CLI terminal.';
1630+
message =
1631+
'Settings dialog is open. Please configure settings in the CLI terminal.';
16081632
reason = 'settings';
16091633
} else if (isModelDialogOpen) {
1610-
message = 'Model selection is open. Please choose a model in the CLI terminal.';
1634+
message =
1635+
'Model selection is open. Please choose a model in the CLI terminal.';
16111636
reason = 'model_selection';
16121637
} else if (isFolderTrustDialogOpen) {
1613-
message = 'Folder trust dialog is open. Please respond in the CLI terminal.';
1638+
message =
1639+
'Folder trust dialog is open. Please respond in the CLI terminal.';
16141640
reason = 'folder_trust';
16151641
} else if (showPrivacyNotice) {
1616-
message = 'Privacy notice is displayed. Please review in the CLI terminal.';
1642+
message =
1643+
'Privacy notice is displayed. Please review in the CLI terminal.';
16171644
reason = 'privacy_notice';
16181645
} else if (proQuotaRequest) {
1619-
message = 'Quota exceeded dialog is open. Please choose an option in the CLI terminal.';
1646+
message =
1647+
'Quota exceeded dialog is open. Please choose an option in the CLI terminal.';
16201648
reason = 'quota_exceeded';
16211649
} else if (shellConfirmationRequest) {
1622-
message = 'Shell command confirmation required. Please respond in the CLI terminal.';
1650+
message =
1651+
'Shell command confirmation required. Please respond in the CLI terminal.';
16231652
reason = 'shell_confirmation';
16241653
} else if (confirmationRequest) {
16251654
message = 'Confirmation required. Please respond in the CLI terminal.';
16261655
reason = 'confirmation';
16271656
} else if (loopDetectionConfirmationRequest) {
1628-
message = 'Loop detection confirmation required. Please choose whether to keep or disable loop detection in the CLI terminal.';
1657+
message =
1658+
'Loop detection confirmation required. Please choose whether to keep or disable loop detection in the CLI terminal.';
16291659
reason = 'loop_detection';
16301660
}
16311661

@@ -1825,6 +1855,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
18251855
debugMessage,
18261856
quittingMessages,
18271857
isSettingsDialogOpen,
1858+
isSessionBrowserOpen,
18281859
isModelDialogOpen,
18291860
isPermissionsDialogOpen,
18301861
permissionsDialogProps,
@@ -1917,6 +1948,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
19171948
debugMessage,
19181949
quittingMessages,
19191950
isSettingsDialogOpen,
1951+
isSessionBrowserOpen,
19201952
isModelDialogOpen,
19211953
isPermissionsDialogOpen,
19221954
permissionsDialogProps,
@@ -2027,6 +2059,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
20272059
handleFinalSubmit,
20282060
handleClearScreen,
20292061
handleProQuotaChoice,
2062+
openSessionBrowser,
2063+
closeSessionBrowser,
2064+
handleResumeSession,
2065+
handleDeleteSession,
20302066
setQueueErrorMessage,
20312067
popAllMessages,
20322068
handleApiKeySubmit,
@@ -2059,6 +2095,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
20592095
handleFinalSubmit,
20602096
handleClearScreen,
20612097
handleProQuotaChoice,
2098+
openSessionBrowser,
2099+
closeSessionBrowser,
2100+
handleResumeSession,
2101+
handleDeleteSession,
20622102
setQueueErrorMessage,
20632103
popAllMessages,
20642104
handleApiKeySubmit,
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type {
8+
OpenDialogActionReturn,
9+
CommandContext,
10+
SlashCommand,
11+
} from './types.js';
12+
import { CommandKind } from './types.js';
13+
14+
export const resumeCommand: SlashCommand = {
15+
name: 'resume',
16+
description: 'Browse and resume auto-saved conversations',
17+
kind: CommandKind.BUILT_IN,
18+
action: async (
19+
_context: CommandContext,
20+
_args: string,
21+
): Promise<OpenDialogActionReturn> => ({
22+
type: 'dialog',
23+
dialog: 'sessionBrowser',
24+
}),
25+
};

packages/cli/src/ui/commands/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export interface OpenDialogActionReturn {
145145
| 'privacy'
146146
| 'language'
147147
| 'settings'
148+
| 'sessionBrowser'
148149
| 'model'
149150
| 'permissions';
150151
}

packages/cli/src/ui/components/DialogManager.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { PrivacyNotice } from '../privacy/PrivacyNotice.js';
2121
import { ProQuotaDialog } from './ProQuotaDialog.js';
2222
import { runExitCleanup } from '../../utils/cleanup.js';
2323
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
24+
import { SessionBrowser } from './SessionBrowser.js';
2425
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
2526
import { ModelDialog } from './ModelDialog.js';
2627
import { theme } from '../semantic-colors.js';
@@ -226,6 +227,16 @@ export const DialogManager = ({
226227
/>
227228
);
228229
}
230+
if (uiState.isSessionBrowserOpen) {
231+
return (
232+
<SessionBrowser
233+
config={config}
234+
onResumeSession={uiActions.handleResumeSession}
235+
onDeleteSession={uiActions.handleDeleteSession}
236+
onExit={uiActions.closeSessionBrowser}
237+
/>
238+
);
239+
}
229240

230241
if (uiState.isPermissionsDialogOpen) {
231242
return (

packages/cli/src/ui/components/InputPrompt.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ const mockSlashCommands: SlashCommand[] = [
8686
},
8787
],
8888
},
89+
{
90+
name: 'resume',
91+
description: 'Browse and resume sessions',
92+
kind: CommandKind.BUILT_IN,
93+
action: vi.fn(),
94+
},
8995
];
9096

9197
describe('InputPrompt', () => {

packages/cli/src/ui/components/SessionBrowser.test.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ vi.mock('./SessionBrowser.js', async (importOriginal) => {
5757
moveSelection,
5858
cycleSortOrder,
5959
props.onResumeSession,
60-
props.onDeleteSession,
60+
props.onDeleteSession ??
61+
(async () => {
62+
// no-op delete handler for tests that don't care about deletion
63+
}),
6164
props.onExit,
6265
);
6366

@@ -146,12 +149,14 @@ describe('SessionBrowser component', () => {
146149
it('shows empty state when no sessions exist', () => {
147150
const config = createMockConfig();
148151
const onResumeSession = vi.fn();
152+
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
149153
const onExit = vi.fn();
150154

151155
const { lastFrame } = render(
152156
<TestSessionBrowser
153157
config={config}
154158
onResumeSession={onResumeSession}
159+
onDeleteSession={onDeleteSession}
155160
onExit={onExit}
156161
testSessions={[]}
157162
/>,
@@ -181,12 +186,14 @@ describe('SessionBrowser component', () => {
181186

182187
const config = createMockConfig();
183188
const onResumeSession = vi.fn();
189+
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
184190
const onExit = vi.fn();
185191

186192
const { lastFrame } = render(
187193
<TestSessionBrowser
188194
config={config}
189195
onResumeSession={onResumeSession}
196+
onDeleteSession={onDeleteSession}
190197
onExit={onExit}
191198
testSessions={[session1, session2]}
192199
/>,
@@ -230,12 +237,14 @@ describe('SessionBrowser component', () => {
230237

231238
const config = createMockConfig();
232239
const onResumeSession = vi.fn();
240+
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
233241
const onExit = vi.fn();
234242

235243
const { lastFrame } = render(
236244
<TestSessionBrowser
237245
config={config}
238246
onResumeSession={onResumeSession}
247+
onDeleteSession={onDeleteSession}
239248
onExit={onExit}
240249
testSessions={[searchSession, otherSession]}
241250
/>,
@@ -279,12 +288,14 @@ describe('SessionBrowser component', () => {
279288

280289
const config = createMockConfig();
281290
const onResumeSession = vi.fn();
291+
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
282292
const onExit = vi.fn();
283293

284294
const { lastFrame } = render(
285295
<TestSessionBrowser
286296
config={config}
287297
onResumeSession={onResumeSession}
298+
onDeleteSession={onDeleteSession}
288299
onExit={onExit}
289300
testSessions={[session1, session2]}
290301
/>,
@@ -323,7 +334,7 @@ describe('SessionBrowser component', () => {
323334

324335
const config = createMockConfig();
325336
const onResumeSession = vi.fn();
326-
const onDeleteSession = vi.fn();
337+
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
327338
const onExit = vi.fn();
328339

329340
render(
@@ -348,12 +359,14 @@ describe('SessionBrowser component', () => {
348359
it('shows an error state when loading sessions fails', () => {
349360
const config = createMockConfig();
350361
const onResumeSession = vi.fn();
362+
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
351363
const onExit = vi.fn();
352364

353365
const { lastFrame } = render(
354366
<TestSessionBrowser
355367
config={config}
356368
onResumeSession={onResumeSession}
369+
onDeleteSession={onDeleteSession}
357370
onExit={onExit}
358371
testError="storage failure"
359372
/>,

0 commit comments

Comments
 (0)