diff --git a/extensions/cli/src/commands/chat.ts b/extensions/cli/src/commands/chat.ts index a32df348818..3b0df897acc 100644 --- a/extensions/cli/src/commands/chat.ts +++ b/extensions/cli/src/commands/chat.ts @@ -17,7 +17,7 @@ import { SERVICE_NAMES, } from "../services/types.js"; import { - loadSession, + loadResumeSession, updateSessionHistory, updateSessionTitle, } from "../session.js"; @@ -80,6 +80,7 @@ function stripThinkTags(response: string): string { export interface ChatOptions extends ExtendedCommandOptions { headless?: boolean; resume?: boolean; + resumeSessionId?: string; fork?: string; // Fork from an existing session ID format?: "json"; // Output format for headless mode silent?: boolean; // Strip tags and excess whitespace @@ -106,7 +107,7 @@ export async function initializeChatHistory( // Load previous session if --resume flag is used if (options.resume) { - session = loadSession(); + session = loadResumeSession(options.resumeSessionId); if (session) { logger.info(chalk.yellow("Resuming previous session...")); return session.history; diff --git a/extensions/cli/src/commands/ls.test.ts b/extensions/cli/src/commands/ls.test.ts index c56b7eb8cf7..8bf909dddde 100644 --- a/extensions/cli/src/commands/ls.test.ts +++ b/extensions/cli/src/commands/ls.test.ts @@ -1,7 +1,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +const renderState = vi.hoisted(() => ({ + element: undefined as + | { props?: { onSelect?: (sessionId: string) => Promise } } + | undefined, +})); import * as sessionModule from "../session.js"; +import { chat } from "./chat.js"; import { listSessionsCommand } from "./ls.js"; // Mock the session module @@ -17,7 +23,10 @@ vi.mock("../ui/SessionSelector.js", () => ({ // Mock ink vi.mock("ink", () => ({ - render: vi.fn(() => ({ unmount: vi.fn() })), + render: vi.fn((element) => { + renderState.element = element; + return { unmount: vi.fn() }; + }), })); // Mock react with createContext @@ -25,7 +34,11 @@ vi.mock("react", async (importOriginal) => { const actual: any = await importOriginal(); return { ...actual, - createElement: vi.fn(), + createElement: vi.fn((type: any, props: any, ...children: any[]) => ({ + type, + props, + children, + })), createContext: vi.fn(() => ({ Provider: vi.fn(), Consumer: vi.fn() })), }; }); @@ -42,9 +55,12 @@ vi.mock("./remote.js", () => ({ describe("listSessionsCommand", () => { const mockListSessions = vi.mocked(sessionModule.listSessions); + const mockLoadSessionById = vi.mocked(sessionModule.loadSessionById); + const mockChat = vi.mocked(chat); beforeEach(() => { vi.clearAllMocks(); + renderState.element = undefined; }); afterEach(() => { @@ -152,4 +168,47 @@ describe("listSessionsCommand", () => { consoleSpy.mockRestore(); }); + + it("should pass the selected session id to chat resume", async () => { + const mockSessions = [ + { + sessionId: "older-session", + title: "Older session", + dateCreated: "2023-01-01T09:00:00.000Z", + workspaceDirectory: "/workspace", + firstUserMessage: "Older message", + isRemote: false, + }, + { + sessionId: "newer-session", + title: "Newer session", + dateCreated: "2023-01-01T10:00:00.000Z", + workspaceDirectory: "/workspace", + firstUserMessage: "Newer message", + isRemote: false, + }, + ]; + mockListSessions.mockResolvedValue(mockSessions); + mockLoadSessionById.mockReturnValue({ + sessionId: "older-session", + title: "Older session", + workspaceDirectory: "/workspace", + history: [], + } as any); + + const commandPromise = listSessionsCommand({}); + await new Promise((resolve) => setImmediate(resolve)); + + const onSelect = renderState.element?.props?.onSelect; + expect(onSelect).toBeDefined(); + await onSelect!("older-session"); + await commandPromise; + + expect(mockLoadSessionById).toHaveBeenCalledWith("older-session"); + expect(mockChat).toHaveBeenCalledWith(undefined, { + resume: true, + resumeSessionId: "older-session", + headless: false, + }); + }); }); diff --git a/extensions/cli/src/commands/ls.ts b/extensions/cli/src/commands/ls.ts index 8e108a1c9b9..0e6b276f96c 100644 --- a/extensions/cli/src/commands/ls.ts +++ b/extensions/cli/src/commands/ls.ts @@ -11,18 +11,6 @@ interface ListSessionsOptions { format?: "json"; } -/** - * Set a specific session ID for the current process - * This allows us to load a selected session as if it were the current session - */ -function setSessionId(sessionId: string): void { - // Use the same environment variable that getSessionId() checks - process.env.CONTINUE_CLI_TEST_SESSION_ID = sessionId.replace( - "continue-cli-", - "", - ); -} - /** * List recent chat sessions and allow selection */ @@ -79,12 +67,10 @@ export async function listSessionsCommand( logger.info(`Loading session: ${sessionId}`); - // Set the session ID so that when chat() runs, it will load this session - setSessionId(sessionId); - // Start chat with resume flag to load the selected session await chat(undefined, { resume: true, + resumeSessionId: sessionId, headless: false, }); diff --git a/extensions/cli/src/session.test.ts b/extensions/cli/src/session.test.ts index 51bb6456645..1cf69a7914a 100644 --- a/extensions/cli/src/session.test.ts +++ b/extensions/cli/src/session.test.ts @@ -12,6 +12,7 @@ import { hasSession, loadOrCreateSessionById, loadSession, + loadResumeSession, saveSession, startNewSession, updateSessionHistory, @@ -359,6 +360,52 @@ describe("SessionManager", () => { }); }); + describe("loadResumeSession", () => { + it("should load the selected session id and set it as current", () => { + const selectedSession: Session = { + sessionId: "selected-session-id", + title: "Selected Session", + workspaceDirectory: "/test/workspace", + history: [ + { + message: { role: "user", content: "selected" }, + contextItems: [], + }, + ], + }; + + mockHistoryManager.load.mockReturnValue(selectedSession); + + const result = loadResumeSession("selected-session-id"); + + expect(result).toBe(selectedSession); + expect(mockHistoryManager.load).toHaveBeenCalledWith( + "selected-session-id", + ); + expect(getCurrentSession()).toBe(selectedSession); + }); + + it("should preserve bare resume by loading the most recent session", () => { + const recentSession: Session = { + sessionId: "recent-session-id", + title: "Recent Session", + workspaceDirectory: "/test/workspace", + history: [], + }; + + mockFs.readdirSync.mockReturnValue(["recent-session.json" as any]); + mockFs.statSync.mockReturnValue({ + mtime: new Date("2023-01-02"), + } as any); + mockFs.readFileSync.mockReturnValue(JSON.stringify(recentSession)); + + const result = loadResumeSession(); + + expect(result).toEqual(recentSession); + expect(getCurrentSession()).toEqual(recentSession); + }); + }); + describe("clearSession", () => { it("should delete session file if it exists", () => { // Set up a current session diff --git a/extensions/cli/src/session.ts b/extensions/cli/src/session.ts index 45f765a08ae..71910fddf64 100644 --- a/extensions/cli/src/session.ts +++ b/extensions/cli/src/session.ts @@ -331,6 +331,18 @@ export function loadSession(): Session | null { } } +export function loadResumeSession(sessionId?: string): Session | null { + if (!sessionId) { + return loadSession(); + } + + const session = loadSessionById(sessionId); + if (session) { + SessionManager.getInstance().setSession(session); + } + return session; +} + /** * Create a new session */