Skip to content

Commit 2518ab1

Browse files
refactor(SessionManager): stabilize props and remove forced remount
The change keeps `SelectPrompt` mounted instead of forcing a remount on every view/session-count change, and memoizes the menu `options` so `@inkjs/ui` only sees a new array when the actual session-menu inputs change. Kept an explicit `sessionListVersion` bump after deletion so the delete menu refreshes even when tests mutate the mocked session list in place.
1 parent 872da1e commit 2518ab1

2 files changed

Lines changed: 58 additions & 28 deletions

File tree

src/components/SessionManager/SessionManager.test.tsx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,29 @@ describe('SessionManager', () => {
175175
expect(lastFrame()).not.toContain('Second session');
176176
});
177177

178+
it('keeps the prompt mounted while navigating between views', () => {
179+
const sessionManager = (
180+
<SessionManager
181+
currentSessionId="session-1"
182+
onClose={vi.fn()}
183+
onDelete={vi.fn()}
184+
onNew={vi.fn()}
185+
onOpen={vi.fn()}
186+
/>
187+
);
188+
const { rerender } = render(sessionManager);
189+
190+
expect(selectionState.instanceId).toBe('instance-1');
191+
192+
selectionState.onChange?.('open-menu');
193+
rerender(sessionManager);
194+
expect(selectionState.instanceId).toBe('instance-1');
195+
196+
selectionState.onChange?.('back');
197+
rerender(sessionManager);
198+
expect(selectionState.instanceId).toBe('instance-1');
199+
});
200+
178201
it('shows an error when onOpen throws', () => {
179202
const onOpen = vi.fn().mockImplementation(() => {
180203
throw new Error('Session not found');
@@ -466,7 +489,7 @@ describe('SessionManager', () => {
466489
);
467490
});
468491

469-
it('remounts the select prompt when switching between main, open, and delete views', () => {
492+
it('keeps the select prompt mounted when switching between main, open, and delete views', () => {
470493
const sessionManager = (
471494
<SessionManager
472495
currentSessionId="session-1"
@@ -492,9 +515,9 @@ describe('SessionManager', () => {
492515
rerender(sessionManager);
493516
const deleteInstanceId = selectionState.instanceId;
494517

495-
expect(openInstanceId).not.toBe(mainInstanceId);
496-
expect(nextMainInstanceId).not.toBe(openInstanceId);
497-
expect(deleteInstanceId).not.toBe(mainInstanceId);
498-
expect(nextMainInstanceId).not.toBe(deleteInstanceId);
518+
expect(openInstanceId).toBe(mainInstanceId);
519+
expect(nextMainInstanceId).toBe(openInstanceId);
520+
expect(deleteInstanceId).toBe(mainInstanceId);
521+
expect(nextMainInstanceId).toBe(deleteInstanceId);
499522
});
500523
});

src/components/SessionManager/SessionManager.tsx

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Box, Text, useStdout } from 'ink';
2-
import { useCallback, useState } from 'react';
2+
import { useCallback, useMemo, useState } from 'react';
33

44
import { OPTION, THEME, UI } from '@/constants';
55
import type { ThemeDefinition } from '@/types';
@@ -32,6 +32,12 @@ const ACTION = {
3232
} as const;
3333

3434
const SESSION_LABEL_PADDING = 4;
35+
const MAIN_OPTIONS = [
36+
{ label: 'New session', value: ACTION.NEW },
37+
{ label: 'Open session', value: ACTION.OPEN_MENU },
38+
{ label: 'Delete session', value: ACTION.DELETE_MENU },
39+
{ label: 'Close', value: ACTION.CLOSE },
40+
];
3541

3642
function truncate(value: string, maxLength: number): string {
3743
return value.length > maxLength
@@ -65,38 +71,40 @@ export function SessionManager({
6571
}: Props) {
6672
const [view, setView] = useState<View>(View.Main);
6773
const [error, setError] = useState<string>();
68-
const [, refreshSessionList] = useState(0);
74+
const [sessionListVersion, refreshSessionList] = useState(0);
6975
const { stdout } = useStdout();
7076

7177
const sessions = listSessions();
7278
const maxLabelWidth = Math.max(1, stdout.columns - SESSION_LABEL_PADDING);
73-
const options =
74-
view === View.Open
75-
? [
79+
const options = useMemo(() => {
80+
switch (view) {
81+
case View.Open:
82+
return [
7683
...sessions
7784
.filter(({ id }) => id !== currentSessionId)
7885
.map((session) => ({
7986
label: formatSessionLabel(session, maxLabelWidth),
8087
value: `${ACTION.OPEN_PREFIX}${session.id}`,
8188
})),
8289
OPTION.BACK,
83-
]
84-
: view === View.Delete
85-
? [
86-
...sessions
87-
.filter(({ id }) => id !== currentSessionId)
88-
.map((session) => ({
89-
label: formatSessionLabel(session, maxLabelWidth, 'Delete '),
90-
value: `${ACTION.DELETE_PREFIX}${session.id}`,
91-
})),
92-
OPTION.BACK,
93-
]
94-
: [
95-
{ label: 'New session', value: ACTION.NEW },
96-
{ label: 'Open session', value: ACTION.OPEN_MENU },
97-
{ label: 'Delete session', value: ACTION.DELETE_MENU },
98-
{ label: 'Close', value: ACTION.CLOSE },
99-
];
90+
];
91+
92+
case View.Delete:
93+
return [
94+
...sessions
95+
.filter(({ id }) => id !== currentSessionId)
96+
.map((session) => ({
97+
label: formatSessionLabel(session, maxLabelWidth, 'Delete '),
98+
value: `${ACTION.DELETE_PREFIX}${session.id}`,
99+
})),
100+
OPTION.BACK,
101+
];
102+
103+
case View.Main:
104+
default:
105+
return MAIN_OPTIONS;
106+
}
107+
}, [currentSessionId, maxLabelWidth, sessionListVersion, sessions, view]);
100108

101109
const handleChange = useCallback(
102110
(value: string) => {
@@ -172,7 +180,6 @@ export function SessionManager({
172180
)}
173181

174182
<SelectPrompt
175-
key={`${view}:${String(sessions.length)}`}
176183
options={options}
177184
onCancel={onClose}
178185
onChange={handleChange}

0 commit comments

Comments
 (0)