feat(sessions): add /resume slash command to open the session browser (#13621)

This commit is contained in:
bl-ue
2025-11-25 11:54:09 -07:00
committed by GitHub
parent 098e5c281c
commit 94c3eecb99
16 changed files with 142 additions and 36 deletions

View File

@@ -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';
@@ -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 (

View File

@@ -86,6 +86,12 @@ const mockSlashCommands: SlashCommand[] = [
},
],
},
{
name: 'resume',
description: 'Browse and resume sessions',
kind: CommandKind.BUILT_IN,
action: vi.fn(),
},
];
describe('InputPrompt', () => {

View File

@@ -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,
);
@@ -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={[]}
/>,
@@ -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]}
/>,
@@ -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]}
/>,
@@ -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]}
/>,
@@ -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(
@@ -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"
/>,

View File

@@ -28,7 +28,7 @@ export interface SessionBrowserProps {
/** Callback when user selects a session to resume */
onResumeSession: (session: SessionInfo) => void;
/** Callback when user deletes a session */
onDeleteSession?: (session: SessionInfo) => void;
onDeleteSession: (session: SessionInfo) => Promise<void>;
/** Callback when user exits the session browser */
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(
20,
terminalWidth - FIXED_SESSION_COLUMNS_WIDTH,
terminalWidth - FIXED_SESSION_COLUMNS_WIDTH - reservedForMeta,
);
const truncatedMessage =
@@ -759,7 +761,7 @@ export const useSessionBrowserInput = (
moveSelection: (delta: number) => void,
cycleSortOrder: () => void,
onResumeSession: (session: SessionInfo) => void,
onDeleteSession: ((session: SessionInfo) => void) | undefined,
onDeleteSession: (session: SessionInfo) => Promise<void>,
onExit: () => void,
) => {
useKeypress(
@@ -817,38 +819,35 @@ export const useSessionBrowserInput = (
else if (key.sequence === 'x' || key.sequence === 'X') {
const selectedSession =
state.filteredAndSortedSessions[state.activeIndex];
if (
selectedSession &&
!selectedSession.isCurrentSession &&
onDeleteSession
) {
try {
onDeleteSession(selectedSession);
// Remove the session from the state
state.setSessions(
state.sessions.filter((s) => s.id !== selectedSession.id),
);
// Adjust active index if needed
if (
state.activeIndex >=
state.filteredAndSortedSessions.length - 1
) {
state.setActiveIndex(
Math.max(0, state.filteredAndSortedSessions.length - 2),
if (selectedSession && !selectedSession.isCurrentSession) {
onDeleteSession(selectedSession)
.then(() => {
// Remove the session from the state
state.setSessions(
state.sessions.filter((s) => s.id !== selectedSession.id),
);
}
} catch (error) {
state.setError(
`Failed to delete session: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
}
// Adjust active index if needed
if (
state.activeIndex >=
state.filteredAndSortedSessions.length - 1
) {
state.setActiveIndex(
Math.max(0, state.filteredAndSortedSessions.length - 2),
);
}
})
.catch((error) => {
state.setError(
`Failed to delete session: ${error instanceof Error ? error.message : 'Unknown error'}`,
);
});
}
}
// less-like u/d controls.
else if (key.sequence === 'd') {
else if (key.sequence === 'u') {
moveSelection(-Math.round(SESSIONS_PER_PAGE / 2));
} else if (key.sequence === 'u') {
} else if (key.sequence === 'd') {
moveSelection(Math.round(SESSIONS_PER_PAGE / 2));
}
}