Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions extensions/cli/src/commands/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
SERVICE_NAMES,
} from "../services/types.js";
import {
loadSession,
loadResumeSession,
updateSessionHistory,
updateSessionTitle,
} from "../session.js";
Expand Down Expand Up @@ -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 <think></think> tags and excess whitespace
Expand All @@ -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;
Expand Down
63 changes: 61 additions & 2 deletions extensions/cli/src/commands/ls.test.ts
Original file line number Diff line number Diff line change
@@ -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<void> } }
| undefined,
}));

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

import { chat } from "./chat.js";
import { listSessionsCommand } from "./ls.js";

// Mock the session module
Expand All @@ -17,15 +23,22 @@ 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
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() })),
};
});
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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,
});
});
});
16 changes: 1 addition & 15 deletions extensions/cli/src/commands/ls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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,
});

Expand Down
47 changes: 47 additions & 0 deletions extensions/cli/src/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
hasSession,
loadOrCreateSessionById,
loadSession,
loadResumeSession,
saveSession,
startNewSession,
updateSessionHistory,
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions extensions/cli/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Loading