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
*/