Skip to content

Commit 15fdedc

Browse files
Merge pull request #139 from ai-action/feat/resume
2 parents f949b38 + 965fcd1 commit 15fdedc

5 files changed

Lines changed: 66 additions & 21 deletions

File tree

src/cli.test.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ToolResult } from './types';
22

33
type RunAction = (model: string, prompt: string) => Promise<void>;
4-
type ResumeAction = (sessionId: string) => Promise<void>;
4+
type ResumeAction = (sessionId?: string) => Promise<void>;
55

66
const {
77
color,
@@ -105,7 +105,7 @@ describe('cli', () => {
105105
it('renders TUI with no args', async () => {
106106
await main([]);
107107
expect(mockReset).toHaveBeenCalledOnce();
108-
expect(renderApp).toHaveBeenCalledWith(undefined);
108+
expect(renderApp).toHaveBeenCalledWith({});
109109
expect(parse).not.toHaveBeenCalled();
110110
});
111111

@@ -349,6 +349,16 @@ describe('cli', () => {
349349
expect(process.exitCode).toBe(1);
350350
});
351351

352+
it('opens the session picker when resume is called without a sessionId', async () => {
353+
await commandState.resumeAction?.();
354+
355+
expect(loadSession).not.toHaveBeenCalled();
356+
expect(mockReset).toHaveBeenCalledOnce();
357+
expect(renderApp).toHaveBeenCalledWith({
358+
initialScreen: 'session-manager',
359+
});
360+
});
361+
352362
it('loads the requested session and renders the TUI for resume', async () => {
353363
loadSession.mockReturnValueOnce({
354364
metadata: { id: 'session-1', directory: process.cwd() },
@@ -359,7 +369,7 @@ describe('cli', () => {
359369

360370
expect(loadSession).toHaveBeenCalledWith('session-1');
361371
expect(mockReset).toHaveBeenCalledOnce();
362-
expect(renderApp).toHaveBeenCalledWith('session-1');
372+
expect(renderApp).toHaveBeenCalledWith({ sessionId: 'session-1' });
363373
});
364374

365375
it('allows resume when session has no directory field (legacy session)', async () => {
@@ -370,7 +380,7 @@ describe('cli', () => {
370380

371381
await commandState.resumeAction?.('session-1');
372382

373-
expect(renderApp).toHaveBeenCalledWith('session-1');
383+
expect(renderApp).toHaveBeenCalledWith({ sessionId: 'session-1' });
374384
expect(writeError).not.toHaveBeenCalled();
375385
});
376386

src/cli.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@ import { realpathSync } from 'node:fs';
44

55
import cac from 'cac';
66

7-
import { PACKAGE, ROLE, UI } from './constants';
7+
import { PACKAGE, ROLE, SCREEN, UI } from './constants';
8+
import type { Screen } from './types';
89
import { agents, ollama, screen, session, terminal, tools } from './utils';
910

11+
interface LaunchOptions {
12+
sessionId?: string;
13+
initialScreen?: Screen;
14+
}
15+
1016
const cli = cac('code-ollama');
1117
const MAX_TOOL_TURNS = 25;
1218
const MAX_TOOL_INTENT_CORRECTIONS = 2;
@@ -28,10 +34,16 @@ cli
2834
});
2935

3036
cli
31-
.command('resume <sessionId>', 'Resume a saved session')
32-
.action(async (sessionId: string) => {
37+
.command('resume [sessionId]', 'Resume a saved session')
38+
.action(async (sessionId?: string) => {
3339
try {
40+
if (!sessionId) {
41+
await launchTui({ initialScreen: SCREEN.SESSION_MANAGER });
42+
return;
43+
}
44+
3445
const loaded = session.loadSession(sessionId);
46+
3547
if (
3648
loaded.metadata.directory &&
3749
loaded.metadata.directory !== process.cwd()
@@ -45,7 +57,8 @@ cli
4557
process.exitCode = 1;
4658
return;
4759
}
48-
await launchTui(sessionId);
60+
61+
await launchTui({ sessionId });
4962
} catch (error) {
5063
const message = error instanceof Error ? error.message : 'Unknown error';
5164
terminal.writeError(`Error: ${message}\n`);
@@ -164,10 +177,10 @@ export async function main(
164177
}
165178
}
166179

167-
async function launchTui(sessionId?: string): Promise<void> {
180+
async function launchTui(options: LaunchOptions = {}): Promise<void> {
168181
const { renderApp } = await import('./tui');
169182
screen.reset();
170-
renderApp(sessionId);
183+
renderApp(options);
171184
}
172185

173186
// v8 ignore start

src/components/App/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,18 @@ import { SessionManager } from '@/components/SessionManager';
1010
import { ThemeSettings } from '@/components/ThemeSettings';
1111
import { UpdateBanner } from '@/components/UpdateBanner';
1212
import { MODE, SCREEN, THEME } from '@/constants';
13-
import type { Config, Mode } from '@/types';
13+
import type { Config, Mode, Screen } from '@/types';
1414
import { config, ollama, session } from '@/utils';
1515

1616
import { useScreenRouter, useSessionManager, useThemeSettings } from './hooks';
1717
import { ReadinessCheck, ReadinessState } from './ReadinessCheck';
1818

1919
interface Props {
2020
sessionId?: string;
21+
initialScreen?: Screen;
2122
}
2223

23-
export function App({ sessionId }: Props) {
24+
export function App({ sessionId, initialScreen }: Props) {
2425
const [appConfig, setConfig] = useState(() => config.loadConfig());
2526
const [mode, setMode] = useState<Mode>(MODE.SAFE);
2627
const [isLoaded, setIsLoaded] = useState(false);
@@ -32,7 +33,7 @@ export function App({ sessionId }: Props) {
3233
);
3334

3435
const { currentScreen, setScreen, handleClose, handleCommand } =
35-
useScreenRouter();
36+
useScreenRouter({ initialScreen });
3637

3738
const {
3839
activeSession,

src/components/App/hooks/useScreenRouter.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,17 @@ export interface CommandCallbacks {
1212
onSetPreviewThemeId: (themeId: ThemeId) => void;
1313
}
1414

15-
export function useScreenRouter() {
15+
interface UseScreenRouterOptions {
16+
initialScreen?: Screen;
17+
}
18+
19+
export function useScreenRouter({
20+
initialScreen,
21+
}: UseScreenRouterOptions = {}) {
1622
const { exit } = useApp();
17-
const [currentScreen, setScreen] = useState<Screen>(SCREEN.CHAT);
23+
const [currentScreen, setScreen] = useState<Screen>(
24+
initialScreen ?? SCREEN.CHAT,
25+
);
1826

1927
const handleClose = useCallback(() => {
2028
setScreen(SCREEN.CHAT);

src/tui.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
import { render } from 'ink';
22

33
import { App } from './components';
4+
import type { Screen } from './types';
45
import { screen } from './utils';
56

6-
export function renderApp(sessionId?: string): void {
7+
export interface LaunchOptions {
8+
sessionId?: string;
9+
initialScreen?: Screen;
10+
}
11+
12+
export function renderApp(options: LaunchOptions = {}): void {
713
let resetKey = 0;
814

9-
const app = render(<App key={resetKey} sessionId={sessionId} />, {
10-
exitOnCtrlC: false,
11-
maxFps: 60,
12-
});
15+
const app = render(
16+
<App
17+
key={resetKey}
18+
sessionId={options.sessionId}
19+
initialScreen={options.initialScreen}
20+
/>,
21+
{
22+
exitOnCtrlC: false,
23+
maxFps: 60,
24+
},
25+
);
1326

1427
screen.setClearHandler((nextSessionId) => {
1528
screen.reset();
1629
app.rerender(
17-
<App key={++resetKey} sessionId={nextSessionId ?? sessionId} />,
30+
<App key={++resetKey} sessionId={nextSessionId ?? options.sessionId} />,
1831
);
1932
});
2033
}

0 commit comments

Comments
 (0)