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
10 changes: 10 additions & 0 deletions extensions/cli/src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ export const SYSTEM_SLASH_COMMANDS: SystemCommand[] = [
description: "List background jobs",
category: "system",
},
{
name: "export",
description: "Export a session to JSON file",
category: "system",
},
{
name: "import",
description: "Import a session from JSON file",
category: "system",
},
];

// Remote mode specific commands
Expand Down
84 changes: 84 additions & 0 deletions extensions/cli/src/slashCommands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import fs from "fs";

import { type AssistantConfig } from "@continuedev/sdk";
import chalk from "chalk";
import type { Session } from "core/index.js";
import historyManager from "core/util/history.js";
import { v4 as uuidv4 } from "uuid";

import {
isAuthenticated,
Expand Down Expand Up @@ -173,6 +178,83 @@ function handleJobs() {
return { openJobsSelector: true };
}

interface ExportedSession {
version: 1;
exportedAt: string;
session: Session;
}

function handleExport(_args: string[]): SlashCommandResult {
posthogService.capture("useSlashCommand", { name: "export" });

return {
exit: false,
openExportSelector: true,
};
}

function handleImport(args: string[]): SlashCommandResult {
posthogService.capture("useSlashCommand", { name: "import" });

const filePath = args[0];
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: /import only uses args[0], so file paths containing spaces are truncated and reported as missing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At extensions/cli/src/slashCommands.ts, line 199:

<comment>`/import` only uses `args[0]`, so file paths containing spaces are truncated and reported as missing.</comment>

<file context>
@@ -173,6 +178,83 @@ function handleJobs() {
+function handleImport(args: string[]): SlashCommandResult {
+  posthogService.capture("useSlashCommand", { name: "import" });
+
+  const filePath = args[0];
+  if (!filePath) {
+    return {
</file context>
Suggested change
const filePath = args[0];
const filePath = args.join(" ").trim().replace(/^['\"]|['\"]$/g, "");
Fix with Cubic

if (!filePath) {
return {
exit: false,
output: chalk.yellow(
"Please provide a file path. Usage: /import <file-path>",
),
};
}

if (!fs.existsSync(filePath)) {
return {
exit: false,
output: chalk.red(`File not found: ${filePath}`),
};
}

try {
const fileContent = fs.readFileSync(filePath, "utf-8");
const exportedData: ExportedSession = JSON.parse(fileContent);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Import command lacks runtime validation of exported session format/version before persistence, allowing incompatible or malformed session data to be saved.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At extensions/cli/src/slashCommands.ts, line 218:

<comment>Import command lacks runtime validation of exported session format/version before persistence, allowing incompatible or malformed session data to be saved.</comment>

<file context>
@@ -173,6 +178,83 @@ function handleJobs() {
+
+  try {
+    const fileContent = fs.readFileSync(filePath, "utf-8");
+    const exportedData: ExportedSession = JSON.parse(fileContent);
+
+    let session = exportedData.session;
</file context>
Fix with Cubic


let session = exportedData.session;

const existingSessions = historyManager.list({ limit: 1000 });
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Import collision detection only checks 1000 sessions, so duplicate IDs outside that window can be missed and existing sessions overwritten.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At extensions/cli/src/slashCommands.ts, line 222:

<comment>Import collision detection only checks 1000 sessions, so duplicate IDs outside that window can be missed and existing sessions overwritten.</comment>

<file context>
@@ -173,6 +178,83 @@ function handleJobs() {
+
+    let session = exportedData.session;
+
+    const existingSessions = historyManager.list({ limit: 1000 });
+    const sessionExists = existingSessions.some(
+      (s) => s.sessionId === session.sessionId,
</file context>
Suggested change
const existingSessions = historyManager.list({ limit: 1000 });
const existingSessions = historyManager.list({});
Fix with Cubic

const sessionExists = existingSessions.some(
(s) => s.sessionId === session.sessionId,
);

if (sessionExists) {
const originalId = session.sessionId;
session = {
...session,
sessionId: uuidv4(),
};
historyManager.save(session);
return {
exit: false,
output: chalk.green(
`Session imported with new ID: ${session.sessionId}\n` +
chalk.gray(`(original ID: ${originalId} already existed)`),
),
};
}

historyManager.save(session);
return {
exit: false,
output: chalk.green(
`Session imported: ${session.sessionId} (${session.title})`,
),
};
} catch (error: any) {
return {
exit: false,
output: chalk.red(`Failed to import session: ${error.message}`),
};
}
}

const commandHandlers: Record<string, CommandHandler> = {
help: handleHelp,
clear: () => {
Expand Down Expand Up @@ -208,6 +290,8 @@ const commandHandlers: Record<string, CommandHandler> = {
return { openUpdateSelector: true };
},
jobs: handleJobs,
export: handleExport,
import: handleImport,
};

export async function handleSlashCommands(
Expand Down
67 changes: 67 additions & 0 deletions extensions/cli/src/ui/TUIChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ const TUIChat: React.FC<TUIChatProps> = ({
onShowUpdateSelector: () => navigateTo("update"),
onShowSessionSelector: () => navigateTo("session"),
onShowJobsSelector: () => navigateTo("jobs"),
onShowExportSelector: () => navigateTo("export"),
onReload: handleReload,
onClear: handleClear,
onRefreshStatic: () => setStaticRefreshTrigger((prev) => prev + 1),
Expand Down Expand Up @@ -332,6 +333,71 @@ const TUIChat: React.FC<TUIChatProps> = ({
[closeCurrentScreen, setChatHistory, setShowIntroMessage],
);

// Export session handler
const handleExportSession = useCallback(
async (sessionId: string) => {
try {
const { loadSessionById } = await import("../session.js");
const fs = await import("fs");
const path = await import("path");

const session = loadSessionById(sessionId);
if (!session) {
setChatHistory((prev) => [
...prev,
{
message: {
role: "system",
content: `Failed to export: Session ${sessionId} not found`,
},
contextItems: [],
},
]);
closeCurrentScreen();
return;
}

const exportPayload = {
version: 1,
exportedAt: new Date().toISOString(),
session,
};

const defaultPath = path.join(
process.cwd(),
`continue-session-${session.sessionId}.json`,
);
const jsonOutput = JSON.stringify(exportPayload, null, 2);
fs.writeFileSync(defaultPath, jsonOutput, "utf-8");

setChatHistory((prev) => [
...prev,
{
message: {
role: "system",
content: `Session exported to ${defaultPath}`,
},
contextItems: [],
},
]);
closeCurrentScreen();
} catch (error: any) {
setChatHistory((prev) => [
...prev,
{
message: {
role: "system",
content: `Failed to export session: ${error.message}`,
},
contextItems: [],
},
]);
closeCurrentScreen();
}
},
[closeCurrentScreen, setChatHistory],
);

// Determine if input should be disabled
// Allow input even when services are loading, but disable for UI overlays
const isInputDisabled =
Expand Down Expand Up @@ -432,6 +498,7 @@ const TUIChat: React.FC<TUIChatProps> = ({
handleConfigSelect={handleConfigSelect}
handleModelSelect={handleModelSelect}
handleSessionSelect={handleSessionSelect}
handleExportSession={handleExportSession}
handleReload={handleReload}
closeCurrentScreen={closeCurrentScreen}
activePermissionRequest={activePermissionRequest}
Expand Down
12 changes: 12 additions & 0 deletions extensions/cli/src/ui/components/ScreenContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface ScreenContentProps {
handleConfigSelect: (config: ConfigOption) => Promise<void>;
handleModelSelect: (model: ModelOption) => Promise<void>;
handleSessionSelect: (sessionId: string) => Promise<void>;
handleExportSession: (sessionId: string) => Promise<void>;
handleReload: () => Promise<void>;
closeCurrentScreen: () => void;
activePermissionRequest: ActivePermissionRequest | null;
Expand Down Expand Up @@ -74,6 +75,7 @@ export const ScreenContent: React.FC<ScreenContentProps> = ({
handleConfigSelect,
handleModelSelect,
handleSessionSelect,
handleExportSession,
handleReload,
closeCurrentScreen,
activePermissionRequest,
Expand Down Expand Up @@ -164,6 +166,16 @@ export const ScreenContent: React.FC<ScreenContentProps> = ({
);
}

// Export selector
if (isScreenActive("export")) {
return (
<SessionSelectorWithLoading
onSelect={handleExportSession}
onExit={closeCurrentScreen}
/>
);
}

// Jobs selector
if (isScreenActive("jobs")) {
return <JobsSelector onCancel={closeCurrentScreen} />;
Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/src/ui/context/NavigationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export type NavigationScreen =
| "update" // Update selector
| "edit" // Edit message selector
| "jobs" // Background Jobs selector
| "session"; // Session selector
| "export"; // Export session selector

interface NavigationState {
currentScreen: NavigationScreen;
Expand Down
7 changes: 7 additions & 0 deletions extensions/cli/src/ui/hooks/useChat.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ interface ProcessSlashCommandResultOptions {
onShowMCPSelector?: () => void;
onShowSessionSelector?: () => void;
onShowJobsSelector?: () => void;
onShowExportSelector?: () => void;
onClear?: () => void;
}

Expand All @@ -68,6 +69,7 @@ export function processSlashCommandResult({
onShowMCPSelector,
onShowSessionSelector,
onShowJobsSelector,
onShowExportSelector,
onClear,
}: ProcessSlashCommandResultOptions): string | null {
if (result.exit) {
Expand Down Expand Up @@ -104,6 +106,11 @@ export function processSlashCommandResult({
return null;
}

if (result.openExportSelector && onShowExportSelector) {
onShowExportSelector();
return null;
}

if (result.clear) {
const systemMessage = chatHistory.find(
(item) => item.message.role === "system",
Expand Down
2 changes: 2 additions & 0 deletions extensions/cli/src/ui/hooks/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function useChat({
onShowMCPSelector,
onShowSessionSelector,
onShowJobsSelector,
onShowExportSelector,
onClear,
onRefreshStatic,
isRemoteMode = false,
Expand Down Expand Up @@ -453,6 +454,7 @@ export function useChat({
onShowSessionSelector,
onShowJobsSelector,
onShowUpdateSelector,
onShowExportSelector,
onClear,
});

Expand Down
2 changes: 2 additions & 0 deletions extensions/cli/src/ui/hooks/useChat.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface UseChatProps {
onShowModelSelector?: () => void;
onShowSessionSelector?: () => void;
onShowJobsSelector?: () => void;
onShowExportSelector?: () => void;
onReload?: () => Promise<void>;
onClear?: () => void;
onRefreshStatic?: () => void;
Expand Down Expand Up @@ -68,6 +69,7 @@ export interface SlashCommandResult {
openUpdateSelector?: boolean;
openSessionSelector?: boolean;
openJobsSelector?: boolean;
openExportSelector?: boolean;
compact?: boolean;
diffContent?: string;
}
Loading