Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/src/services/BuiltinCommandLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ vi.mock('../ui/commands/modelCommand.js', () => ({
}));
vi.mock('../ui/commands/privacyCommand.js', () => ({ privacyCommand: {} }));
vi.mock('../ui/commands/quitCommand.js', () => ({ quitCommand: {} }));
vi.mock('../ui/commands/resumeCommand.js', () => ({ resumeCommand: {} }));
vi.mock('../ui/commands/statsCommand.js', () => ({ statsCommand: {} }));
vi.mock('../ui/commands/themeCommand.js', () => ({ themeCommand: {} }));
vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} }));
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { policiesCommand } from '../ui/commands/policiesCommand.js';
import { profileCommand } from '../ui/commands/profileCommand.js';
import { quitCommand } from '../ui/commands/quitCommand.js';
import { restoreCommand } from '../ui/commands/restoreCommand.js';
import { resumeCommand } from '../ui/commands/resumeCommand.js';
import { statsCommand } from '../ui/commands/statsCommand.js';
import { themeCommand } from '../ui/commands/themeCommand.js';
import { toolsCommand } from '../ui/commands/toolsCommand.js';
Expand Down Expand Up @@ -82,6 +83,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
...(isDevelopment ? [profileCommand] : []),
quitCommand,
restoreCommand(this.config),
resumeCommand,
statsCommand,
themeCommand,
toolsCommand,
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/test-utils/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ const mockUIActions: UIActions = {
closeSettingsDialog: vi.fn(),
closeModelDialog: vi.fn(),
openPermissionsDialog: vi.fn(),
openSessionBrowser: vi.fn(),
closeSessionBrowser: vi.fn(),
handleResumeSession: vi.fn(),
handleDeleteSession: vi.fn(),
closePermissionsDialog: vi.fn(),
setShellModeActive: vi.fn(),
vimHandleInput: vi.fn(),
Expand Down
33 changes: 31 additions & 2 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { type UpdateObject } from './utils/updateCheck.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
import type { SessionInfo } from '../utils/sessionUtils.js';
import { useMessageQueue } from './hooks/useMessageQueue.js';
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
import { useSessionStats } from './contexts/SessionContext.js';
Expand All @@ -108,9 +109,10 @@ import {
useExtensionUpdates,
} from './hooks/useExtensionUpdates.js';
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
import { useSessionResume } from './hooks/useSessionResume.js';
import { type ExtensionManager } from '../config/extension-manager.js';
import { requestConsentInteractive } from '../config/extensions/consent.js';
import { useSessionBrowser } from './hooks/useSessionBrowser.js';
import { useSessionResume } from './hooks/useSessionResume.js';
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
Expand Down Expand Up @@ -436,7 +438,7 @@ export const AppContainer = (props: AppContainerProps) => {
// Session browser and resume functionality
const isGeminiClientInitialized = config.getGeminiClient()?.isInitialized();

useSessionResume({
const { loadHistoryForResume } = useSessionResume({
config,
historyManager,
refreshStatic,
Expand All @@ -445,6 +447,20 @@ export const AppContainer = (props: AppContainerProps) => {
resumedSessionData,
isAuthenticating,
});
const {
isSessionBrowserOpen,
openSessionBrowser,
closeSessionBrowser,
handleResumeSession,
handleDeleteSession: handleDeleteSessionSync,
} = useSessionBrowser(config, loadHistoryForResume);
// Wrap handleDeleteSession to return a Promise for UIActions interface
const handleDeleteSession = useCallback(
async (session: SessionInfo): Promise<void> => {
handleDeleteSessionSync(session);
},
[handleDeleteSessionSync],
);

// Create handleAuthSelect wrapper for backward compatibility
const handleAuthSelect = useCallback(
Expand Down Expand Up @@ -570,6 +586,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
openEditorDialog,
openPrivacyNotice: () => setShowPrivacyNotice(true),
openSettingsDialog,
openSessionBrowser,
openModelDialog,
openPermissionsDialog,
quit: (messages: HistoryItem[]) => {
Expand All @@ -590,6 +607,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
openThemeDialog,
openEditorDialog,
openSettingsDialog,
openSessionBrowser,
openModelDialog,
setQuittingMessages,
setDebugMessage,
Expand Down Expand Up @@ -1330,6 +1348,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
showPrivacyNotice ||
showIdeRestartPrompt ||
!!proQuotaRequest ||
isSessionBrowserOpen ||
isAuthDialogOpen ||
authState === AuthState.AwaitingApiKeyInput;

Expand Down Expand Up @@ -1402,6 +1421,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
debugMessage,
quittingMessages,
isSettingsDialogOpen,
isSessionBrowserOpen,
isModelDialogOpen,
isPermissionsDialogOpen,
permissionsDialogProps,
Expand Down Expand Up @@ -1492,6 +1512,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
debugMessage,
quittingMessages,
isSettingsDialogOpen,
isSessionBrowserOpen,
isModelDialogOpen,
isPermissionsDialogOpen,
permissionsDialogProps,
Expand Down Expand Up @@ -1601,6 +1622,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleFinalSubmit,
handleClearScreen,
handleProQuotaChoice,
openSessionBrowser,
closeSessionBrowser,
handleResumeSession,
handleDeleteSession,
setQueueErrorMessage,
popAllMessages,
handleApiKeySubmit,
Expand Down Expand Up @@ -1632,6 +1657,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
handleFinalSubmit,
handleClearScreen,
handleProQuotaChoice,
openSessionBrowser,
closeSessionBrowser,
handleResumeSession,
handleDeleteSession,
setQueueErrorMessage,
popAllMessages,
handleApiKeySubmit,
Expand Down
25 changes: 25 additions & 0 deletions packages/cli/src/ui/commands/resumeCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import type {
OpenDialogActionReturn,
CommandContext,
SlashCommand,
} from './types.js';
import { CommandKind } from './types.js';

export const resumeCommand: SlashCommand = {
name: 'resume',
description: 'Browse and resume auto-saved conversations',
kind: CommandKind.BUILT_IN,
action: async (
_context: CommandContext,
_args: string,
): Promise<OpenDialogActionReturn> => ({
type: 'dialog',
dialog: 'sessionBrowser',
}),
};
1 change: 1 addition & 0 deletions packages/cli/src/ui/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export interface OpenDialogActionReturn {
| 'editor'
| 'privacy'
| 'settings'
| 'sessionBrowser'
| 'model'
| 'permissions';
}
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/ui/components/DialogManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { PrivacyNotice } from '../privacy/PrivacyNotice.js';
import { ProQuotaDialog } from './ProQuotaDialog.js';
import { runExitCleanup } from '../../utils/cleanup.js';
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
import { SessionBrowser } from './SessionBrowser.js';
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
import { ModelDialog } from './ModelDialog.js';
import { theme } from '../semantic-colors.js';
Expand Down Expand Up @@ -210,6 +211,16 @@ export const DialogManager = ({
/>
);
}
if (uiState.isSessionBrowserOpen) {
return (
<SessionBrowser
config={config}
onResumeSession={uiActions.handleResumeSession}
onDeleteSession={uiActions.handleDeleteSession}
onExit={uiActions.closeSessionBrowser}
/>
);
}

if (uiState.isPermissionsDialogOpen) {
return (
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/ui/components/InputPrompt.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ const mockSlashCommands: SlashCommand[] = [
},
],
},
{
name: 'resume',
description: 'Browse and resume sessions',
kind: CommandKind.BUILT_IN,
action: vi.fn(),
},
];

describe('InputPrompt', () => {
Expand Down
17 changes: 15 additions & 2 deletions packages/cli/src/ui/components/SessionBrowser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ vi.mock('./SessionBrowser.js', async (importOriginal) => {
moveSelection,
cycleSortOrder,
props.onResumeSession,
props.onDeleteSession,
props.onDeleteSession ??
(async () => {
// no-op delete handler for tests that don't care about deletion
}),
props.onExit,
);

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

const { lastFrame } = render(
<TestSessionBrowser
config={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
testSessions={[]}
/>,
Expand Down Expand Up @@ -181,12 +186,14 @@ describe('SessionBrowser component', () => {

const config = createMockConfig();
const onResumeSession = vi.fn();
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
const onExit = vi.fn();

const { lastFrame } = render(
<TestSessionBrowser
config={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
testSessions={[session1, session2]}
/>,
Expand Down Expand Up @@ -230,12 +237,14 @@ describe('SessionBrowser component', () => {

const config = createMockConfig();
const onResumeSession = vi.fn();
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
const onExit = vi.fn();

const { lastFrame } = render(
<TestSessionBrowser
config={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
testSessions={[searchSession, otherSession]}
/>,
Expand Down Expand Up @@ -279,12 +288,14 @@ describe('SessionBrowser component', () => {

const config = createMockConfig();
const onResumeSession = vi.fn();
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
const onExit = vi.fn();

const { lastFrame } = render(
<TestSessionBrowser
config={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
testSessions={[session1, session2]}
/>,
Expand Down Expand Up @@ -323,7 +334,7 @@ describe('SessionBrowser component', () => {

const config = createMockConfig();
const onResumeSession = vi.fn();
const onDeleteSession = vi.fn();
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
const onExit = vi.fn();

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

const { lastFrame } = render(
<TestSessionBrowser
config={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
testError="storage failure"
/>,
Expand Down
Loading