Skip to content

Commit cfc2ea9

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

5 files changed

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

38
import * as sessionModule from "../session.js";
49

10+
import { chat } from "./chat.js";
511
import { listSessionsCommand } from "./ls.js";
612

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

1824
// Mock ink
1925
vi.mock("ink", () => ({
20-
render: vi.fn(() => ({ unmount: vi.fn() })),
26+
render: vi.fn((element) => {
27+
renderState.element = element;
28+
return { unmount: vi.fn() };
29+
}),
2130
}));
2231

2332
// Mock react with createContext
2433
vi.mock("react", async (importOriginal) => {
2534
const actual: any = await importOriginal();
2635
return {
2736
...actual,
28-
createElement: vi.fn(),
37+
createElement: vi.fn((type: any, props: any, ...children: any[]) => ({
38+
type,
39+
props,
40+
children,
41+
})),
2942
createContext: vi.fn(() => ({ Provider: vi.fn(), Consumer: vi.fn() })),
3043
};
3144
});
@@ -42,9 +55,12 @@ vi.mock("./remote.js", () => ({
4255

4356
describe("listSessionsCommand", () => {
4457
const mockListSessions = vi.mocked(sessionModule.listSessions);
58+
const mockLoadSessionById = vi.mocked(sessionModule.loadSessionById);
59+
const mockChat = vi.mocked(chat);
4560

4661
beforeEach(() => {
4762
vi.clearAllMocks();
63+
renderState.element = undefined;
4864
});
4965

5066
afterEach(() => {
@@ -152,4 +168,47 @@ describe("listSessionsCommand", () => {
152168

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

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)