diff --git a/src/cli.test.ts b/src/cli.test.ts index 6c7fbdb..0c51d24 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,7 +1,7 @@ import type { ToolResult } from './types'; type RunAction = (model: string, prompt: string) => Promise; -type ResumeAction = (sessionId: string) => Promise; +type ResumeAction = (sessionId?: string) => Promise; const { color, @@ -105,7 +105,7 @@ describe('cli', () => { it('renders TUI with no args', async () => { await main([]); expect(mockReset).toHaveBeenCalledOnce(); - expect(renderApp).toHaveBeenCalledWith(undefined); + expect(renderApp).toHaveBeenCalledWith({}); expect(parse).not.toHaveBeenCalled(); }); @@ -349,6 +349,16 @@ describe('cli', () => { expect(process.exitCode).toBe(1); }); + it('opens the session picker when resume is called without a sessionId', async () => { + await commandState.resumeAction?.(); + + expect(loadSession).not.toHaveBeenCalled(); + expect(mockReset).toHaveBeenCalledOnce(); + expect(renderApp).toHaveBeenCalledWith({ + initialScreen: 'session-manager', + }); + }); + it('loads the requested session and renders the TUI for resume', async () => { loadSession.mockReturnValueOnce({ metadata: { id: 'session-1', directory: process.cwd() }, @@ -359,7 +369,7 @@ describe('cli', () => { expect(loadSession).toHaveBeenCalledWith('session-1'); expect(mockReset).toHaveBeenCalledOnce(); - expect(renderApp).toHaveBeenCalledWith('session-1'); + expect(renderApp).toHaveBeenCalledWith({ sessionId: 'session-1' }); }); it('allows resume when session has no directory field (legacy session)', async () => { @@ -370,7 +380,7 @@ describe('cli', () => { await commandState.resumeAction?.('session-1'); - expect(renderApp).toHaveBeenCalledWith('session-1'); + expect(renderApp).toHaveBeenCalledWith({ sessionId: 'session-1' }); expect(writeError).not.toHaveBeenCalled(); }); diff --git a/src/cli.ts b/src/cli.ts index a2b676c..708473e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -4,9 +4,15 @@ import { realpathSync } from 'node:fs'; import cac from 'cac'; -import { PACKAGE, ROLE, UI } from './constants'; +import { PACKAGE, ROLE, SCREEN, UI } from './constants'; +import type { Screen } from './types'; import { agents, ollama, screen, session, terminal, tools } from './utils'; +interface LaunchOptions { + sessionId?: string; + initialScreen?: Screen; +} + const cli = cac('code-ollama'); const MAX_TOOL_TURNS = 25; const MAX_TOOL_INTENT_CORRECTIONS = 2; @@ -28,10 +34,16 @@ cli }); cli - .command('resume ', 'Resume a saved session') - .action(async (sessionId: string) => { + .command('resume [sessionId]', 'Resume a saved session') + .action(async (sessionId?: string) => { try { + if (!sessionId) { + await launchTui({ initialScreen: SCREEN.SESSION_MANAGER }); + return; + } + const loaded = session.loadSession(sessionId); + if ( loaded.metadata.directory && loaded.metadata.directory !== process.cwd() @@ -45,7 +57,8 @@ cli process.exitCode = 1; return; } - await launchTui(sessionId); + + await launchTui({ sessionId }); } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; terminal.writeError(`Error: ${message}\n`); @@ -164,10 +177,10 @@ export async function main( } } -async function launchTui(sessionId?: string): Promise { +async function launchTui(options: LaunchOptions = {}): Promise { const { renderApp } = await import('./tui'); screen.reset(); - renderApp(sessionId); + renderApp(options); } // v8 ignore start diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index eedca6b..feb27bf 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -10,7 +10,7 @@ import { SessionManager } from '@/components/SessionManager'; import { ThemeSettings } from '@/components/ThemeSettings'; import { UpdateBanner } from '@/components/UpdateBanner'; import { MODE, SCREEN, THEME } from '@/constants'; -import type { Config, Mode } from '@/types'; +import type { Config, Mode, Screen } from '@/types'; import { config, ollama, session } from '@/utils'; import { useScreenRouter, useSessionManager, useThemeSettings } from './hooks'; @@ -18,9 +18,10 @@ import { ReadinessCheck, ReadinessState } from './ReadinessCheck'; interface Props { sessionId?: string; + initialScreen?: Screen; } -export function App({ sessionId }: Props) { +export function App({ sessionId, initialScreen }: Props) { const [appConfig, setConfig] = useState(() => config.loadConfig()); const [mode, setMode] = useState(MODE.SAFE); const [isLoaded, setIsLoaded] = useState(false); @@ -32,7 +33,7 @@ export function App({ sessionId }: Props) { ); const { currentScreen, setScreen, handleClose, handleCommand } = - useScreenRouter(); + useScreenRouter({ initialScreen }); const { activeSession, diff --git a/src/components/App/hooks/useScreenRouter.ts b/src/components/App/hooks/useScreenRouter.ts index 58c5c10..2206135 100644 --- a/src/components/App/hooks/useScreenRouter.ts +++ b/src/components/App/hooks/useScreenRouter.ts @@ -12,9 +12,17 @@ export interface CommandCallbacks { onSetPreviewThemeId: (themeId: ThemeId) => void; } -export function useScreenRouter() { +interface UseScreenRouterOptions { + initialScreen?: Screen; +} + +export function useScreenRouter({ + initialScreen, +}: UseScreenRouterOptions = {}) { const { exit } = useApp(); - const [currentScreen, setScreen] = useState(SCREEN.CHAT); + const [currentScreen, setScreen] = useState( + initialScreen ?? SCREEN.CHAT, + ); const handleClose = useCallback(() => { setScreen(SCREEN.CHAT); diff --git a/src/tui.tsx b/src/tui.tsx index 90b13ab..ee1bdb3 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -1,20 +1,33 @@ import { render } from 'ink'; import { App } from './components'; +import type { Screen } from './types'; import { screen } from './utils'; -export function renderApp(sessionId?: string): void { +export interface LaunchOptions { + sessionId?: string; + initialScreen?: Screen; +} + +export function renderApp(options: LaunchOptions = {}): void { let resetKey = 0; - const app = render(, { - exitOnCtrlC: false, - maxFps: 60, - }); + const app = render( + , + { + exitOnCtrlC: false, + maxFps: 60, + }, + ); screen.setClearHandler((nextSessionId) => { screen.reset(); app.rerender( - , + , ); }); }