Skip to content

Commit 18d10b1

Browse files
committed
fix(cli): resume selected session from ls
1 parent d0a3c0b commit 18d10b1

5 files changed

Lines changed: 123 additions & 19 deletions

File tree

extensions/cli/src/commands/chat.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
SERVICE_NAMES,
1818
} from "../services/types.js";
1919
import {
20-
loadSession,
20+
loadResumeSession,
2121
updateSessionHistory,
2222
updateSessionTitle,
2323
} from "../session.js";
@@ -80,6 +80,7 @@ function stripThinkTags(response: string): string {
8080
export interface ChatOptions extends ExtendedCommandOptions {
8181
headless?: boolean;
8282
resume?: boolean;
83+
resumeSessionId?: string;
8384
fork?: string; // Fork from an existing session ID
8485
format?: "json"; // Output format for headless mode
8586
silent?: boolean; // Strip <think></think> tags and excess whitespace
@@ -106,7 +107,7 @@ export async function initializeChatHistory(
106107

107108
// Load previous session if --resume flag is used
108109
if (options.resume) {
109-
session = loadSession();
110+
session = loadResumeSession(options.resumeSessionId);
110111
if (session) {
111112
logger.info(chalk.yellow("Resuming previous session..."));
112113
return session.history;

extensions/cli/src/commands/ls.test.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
const renderState = vi.hoisted(() => ({
3+
element: undefined as { props?: { onSelect?: (sessionId: string) => Promise<void> } } | undefined,
4+
}));
25

36
import * as sessionModule from "../session.js";
47

8+
import { chat } from "./chat.js";
59
import { listSessionsCommand } from "./ls.js";
610

711
// Mock the session module
@@ -17,15 +21,22 @@ vi.mock("../ui/SessionSelector.js", () => ({
1721

1822
// Mock ink
1923
vi.mock("ink", () => ({
20-
render: vi.fn(() => ({ unmount: vi.fn() })),
24+
render: vi.fn((element) => {
25+
renderState.element = element;
26+
return { unmount: vi.fn() };
27+
}),
2128
}));
2229

2330
// Mock react with createContext
2431
vi.mock("react", async (importOriginal) => {
2532
const actual: any = await importOriginal();
2633
return {
2734
...actual,
28-
createElement: vi.fn(),
35+
createElement: vi.fn((type: any, props: any, ...children: any[]) => ({
36+
type,
37+
props,
38+
children,
39+
})),
2940
createContext: vi.fn(() => ({ Provider: vi.fn(), Consumer: vi.fn() })),
3041
};
3142
});
@@ -42,9 +53,13 @@ vi.mock("./remote.js", () => ({
4253

4354
describe("listSessionsCommand", () => {
4455
const mockListSessions = vi.mocked(sessionModule.listSessions);
56+
const mockLoadSessionById = vi.mocked(sessionModule.loadSessionById);
57+
const mockChat = vi.mocked(chat);
58+
4559

4660
beforeEach(() => {
4761
vi.clearAllMocks();
62+
renderState.element = undefined;
4863
});
4964

5065
afterEach(() => {
@@ -152,4 +167,47 @@ describe("listSessionsCommand", () => {
152167

153168
consoleSpy.mockRestore();
154169
});
170+
171+
it("should pass the selected session id to chat resume", async () => {
172+
const mockSessions = [
173+
{
174+
sessionId: "older-session",
175+
title: "Older session",
176+
dateCreated: "2023-01-01T09:00:00.000Z",
177+
workspaceDirectory: "/workspace",
178+
firstUserMessage: "Older message",
179+
isRemote: false,
180+
},
181+
{
182+
sessionId: "newer-session",
183+
title: "Newer session",
184+
dateCreated: "2023-01-01T10:00:00.000Z",
185+
workspaceDirectory: "/workspace",
186+
firstUserMessage: "Newer message",
187+
isRemote: false,
188+
},
189+
];
190+
mockListSessions.mockResolvedValue(mockSessions);
191+
mockLoadSessionById.mockReturnValue({
192+
sessionId: "older-session",
193+
title: "Older session",
194+
workspaceDirectory: "/workspace",
195+
history: [],
196+
} as any);
197+
198+
const commandPromise = listSessionsCommand({});
199+
await new Promise((resolve) => setImmediate(resolve));
200+
201+
const onSelect = renderState.element?.props?.onSelect;
202+
expect(onSelect).toBeDefined();
203+
await onSelect!("older-session");
204+
await commandPromise;
205+
206+
expect(mockLoadSessionById).toHaveBeenCalledWith("older-session");
207+
expect(mockChat).toHaveBeenCalledWith(undefined, {
208+
resume: true,
209+
resumeSessionId: "older-session",
210+
headless: false,
211+
});
212+
});
155213
});

extensions/cli/src/commands/ls.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,6 @@ interface ListSessionsOptions {
1111
format?: "json";
1212
}
1313

14-
/**
15-
* Set a specific session ID for the current process
16-
* This allows us to load a selected session as if it were the current session
17-
*/
18-
function setSessionId(sessionId: string): void {
19-
// Use the same environment variable that getSessionId() checks
20-
process.env.CONTINUE_CLI_TEST_SESSION_ID = sessionId.replace(
21-
"continue-cli-",
22-
"",
23-
);
24-
}
25-
2614
/**
2715
* List recent chat sessions and allow selection
2816
*/
@@ -79,12 +67,10 @@ export async function listSessionsCommand(
7967

8068
logger.info(`Loading session: ${sessionId}`);
8169

82-
// Set the session ID so that when chat() runs, it will load this session
83-
setSessionId(sessionId);
84-
8570
// Start chat with resume flag to load the selected session
8671
await chat(undefined, {
8772
resume: true,
73+
resumeSessionId: sessionId,
8874
headless: false,
8975
});
9076

extensions/cli/src/session.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
hasSession,
1313
loadOrCreateSessionById,
1414
loadSession,
15+
loadResumeSession,
1516
saveSession,
1617
startNewSession,
1718
updateSessionHistory,
@@ -359,6 +360,52 @@ describe("SessionManager", () => {
359360
});
360361
});
361362

363+
describe("loadResumeSession", () => {
364+
it("should load the selected session id and set it as current", () => {
365+
const selectedSession: Session = {
366+
sessionId: "selected-session-id",
367+
title: "Selected Session",
368+
workspaceDirectory: "/test/workspace",
369+
history: [
370+
{
371+
message: { role: "user", content: "selected" },
372+
contextItems: [],
373+
},
374+
],
375+
};
376+
377+
mockHistoryManager.load.mockReturnValue(selectedSession);
378+
379+
const result = loadResumeSession("selected-session-id");
380+
381+
expect(result).toBe(selectedSession);
382+
expect(mockHistoryManager.load).toHaveBeenCalledWith(
383+
"selected-session-id",
384+
);
385+
expect(getCurrentSession()).toBe(selectedSession);
386+
});
387+
388+
it("should preserve bare resume by loading the most recent session", () => {
389+
const recentSession: Session = {
390+
sessionId: "recent-session-id",
391+
title: "Recent Session",
392+
workspaceDirectory: "/test/workspace",
393+
history: [],
394+
};
395+
396+
mockFs.readdirSync.mockReturnValue(["recent-session.json" as any]);
397+
mockFs.statSync.mockReturnValue({
398+
mtime: new Date("2023-01-02"),
399+
} as any);
400+
mockFs.readFileSync.mockReturnValue(JSON.stringify(recentSession));
401+
402+
const result = loadResumeSession();
403+
404+
expect(result).toEqual(recentSession);
405+
expect(getCurrentSession()).toEqual(recentSession);
406+
});
407+
});
408+
362409
describe("clearSession", () => {
363410
it("should delete session file if it exists", () => {
364411
// Set up a current session

extensions/cli/src/session.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,18 @@ export function loadSession(): Session | null {
331331
}
332332
}
333333

334+
export function loadResumeSession(sessionId?: string): Session | null {
335+
if (!sessionId) {
336+
return loadSession();
337+
}
338+
339+
const session = loadSessionById(sessionId);
340+
if (session) {
341+
SessionManager.getInstance().setSession(session);
342+
}
343+
return session;
344+
}
345+
334346
/**
335347
* Create a new session
336348
*/

0 commit comments

Comments
 (0)