mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(sessions): add /resume slash command to open the session browser (#13621)
This commit is contained in:
@@ -75,6 +75,7 @@ vi.mock('../ui/commands/modelCommand.js', () => ({
|
|||||||
}));
|
}));
|
||||||
vi.mock('../ui/commands/privacyCommand.js', () => ({ privacyCommand: {} }));
|
vi.mock('../ui/commands/privacyCommand.js', () => ({ privacyCommand: {} }));
|
||||||
vi.mock('../ui/commands/quitCommand.js', () => ({ quitCommand: {} }));
|
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/statsCommand.js', () => ({ statsCommand: {} }));
|
||||||
vi.mock('../ui/commands/themeCommand.js', () => ({ themeCommand: {} }));
|
vi.mock('../ui/commands/themeCommand.js', () => ({ themeCommand: {} }));
|
||||||
vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} }));
|
vi.mock('../ui/commands/toolsCommand.js', () => ({ toolsCommand: {} }));
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { policiesCommand } from '../ui/commands/policiesCommand.js';
|
|||||||
import { profileCommand } from '../ui/commands/profileCommand.js';
|
import { profileCommand } from '../ui/commands/profileCommand.js';
|
||||||
import { quitCommand } from '../ui/commands/quitCommand.js';
|
import { quitCommand } from '../ui/commands/quitCommand.js';
|
||||||
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
||||||
|
import { resumeCommand } from '../ui/commands/resumeCommand.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';
|
||||||
import { toolsCommand } from '../ui/commands/toolsCommand.js';
|
import { toolsCommand } from '../ui/commands/toolsCommand.js';
|
||||||
@@ -82,6 +83,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
|||||||
...(isDevelopment ? [profileCommand] : []),
|
...(isDevelopment ? [profileCommand] : []),
|
||||||
quitCommand,
|
quitCommand,
|
||||||
restoreCommand(this.config),
|
restoreCommand(this.config),
|
||||||
|
resumeCommand,
|
||||||
statsCommand,
|
statsCommand,
|
||||||
themeCommand,
|
themeCommand,
|
||||||
toolsCommand,
|
toolsCommand,
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ const mockUIActions: UIActions = {
|
|||||||
closeSettingsDialog: vi.fn(),
|
closeSettingsDialog: vi.fn(),
|
||||||
closeModelDialog: vi.fn(),
|
closeModelDialog: vi.fn(),
|
||||||
openPermissionsDialog: vi.fn(),
|
openPermissionsDialog: vi.fn(),
|
||||||
|
openSessionBrowser: vi.fn(),
|
||||||
|
closeSessionBrowser: vi.fn(),
|
||||||
|
handleResumeSession: vi.fn(),
|
||||||
|
handleDeleteSession: vi.fn(),
|
||||||
closePermissionsDialog: vi.fn(),
|
closePermissionsDialog: vi.fn(),
|
||||||
setShellModeActive: vi.fn(),
|
setShellModeActive: vi.fn(),
|
||||||
vimHandleInput: vi.fn(),
|
vimHandleInput: vi.fn(),
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ import { type UpdateObject } from './utils/updateCheck.js';
|
|||||||
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
|
||||||
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
import { registerCleanup, runExitCleanup } from '../utils/cleanup.js';
|
||||||
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
|
import { RELAUNCH_EXIT_CODE } from '../utils/processUtils.js';
|
||||||
|
import type { SessionInfo } from '../utils/sessionUtils.js';
|
||||||
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
import { useMessageQueue } from './hooks/useMessageQueue.js';
|
||||||
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
import { useAutoAcceptIndicator } from './hooks/useAutoAcceptIndicator.js';
|
||||||
import { useSessionStats } from './contexts/SessionContext.js';
|
import { useSessionStats } from './contexts/SessionContext.js';
|
||||||
@@ -108,9 +109,10 @@ import {
|
|||||||
useExtensionUpdates,
|
useExtensionUpdates,
|
||||||
} from './hooks/useExtensionUpdates.js';
|
} from './hooks/useExtensionUpdates.js';
|
||||||
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
|
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
|
||||||
import { useSessionResume } from './hooks/useSessionResume.js';
|
|
||||||
import { type ExtensionManager } from '../config/extension-manager.js';
|
import { type ExtensionManager } from '../config/extension-manager.js';
|
||||||
import { requestConsentInteractive } from '../config/extensions/consent.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 { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
|
||||||
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
|
||||||
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
|
||||||
@@ -436,7 +438,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
// Session browser and resume functionality
|
// Session browser and resume functionality
|
||||||
const isGeminiClientInitialized = config.getGeminiClient()?.isInitialized();
|
const isGeminiClientInitialized = config.getGeminiClient()?.isInitialized();
|
||||||
|
|
||||||
useSessionResume({
|
const { loadHistoryForResume } = useSessionResume({
|
||||||
config,
|
config,
|
||||||
historyManager,
|
historyManager,
|
||||||
refreshStatic,
|
refreshStatic,
|
||||||
@@ -445,6 +447,20 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
resumedSessionData,
|
resumedSessionData,
|
||||||
isAuthenticating,
|
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
|
// Create handleAuthSelect wrapper for backward compatibility
|
||||||
const handleAuthSelect = useCallback(
|
const handleAuthSelect = useCallback(
|
||||||
@@ -570,6 +586,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
openEditorDialog,
|
openEditorDialog,
|
||||||
openPrivacyNotice: () => setShowPrivacyNotice(true),
|
openPrivacyNotice: () => setShowPrivacyNotice(true),
|
||||||
openSettingsDialog,
|
openSettingsDialog,
|
||||||
|
openSessionBrowser,
|
||||||
openModelDialog,
|
openModelDialog,
|
||||||
openPermissionsDialog,
|
openPermissionsDialog,
|
||||||
quit: (messages: HistoryItem[]) => {
|
quit: (messages: HistoryItem[]) => {
|
||||||
@@ -590,6 +607,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
openThemeDialog,
|
openThemeDialog,
|
||||||
openEditorDialog,
|
openEditorDialog,
|
||||||
openSettingsDialog,
|
openSettingsDialog,
|
||||||
|
openSessionBrowser,
|
||||||
openModelDialog,
|
openModelDialog,
|
||||||
setQuittingMessages,
|
setQuittingMessages,
|
||||||
setDebugMessage,
|
setDebugMessage,
|
||||||
@@ -1330,6 +1348,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
showPrivacyNotice ||
|
showPrivacyNotice ||
|
||||||
showIdeRestartPrompt ||
|
showIdeRestartPrompt ||
|
||||||
!!proQuotaRequest ||
|
!!proQuotaRequest ||
|
||||||
|
isSessionBrowserOpen ||
|
||||||
isAuthDialogOpen ||
|
isAuthDialogOpen ||
|
||||||
authState === AuthState.AwaitingApiKeyInput;
|
authState === AuthState.AwaitingApiKeyInput;
|
||||||
|
|
||||||
@@ -1402,6 +1421,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
debugMessage,
|
debugMessage,
|
||||||
quittingMessages,
|
quittingMessages,
|
||||||
isSettingsDialogOpen,
|
isSettingsDialogOpen,
|
||||||
|
isSessionBrowserOpen,
|
||||||
isModelDialogOpen,
|
isModelDialogOpen,
|
||||||
isPermissionsDialogOpen,
|
isPermissionsDialogOpen,
|
||||||
permissionsDialogProps,
|
permissionsDialogProps,
|
||||||
@@ -1492,6 +1512,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
debugMessage,
|
debugMessage,
|
||||||
quittingMessages,
|
quittingMessages,
|
||||||
isSettingsDialogOpen,
|
isSettingsDialogOpen,
|
||||||
|
isSessionBrowserOpen,
|
||||||
isModelDialogOpen,
|
isModelDialogOpen,
|
||||||
isPermissionsDialogOpen,
|
isPermissionsDialogOpen,
|
||||||
permissionsDialogProps,
|
permissionsDialogProps,
|
||||||
@@ -1601,6 +1622,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
handleFinalSubmit,
|
handleFinalSubmit,
|
||||||
handleClearScreen,
|
handleClearScreen,
|
||||||
handleProQuotaChoice,
|
handleProQuotaChoice,
|
||||||
|
openSessionBrowser,
|
||||||
|
closeSessionBrowser,
|
||||||
|
handleResumeSession,
|
||||||
|
handleDeleteSession,
|
||||||
setQueueErrorMessage,
|
setQueueErrorMessage,
|
||||||
popAllMessages,
|
popAllMessages,
|
||||||
handleApiKeySubmit,
|
handleApiKeySubmit,
|
||||||
@@ -1632,6 +1657,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
handleFinalSubmit,
|
handleFinalSubmit,
|
||||||
handleClearScreen,
|
handleClearScreen,
|
||||||
handleProQuotaChoice,
|
handleProQuotaChoice,
|
||||||
|
openSessionBrowser,
|
||||||
|
closeSessionBrowser,
|
||||||
|
handleResumeSession,
|
||||||
|
handleDeleteSession,
|
||||||
setQueueErrorMessage,
|
setQueueErrorMessage,
|
||||||
popAllMessages,
|
popAllMessages,
|
||||||
handleApiKeySubmit,
|
handleApiKeySubmit,
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -123,6 +123,7 @@ export interface OpenDialogActionReturn {
|
|||||||
| 'editor'
|
| 'editor'
|
||||||
| 'privacy'
|
| 'privacy'
|
||||||
| 'settings'
|
| 'settings'
|
||||||
|
| 'sessionBrowser'
|
||||||
| 'model'
|
| 'model'
|
||||||
| 'permissions';
|
| 'permissions';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { PrivacyNotice } from '../privacy/PrivacyNotice.js';
|
|||||||
import { ProQuotaDialog } from './ProQuotaDialog.js';
|
import { ProQuotaDialog } from './ProQuotaDialog.js';
|
||||||
import { runExitCleanup } from '../../utils/cleanup.js';
|
import { runExitCleanup } from '../../utils/cleanup.js';
|
||||||
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
import { RELAUNCH_EXIT_CODE } from '../../utils/processUtils.js';
|
||||||
|
import { SessionBrowser } from './SessionBrowser.js';
|
||||||
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js';
|
||||||
import { ModelDialog } from './ModelDialog.js';
|
import { ModelDialog } from './ModelDialog.js';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
@@ -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) {
|
if (uiState.isPermissionsDialogOpen) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ const mockSlashCommands: SlashCommand[] = [
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'resume',
|
||||||
|
description: 'Browse and resume sessions',
|
||||||
|
kind: CommandKind.BUILT_IN,
|
||||||
|
action: vi.fn(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('InputPrompt', () => {
|
describe('InputPrompt', () => {
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ vi.mock('./SessionBrowser.js', async (importOriginal) => {
|
|||||||
moveSelection,
|
moveSelection,
|
||||||
cycleSortOrder,
|
cycleSortOrder,
|
||||||
props.onResumeSession,
|
props.onResumeSession,
|
||||||
props.onDeleteSession,
|
props.onDeleteSession ??
|
||||||
|
(async () => {
|
||||||
|
// no-op delete handler for tests that don't care about deletion
|
||||||
|
}),
|
||||||
props.onExit,
|
props.onExit,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,12 +149,14 @@ describe('SessionBrowser component', () => {
|
|||||||
it('shows empty state when no sessions exist', () => {
|
it('shows empty state when no sessions exist', () => {
|
||||||
const config = createMockConfig();
|
const config = createMockConfig();
|
||||||
const onResumeSession = vi.fn();
|
const onResumeSession = vi.fn();
|
||||||
|
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
|
||||||
const onExit = vi.fn();
|
const onExit = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<TestSessionBrowser
|
<TestSessionBrowser
|
||||||
config={config}
|
config={config}
|
||||||
onResumeSession={onResumeSession}
|
onResumeSession={onResumeSession}
|
||||||
|
onDeleteSession={onDeleteSession}
|
||||||
onExit={onExit}
|
onExit={onExit}
|
||||||
testSessions={[]}
|
testSessions={[]}
|
||||||
/>,
|
/>,
|
||||||
@@ -181,12 +186,14 @@ describe('SessionBrowser component', () => {
|
|||||||
|
|
||||||
const config = createMockConfig();
|
const config = createMockConfig();
|
||||||
const onResumeSession = vi.fn();
|
const onResumeSession = vi.fn();
|
||||||
|
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
|
||||||
const onExit = vi.fn();
|
const onExit = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<TestSessionBrowser
|
<TestSessionBrowser
|
||||||
config={config}
|
config={config}
|
||||||
onResumeSession={onResumeSession}
|
onResumeSession={onResumeSession}
|
||||||
|
onDeleteSession={onDeleteSession}
|
||||||
onExit={onExit}
|
onExit={onExit}
|
||||||
testSessions={[session1, session2]}
|
testSessions={[session1, session2]}
|
||||||
/>,
|
/>,
|
||||||
@@ -230,12 +237,14 @@ describe('SessionBrowser component', () => {
|
|||||||
|
|
||||||
const config = createMockConfig();
|
const config = createMockConfig();
|
||||||
const onResumeSession = vi.fn();
|
const onResumeSession = vi.fn();
|
||||||
|
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
|
||||||
const onExit = vi.fn();
|
const onExit = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<TestSessionBrowser
|
<TestSessionBrowser
|
||||||
config={config}
|
config={config}
|
||||||
onResumeSession={onResumeSession}
|
onResumeSession={onResumeSession}
|
||||||
|
onDeleteSession={onDeleteSession}
|
||||||
onExit={onExit}
|
onExit={onExit}
|
||||||
testSessions={[searchSession, otherSession]}
|
testSessions={[searchSession, otherSession]}
|
||||||
/>,
|
/>,
|
||||||
@@ -279,12 +288,14 @@ describe('SessionBrowser component', () => {
|
|||||||
|
|
||||||
const config = createMockConfig();
|
const config = createMockConfig();
|
||||||
const onResumeSession = vi.fn();
|
const onResumeSession = vi.fn();
|
||||||
|
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
|
||||||
const onExit = vi.fn();
|
const onExit = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<TestSessionBrowser
|
<TestSessionBrowser
|
||||||
config={config}
|
config={config}
|
||||||
onResumeSession={onResumeSession}
|
onResumeSession={onResumeSession}
|
||||||
|
onDeleteSession={onDeleteSession}
|
||||||
onExit={onExit}
|
onExit={onExit}
|
||||||
testSessions={[session1, session2]}
|
testSessions={[session1, session2]}
|
||||||
/>,
|
/>,
|
||||||
@@ -323,7 +334,7 @@ describe('SessionBrowser component', () => {
|
|||||||
|
|
||||||
const config = createMockConfig();
|
const config = createMockConfig();
|
||||||
const onResumeSession = vi.fn();
|
const onResumeSession = vi.fn();
|
||||||
const onDeleteSession = vi.fn();
|
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
|
||||||
const onExit = vi.fn();
|
const onExit = vi.fn();
|
||||||
|
|
||||||
render(
|
render(
|
||||||
@@ -348,12 +359,14 @@ describe('SessionBrowser component', () => {
|
|||||||
it('shows an error state when loading sessions fails', () => {
|
it('shows an error state when loading sessions fails', () => {
|
||||||
const config = createMockConfig();
|
const config = createMockConfig();
|
||||||
const onResumeSession = vi.fn();
|
const onResumeSession = vi.fn();
|
||||||
|
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
|
||||||
const onExit = vi.fn();
|
const onExit = vi.fn();
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = render(
|
||||||
<TestSessionBrowser
|
<TestSessionBrowser
|
||||||
config={config}
|
config={config}
|
||||||
onResumeSession={onResumeSession}
|
onResumeSession={onResumeSession}
|
||||||
|
onDeleteSession={onDeleteSession}
|
||||||
onExit={onExit}
|
onExit={onExit}
|
||||||
testError="storage failure"
|
testError="storage failure"
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export interface SessionBrowserProps {
|
|||||||
/** Callback when user selects a session to resume */
|
/** Callback when user selects a session to resume */
|
||||||
onResumeSession: (session: SessionInfo) => void;
|
onResumeSession: (session: SessionInfo) => void;
|
||||||
/** Callback when user deletes a session */
|
/** Callback when user deletes a session */
|
||||||
onDeleteSession?: (session: SessionInfo) => void;
|
onDeleteSession: (session: SessionInfo) => Promise<void>;
|
||||||
/** Callback when user exits the session browser */
|
/** Callback when user exits the session browser */
|
||||||
onExit: () => void;
|
onExit: () => void;
|
||||||
}
|
}
|
||||||
@@ -463,9 +463,11 @@ const SessionItem = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reserve a few characters for metadata like " (current)" so the name doesn't wrap awkwardly.
|
||||||
|
const reservedForMeta = additionalInfo ? additionalInfo.length + 1 : 0;
|
||||||
const availableMessageWidth = Math.max(
|
const availableMessageWidth = Math.max(
|
||||||
20,
|
20,
|
||||||
terminalWidth - FIXED_SESSION_COLUMNS_WIDTH,
|
terminalWidth - FIXED_SESSION_COLUMNS_WIDTH - reservedForMeta,
|
||||||
);
|
);
|
||||||
|
|
||||||
const truncatedMessage =
|
const truncatedMessage =
|
||||||
@@ -759,7 +761,7 @@ export const useSessionBrowserInput = (
|
|||||||
moveSelection: (delta: number) => void,
|
moveSelection: (delta: number) => void,
|
||||||
cycleSortOrder: () => void,
|
cycleSortOrder: () => void,
|
||||||
onResumeSession: (session: SessionInfo) => void,
|
onResumeSession: (session: SessionInfo) => void,
|
||||||
onDeleteSession: ((session: SessionInfo) => void) | undefined,
|
onDeleteSession: (session: SessionInfo) => Promise<void>,
|
||||||
onExit: () => void,
|
onExit: () => void,
|
||||||
) => {
|
) => {
|
||||||
useKeypress(
|
useKeypress(
|
||||||
@@ -817,13 +819,9 @@ export const useSessionBrowserInput = (
|
|||||||
else if (key.sequence === 'x' || key.sequence === 'X') {
|
else if (key.sequence === 'x' || key.sequence === 'X') {
|
||||||
const selectedSession =
|
const selectedSession =
|
||||||
state.filteredAndSortedSessions[state.activeIndex];
|
state.filteredAndSortedSessions[state.activeIndex];
|
||||||
if (
|
if (selectedSession && !selectedSession.isCurrentSession) {
|
||||||
selectedSession &&
|
onDeleteSession(selectedSession)
|
||||||
!selectedSession.isCurrentSession &&
|
.then(() => {
|
||||||
onDeleteSession
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
onDeleteSession(selectedSession);
|
|
||||||
// Remove the session from the state
|
// Remove the session from the state
|
||||||
state.setSessions(
|
state.setSessions(
|
||||||
state.sessions.filter((s) => s.id !== selectedSession.id),
|
state.sessions.filter((s) => s.id !== selectedSession.id),
|
||||||
@@ -838,17 +836,18 @@ export const useSessionBrowserInput = (
|
|||||||
Math.max(0, state.filteredAndSortedSessions.length - 2),
|
Math.max(0, state.filteredAndSortedSessions.length - 2),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
})
|
||||||
|
.catch((error) => {
|
||||||
state.setError(
|
state.setError(
|
||||||
`Failed to delete session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
`Failed to delete session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// less-like u/d controls.
|
// less-like u/d controls.
|
||||||
else if (key.sequence === 'd') {
|
else if (key.sequence === 'u') {
|
||||||
moveSelection(-Math.round(SESSIONS_PER_PAGE / 2));
|
moveSelection(-Math.round(SESSIONS_PER_PAGE / 2));
|
||||||
} else if (key.sequence === 'u') {
|
} else if (key.sequence === 'd') {
|
||||||
moveSelection(Math.round(SESSIONS_PER_PAGE / 2));
|
moveSelection(Math.round(SESSIONS_PER_PAGE / 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { type AuthType, type EditorType } from '@google/gemini-cli-core';
|
|||||||
import { type LoadableSettingScope } from '../../config/settings.js';
|
import { type LoadableSettingScope } from '../../config/settings.js';
|
||||||
import type { AuthState } from '../types.js';
|
import type { AuthState } from '../types.js';
|
||||||
import { type PermissionsDialogProps } from '../components/PermissionsModifyTrustDialog.js';
|
import { type PermissionsDialogProps } from '../components/PermissionsModifyTrustDialog.js';
|
||||||
|
import type { SessionInfo } from '../../utils/sessionUtils.js';
|
||||||
|
|
||||||
export interface UIActions {
|
export interface UIActions {
|
||||||
handleThemeSelect: (themeName: string, scope: LoadableSettingScope) => void;
|
handleThemeSelect: (themeName: string, scope: LoadableSettingScope) => void;
|
||||||
@@ -45,6 +46,10 @@ export interface UIActions {
|
|||||||
handleProQuotaChoice: (
|
handleProQuotaChoice: (
|
||||||
choice: 'retry_later' | 'retry_once' | 'retry_always' | 'upgrade',
|
choice: 'retry_later' | 'retry_once' | 'retry_always' | 'upgrade',
|
||||||
) => void;
|
) => void;
|
||||||
|
openSessionBrowser: () => void;
|
||||||
|
closeSessionBrowser: () => void;
|
||||||
|
handleResumeSession: (session: SessionInfo) => Promise<void>;
|
||||||
|
handleDeleteSession: (session: SessionInfo) => Promise<void>;
|
||||||
setQueueErrorMessage: (message: string | null) => void;
|
setQueueErrorMessage: (message: string | null) => void;
|
||||||
popAllMessages: (onPop: (messages: string | undefined) => void) => void;
|
popAllMessages: (onPop: (messages: string | undefined) => void) => void;
|
||||||
handleApiKeySubmit: (apiKey: string) => Promise<void>;
|
handleApiKeySubmit: (apiKey: string) => Promise<void>;
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface UIState {
|
|||||||
debugMessage: string;
|
debugMessage: string;
|
||||||
quittingMessages: HistoryItem[] | null;
|
quittingMessages: HistoryItem[] | null;
|
||||||
isSettingsDialogOpen: boolean;
|
isSettingsDialogOpen: boolean;
|
||||||
|
isSessionBrowserOpen: boolean;
|
||||||
isModelDialogOpen: boolean;
|
isModelDialogOpen: boolean;
|
||||||
isPermissionsDialogOpen: boolean;
|
isPermissionsDialogOpen: boolean;
|
||||||
permissionsDialogProps: { targetDirectory?: string } | null;
|
permissionsDialogProps: { targetDirectory?: string } | null;
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ describe('useSlashCommandProcessor', () => {
|
|||||||
openEditorDialog: vi.fn(),
|
openEditorDialog: vi.fn(),
|
||||||
openPrivacyNotice: vi.fn(),
|
openPrivacyNotice: vi.fn(),
|
||||||
openSettingsDialog: vi.fn(),
|
openSettingsDialog: vi.fn(),
|
||||||
|
openSessionBrowser: vi.fn(),
|
||||||
openModelDialog: mockOpenModelDialog,
|
openModelDialog: mockOpenModelDialog,
|
||||||
openPermissionsDialog: vi.fn(),
|
openPermissionsDialog: vi.fn(),
|
||||||
quit: mockSetQuittingMessages,
|
quit: mockSetQuittingMessages,
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ interface SlashCommandProcessorActions {
|
|||||||
openEditorDialog: () => void;
|
openEditorDialog: () => void;
|
||||||
openPrivacyNotice: () => void;
|
openPrivacyNotice: () => void;
|
||||||
openSettingsDialog: () => void;
|
openSettingsDialog: () => void;
|
||||||
|
openSessionBrowser: () => void;
|
||||||
openModelDialog: () => void;
|
openModelDialog: () => void;
|
||||||
openPermissionsDialog: (props?: { targetDirectory?: string }) => void;
|
openPermissionsDialog: (props?: { targetDirectory?: string }) => void;
|
||||||
quit: (messages: HistoryItem[]) => void;
|
quit: (messages: HistoryItem[]) => void;
|
||||||
@@ -410,6 +411,9 @@ export const useSlashCommandProcessor = (
|
|||||||
case 'privacy':
|
case 'privacy':
|
||||||
actions.openPrivacyNotice();
|
actions.openPrivacyNotice();
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
|
case 'sessionBrowser':
|
||||||
|
actions.openSessionBrowser();
|
||||||
|
return { type: 'handled' };
|
||||||
case 'settings':
|
case 'settings':
|
||||||
actions.openSettingsDialog();
|
actions.openSettingsDialog();
|
||||||
return { type: 'handled' };
|
return { type: 'handled' };
|
||||||
|
|||||||
@@ -91,12 +91,16 @@ export const useSessionBrowser = (
|
|||||||
*/
|
*/
|
||||||
handleDeleteSession: useCallback(
|
handleDeleteSession: useCallback(
|
||||||
(session: SessionInfo) => {
|
(session: SessionInfo) => {
|
||||||
|
// Note: Chat sessions are stored on disk using a filename derived from
|
||||||
|
// the session, e.g. "session-<timestamp>-<sessionIdPrefix>.json".
|
||||||
|
// The ChatRecordingService.deleteSession API expects this file basename
|
||||||
|
// (without the ".json" extension), not the full session UUID.
|
||||||
try {
|
try {
|
||||||
const chatRecordingService = config
|
const chatRecordingService = config
|
||||||
.getGeminiClient()
|
.getGeminiClient()
|
||||||
?.getChatRecordingService();
|
?.getChatRecordingService();
|
||||||
if (chatRecordingService) {
|
if (chatRecordingService) {
|
||||||
chatRecordingService.deleteSession(session.id);
|
chatRecordingService.deleteSession(session.file);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting session:', error);
|
console.error('Error deleting session:', error);
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ export class GeminiChat {
|
|||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.processStreamResponse(model, streamResponse);
|
return this.processStreamResponse(effectiveModel, streamResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user