diff --git a/packages/electron-app/.codenomad-data/mjnyjm5r/sessions.json b/packages/electron-app/.codenomad-data/mjnyjm5r/sessions.json new file mode 100644 index 0000000..d040257 --- /dev/null +++ b/packages/electron-app/.codenomad-data/mjnyjm5r/sessions.json @@ -0,0 +1,33 @@ +{ + "sessions": { + "01KDFA3KMG1VSQNA217HZ3JAYA": { + "id": "01KDFA3KMG1VSQNA217HZ3JAYA", + "workspaceId": "mjnyjm5r", + "title": "New Session", + "parentId": null, + "createdAt": 1766819221136, + "updatedAt": 1766819221136, + "messageIds": [], + "model": { + "providerId": "opencode-zen", + "modelId": "grok-code" + }, + "agent": "Assistant" + }, + "01KDFA3SP5YJAB7B8BC2EM48NY": { + "id": "01KDFA3SP5YJAB7B8BC2EM48NY", + "workspaceId": "mjnyjm5r", + "title": "New Session", + "parentId": null, + "createdAt": 1766819227333, + "updatedAt": 1766819227333, + "messageIds": [], + "model": { + "providerId": "opencode-zen", + "modelId": "grok-code" + }, + "agent": "Assistant" + } + }, + "messages": {} +} \ No newline at end of file diff --git a/packages/server/src/server/routes/native-sessions.ts b/packages/server/src/server/routes/native-sessions.ts index 673dc7e..a6d2f7c 100644 --- a/packages/server/src/server/routes/native-sessions.ts +++ b/packages/server/src/server/routes/native-sessions.ts @@ -158,6 +158,42 @@ export function registerNativeSessionsRoutes(app: FastifyInstance, deps: NativeS } }) + // Import sessions from SDK mode - for migration when switching to native mode + app.post<{ + Params: { workspaceId: string } + Body: { + sessions: Array<{ + id: string + title?: string + parentId?: string | null + createdAt?: number + updatedAt?: number + model?: { providerId: string; modelId: string } + agent?: string + messages?: Array<{ + id: string + role: "user" | "assistant" | "system" | "tool" + content?: string + createdAt?: number + }> + }> + } + }>("/api/native/workspaces/:workspaceId/sessions/import", async (request, reply) => { + try { + const result = await sessionManager.importSessions( + request.params.workspaceId, + request.body.sessions + ) + logger.info({ workspaceId: request.params.workspaceId, ...result }, "Sessions imported from SDK mode") + return { success: true, ...result } + } catch (error) { + logger.error({ error }, "Failed to import sessions") + reply.code(500) + return { error: "Failed to import sessions" } + } + }) + + // Get messages for a session app.get<{ Params: { workspaceId: string; sessionId: string } }>("/api/native/workspaces/:workspaceId/sessions/:sessionId/messages", async (request, reply) => { try { diff --git a/packages/server/src/storage/session-store.ts b/packages/server/src/storage/session-store.ts index a85b0a9..e4ff1b7 100644 --- a/packages/server/src/storage/session-store.ts +++ b/packages/server/src/storage/session-store.ts @@ -311,6 +311,74 @@ export class NativeSessionManager { const store = this.stores.get(workspaceId) return store ? Object.keys(store.sessions).length : 0 } + + /** + * Import sessions from SDK mode format - for migration when switching modes + */ + async importSessions(workspaceId: string, sessions: Array<{ + id: string + title?: string + parentId?: string | null + createdAt?: number + updatedAt?: number + model?: { providerId: string; modelId: string } + agent?: string + messages?: Array<{ + id: string + role: "user" | "assistant" | "system" | "tool" + content?: string + createdAt?: number + }> + }>): Promise<{ imported: number; skipped: number }> { + const store = await this.loadStore(workspaceId) + let imported = 0 + let skipped = 0 + + for (const sdkSession of sessions) { + // Skip if session already exists + if (store.sessions[sdkSession.id]) { + skipped++ + continue + } + + const now = Date.now() + const session: Session = { + id: sdkSession.id, + workspaceId, + title: sdkSession.title || "Imported Session", + parentId: sdkSession.parentId ?? null, + createdAt: sdkSession.createdAt || now, + updatedAt: sdkSession.updatedAt || now, + messageIds: [], + model: sdkSession.model, + agent: sdkSession.agent, + } + + // Import messages if provided + if (sdkSession.messages && Array.isArray(sdkSession.messages)) { + for (const msg of sdkSession.messages) { + const message: SessionMessage = { + id: msg.id, + sessionId: sdkSession.id, + role: msg.role, + content: msg.content, + createdAt: msg.createdAt || now, + updatedAt: msg.createdAt || now, + status: "completed" + } + store.messages[msg.id] = message + session.messageIds.push(msg.id) + } + } + + store.sessions[sdkSession.id] = session + imported++ + } + + await this.saveStore(workspaceId) + log.info({ workspaceId, imported, skipped }, "Imported sessions from SDK mode") + return { imported, skipped } + } } // Singleton instance diff --git a/packages/ui/src/lib/lite-mode.ts b/packages/ui/src/lib/lite-mode.ts index 09ce5ce..9a31b05 100644 --- a/packages/ui/src/lib/lite-mode.ts +++ b/packages/ui/src/lib/lite-mode.ts @@ -165,6 +165,34 @@ export const nativeSessionApi = { return data.messages }, + /** + * Import sessions from SDK mode to Native mode + */ + async importSessions(workspaceId: string, sessions: Array<{ + id: string + title?: string + parentId?: string | null + createdAt?: number + updatedAt?: number + model?: { providerId: string; modelId: string } + agent?: string + messages?: Array<{ + id: string + role: "user" | "assistant" | "system" | "tool" + content?: string + createdAt?: number + }> + }>): Promise<{ success: boolean; imported: number; skipped: number }> { + const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/import`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessions }) + }) + if (!response.ok) throw new Error("Failed to import sessions") + return response.json() + }, + + /** * Send a prompt to the session and get a streaming response */ diff --git a/packages/ui/src/stores/session-api.ts b/packages/ui/src/stores/session-api.ts index a38baed..f71689a 100644 --- a/packages/ui/src/stores/session-api.ts +++ b/packages/ui/src/stores/session-api.ts @@ -3,6 +3,7 @@ import type { Message } from "../types/message" import { instances } from "./instances" import { nativeSessionApi } from "../lib/lite-mode" +import { needsMigration, migrateSessionsToNative, markMigrated, getExistingSdkSessions } from "./session-migration" import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences" import { setSessionCompactionState } from "./session-compaction" import { @@ -389,6 +390,26 @@ async function fetchSessions(instanceId: string): Promise { let responseData: any[] = [] if (isNative) { + // Check if we need to migrate sessions from SDK mode + if (needsMigration(instanceId)) { + const existingSdkSessions = getExistingSdkSessions(instanceId) + if (existingSdkSessions.length > 0) { + log.info({ instanceId, count: existingSdkSessions.length }, "Migrating SDK sessions to native mode") + const migrationData = existingSdkSessions.map(s => ({ + id: s.id, + title: s.title, + parentId: s.parentId, + time: s.time, + model: s.model, + agent: s.agent + })) + const result = await migrateSessionsToNative(instanceId, migrationData) + log.info({ instanceId, result }, "Migration completed") + } else { + markMigrated(instanceId) + } + } + const nativeSessions = await nativeSessionApi.listSessions(instanceId) responseData = nativeSessions.map(s => ({ id: s.id, diff --git a/packages/ui/src/stores/session-migration.ts b/packages/ui/src/stores/session-migration.ts new file mode 100644 index 0000000..02eda99 --- /dev/null +++ b/packages/ui/src/stores/session-migration.ts @@ -0,0 +1,125 @@ +/** + * Session Migration - Handles importing sessions when switching between SDK and Native modes + */ + +import { nativeSessionApi } from "../lib/lite-mode" +import { sessions } from "./session-state" +import { getLogger } from "../lib/logger" +import type { Session } from "../types/session" + +const log = getLogger("session-migration") + +// Track which workspaces have already been migrated to prevent duplicate migrations +const migratedWorkspaces = new Set() + +export interface MigrationResult { + success: boolean + imported: number + skipped: number + error?: string +} + +/** + * Check if a workspace needs session migration + */ +export function needsMigration(workspaceId: string): boolean { + return !migratedWorkspaces.has(workspaceId) +} + +/** + * Mark a workspace as migrated + */ +export function markMigrated(workspaceId: string): void { + migratedWorkspaces.add(workspaceId) +} + +/** + * Get existing SDK sessions for a workspace from the local store + */ +export function getExistingSdkSessions(instanceId: string): Session[] { + const instanceSessions = sessions().get(instanceId) + if (!instanceSessions) return [] + return Array.from(instanceSessions.values()) +} + +/** + * Migrate sessions from SDK mode to Native mode + * This should be called when the user switches from an SDK binary to native mode + */ +export async function migrateSessionsToNative( + workspaceId: string, + sdkSessions: Array<{ + id: string + title?: string + parentId?: string | null + time?: { created?: number; updated?: number } + model?: { providerId: string; modelId: string } + agent?: string + messages?: Array<{ + id: string + role: "user" | "assistant" | "system" | "tool" + content?: string + timestamp?: number + }> + }> +): Promise { + if (sdkSessions.length === 0) { + log.info({ workspaceId }, "No sessions to migrate") + markMigrated(workspaceId) + return { success: true, imported: 0, skipped: 0 } + } + + try { + log.info({ workspaceId, count: sdkSessions.length }, "Starting session migration to native mode") + + // Transform to the format expected by the native API + const sessionsToImport = sdkSessions.map(s => ({ + id: s.id, + title: s.title, + parentId: s.parentId, + createdAt: s.time?.created, + updatedAt: s.time?.updated, + model: s.model, + agent: s.agent, + messages: s.messages?.map(m => ({ + id: m.id, + role: m.role, + content: m.content, + createdAt: m.timestamp + })) + })) + + const result = await nativeSessionApi.importSessions(workspaceId, sessionsToImport) + + log.info({ workspaceId, ...result }, "Session migration completed") + markMigrated(workspaceId) + + return { + success: result.success, + imported: result.imported, + skipped: result.skipped + } + } catch (error) { + log.error({ workspaceId, error }, "Session migration failed") + return { + success: false, + imported: 0, + skipped: 0, + error: error instanceof Error ? error.message : String(error) + } + } +} + +/** + * Clear migration status (for testing or when user explicitly wants to re-migrate) + */ +export function clearMigrationStatus(workspaceId: string): void { + migratedWorkspaces.delete(workspaceId) +} + +/** + * Clear all migration statuses + */ +export function clearAllMigrationStatuses(): void { + migratedWorkspaces.clear() +}