diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts b/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts index ee8a32f28a2..b95d630610f 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/migration.ts @@ -114,15 +114,13 @@ export async function handleStartLegacyMigration( ctx.cachedLegacyData?.settings, ) - // Dispose all instances after migration - // Reloading the data will be handled once the server replies with a global.disposed event - await ctx.disposeGlobal() - - // Only mark as completed if at least one item succeeded — if everything failed - // the user can still re-run migration via Settings → About. + const failed = results.some((r) => r.status === "error") const success = results.some((r) => r.status === "success") - if (success) { + if (!failed && success) { + // Dispose all instances after a fully successful migration. + // Reloading the data will be handled once the server replies with a global.disposed event. + await ctx.disposeGlobal() await MigrationService.setMigrationStatus( ctx.extensionContext as Parameters[0], "completed", diff --git a/packages/kilo-vscode/src/legacy-migration/errors/migration-error.ts b/packages/kilo-vscode/src/legacy-migration/errors/migration-error.ts new file mode 100644 index 00000000000..a54892df80e --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/errors/migration-error.ts @@ -0,0 +1,83 @@ +interface ErrorLike { + message?: unknown + status?: unknown + data?: unknown + body?: unknown +} + +function isObject(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function getText(value: unknown): string | undefined { + if (typeof value === "string") { + const text = value.trim() + return text || undefined + } + + if (typeof value === "number" || typeof value === "boolean") { + return String(value) + } + + return undefined +} + +function getMessage(value: unknown) { + if (!isObject(value)) return undefined + return getText((value as ErrorLike).message) +} + +function getStatus(value: unknown) { + if (!isObject(value)) return undefined + const status = (value as ErrorLike).status + return typeof status === "number" ? String(status) : getText(status) +} + +function getBody(value: unknown) { + if (!isObject(value)) return undefined + + const body = (value as ErrorLike).body + const text = getText(body) + if (text) return text + + if (isObject(body)) { + const msg = getMessage(body) + if (msg) return msg + } + + return undefined +} + +function getData(value: unknown) { + if (!isObject(value)) return undefined + + const data = (value as ErrorLike).data + const text = getText(data) + if (text) return text + + if (isObject(data)) { + const msg = getMessage(data) + if (msg) return msg + } + + return undefined +} + +export function getMigrationErrorMessage(err: unknown) { + const message = getMessage(err) + if (message) return message + + const body = getBody(err) + if (body) return body + + const data = getData(err) + if (data) return data + + const status = getStatus(err) + if (status) return `Request failed (${status})` + + const text = getText(err) + if (text) return text + + return "Unknown migration error" +} diff --git a/packages/kilo-vscode/src/legacy-migration/legacy-types.ts b/packages/kilo-vscode/src/legacy-migration/legacy-types.ts index c5a9231d5ed..854a7655bfb 100644 --- a/packages/kilo-vscode/src/legacy-migration/legacy-types.ts +++ b/packages/kilo-vscode/src/legacy-migration/legacy-types.ts @@ -323,10 +323,3 @@ export interface MigrationSelections { defaultModel: boolean settings: MigrationSettingsSelections } - -export interface MigrationResultItem { - item: string - category: "provider" | "mcpServer" | "customMode" | "defaultModel" | "settings" | "session" - status: "success" | "warning" | "error" - message?: string -} diff --git a/packages/kilo-vscode/src/legacy-migration/migration-service.ts b/packages/kilo-vscode/src/legacy-migration/migration-service.ts index b3e4986aac8..1f4d4117204 100644 --- a/packages/kilo-vscode/src/legacy-migration/migration-service.ts +++ b/packages/kilo-vscode/src/legacy-migration/migration-service.ts @@ -16,6 +16,7 @@ import type { } from "@kilocode/sdk/v2/client" import { PROVIDER_MAP, UNSUPPORTED_PROVIDERS, DEFAULT_MODE_SLUGS } from "./provider-mapping" import type { ProviderMapping } from "./provider-mapping" +import { getMigrationErrorMessage } from "./errors/migration-error" import type { LegacyProviderProfiles, LegacyProviderSettings, @@ -30,8 +31,9 @@ import type { MigrationProviderInfo, MigrationMcpServerInfo, MigrationCustomModeInfo, - MigrationResultItem, } from "./legacy-types" +import type { MigrationResultItem } from "./migration-types" +import { createSessionID } from "./sessions/lib/ids" import { migrate as migrateSession } from "./sessions/migrate" // --------------------------------------------------------------------------- @@ -212,19 +214,16 @@ export async function migrate( if (selections.sessions?.length) { for (const id of selections.sessions) { - onProgress("Chat sessions", "migrating") + onProgress(id, "migrating") const result = await migrateSession(id, context, client) + const reason = result.ok ? "Session migrated" : result.message results.push({ item: id, category: "session", status: result.ok ? "success" : "error", - message: result.ok ? "Session migrated" : "Session migration failed", + message: reason, }) - onProgress( - "Chat sessions", - result.ok ? "success" : "error", - result.ok ? "Session migrated" : "Session migration failed", - ) + onProgress(id, result.ok ? "success" : "error", reason) } } @@ -666,7 +665,7 @@ async function migrateAutocomplete(settings: LegacyAutocompleteSettings): Promis item: "Autocomplete settings", category: "settings", status: "error", - message: err instanceof Error ? err.message : String(err), + message: getMigrationErrorMessage(err), } } } @@ -714,7 +713,7 @@ async function migrateLanguage(language: string): Promise { item: "Language preference", category: "settings", status: "error", - message: err instanceof Error ? err.message : String(err), + message: getMigrationErrorMessage(err), } } } diff --git a/packages/kilo-vscode/src/legacy-migration/migration-types.ts b/packages/kilo-vscode/src/legacy-migration/migration-types.ts new file mode 100644 index 00000000000..bd32d39ffd5 --- /dev/null +++ b/packages/kilo-vscode/src/legacy-migration/migration-types.ts @@ -0,0 +1,6 @@ +export interface MigrationResultItem { + item: string + category: "provider" | "mcpServer" | "customMode" | "defaultModel" | "settings" | "session" + status: "success" | "warning" | "error" + message?: string +} diff --git a/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts b/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts index 8ca481e3178..0fa38fdaf0d 100644 --- a/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts +++ b/packages/kilo-vscode/src/legacy-migration/sessions/migrate.ts @@ -1,9 +1,22 @@ import * as vscode from "vscode" import type { KiloClient } from "@kilocode/sdk/v2/client" +import { getMigrationErrorMessage } from "../errors/migration-error" import type { LegacyHistoryItem } from "./lib/legacy-types" import { parseSession } from "./parser" -export async function migrate(id: string, context: vscode.ExtensionContext, client: KiloClient) { +type Result = + | { + ok: true + skipped?: boolean + payload: Awaited> + } + | { + ok: false + payload: Awaited> + message: string + } + +export async function migrate(id: string, context: vscode.ExtensionContext, client: KiloClient): Promise { const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath const items = context.globalState.get("taskHistory", []) const item = items.find((item) => item.id === id) @@ -46,7 +59,7 @@ export async function migrate(id: string, context: vscode.ExtensionContext, clie return { ok: false, payload, - error, + message: getMigrationErrorMessage(error), } } } diff --git a/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx b/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx index 3cfda340070..deca1fdb570 100644 --- a/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/migration/MigrationWizard.tsx @@ -8,6 +8,7 @@ import { Show, createSignal, onMount, onCleanup } from "solid-js" import type { Component, JSX } from "solid-js" +import { showToast } from "@kilocode/kilo-ui/toast" import { useVSCode } from "../../context/vscode" import { useLanguage } from "../../context/language" import type { @@ -154,7 +155,7 @@ const WarningSvg = (): JSX.Element => ( // --------------------------------------------------------------------------- type Screen = "whats-new" | "migrate" -type MigratePhase = "selecting" | "migrating" | "done" +type MigratePhase = "selecting" | "migrating" | "error" | "done" interface ProgressEntry { item: string @@ -263,7 +264,11 @@ const MigrationWizard: Component = (props) => { if (msg?.type === "legacyMigrationComplete") { const complete = msg as LegacyMigrationCompleteMessage setResults(complete.results) - setPhase("done") + const hasErrors = complete.results.some((r) => r.status === "error") + setPhase(hasErrors ? "error" : "done") + if (!hasErrors) { + vscode.postMessage({ type: "loadSessions" }) + } } } @@ -323,8 +328,8 @@ const MigrationWizard: Component = (props) => { const mode = customModes().find((m) => m.slug === slug) return { item: mode?.name ?? slug, group: "customModes", status: "pending" as const } }), - ...(migrateSessions() && sessions().length > 0 - ? [{ item: "Chat sessions", group: "sessions", status: "pending" as const }] + ...(migrateSessions() + ? sessions().map((id) => ({ item: id, group: "sessions", status: "pending" as const })) : []), ...(migrateDefaultModel() && defaultModel() ? [{ item: "Default model", group: "defaultModel", status: "pending" as const }] @@ -382,6 +387,11 @@ const MigrationWizard: Component = (props) => { props.onComplete() } + const copySessionError = async (text: string) => { + await navigator.clipboard.writeText(text) + showToast({ variant: "success", title: language.t("migration.error.toast.copied") }) + } + // --------------------------------------------------------------------------- // Data helpers // --------------------------------------------------------------------------- @@ -431,17 +441,19 @@ const MigrationWizard: Component = (props) => { // Group-level status for progress display const groupStatus = (group: string): ProgressEntry["status"] => { - const entries = progressEntries().filter((e) => e.group === group) + const entries = progressEntries().filter((entry) => entry.group === group) if (entries.length === 0) return "pending" - if (entries.some((e) => e.status === "error")) return "error" - if (entries.some((e) => e.status === "warning")) return "warning" - if (entries.every((e) => e.status === "success")) return "success" - if (entries.some((e) => e.status === "migrating")) return "migrating" + if (entries.some((entry) => entry.status === "error")) return "error" + if (entries.some((entry) => entry.status === "warning")) return "warning" + if (entries.every((entry) => entry.status === "success")) return "success" + if (entries.some((entry) => entry.status === "migrating")) return "migrating" return "pending" } - const successCount = () => results().filter((r) => r.status === "success").length + const successCount = () => results().filter((result) => result.status === "success").length const totalCount = () => results().length + const groupMessage = (group: string) => + progressEntries().find((entry) => entry.group === group && entry.status === "error")?.message // --------------------------------------------------------------------------- // Status icon renderer @@ -677,8 +689,33 @@ const MigrationWizard: Component = (props) => {
-
Chat Sessions & History
-
{sessions().length} sessions detected
+
{language.t("migration.migrate.chatHistory")}
+
+ {language.t("migration.migrate.sessionsDetected", { count: String(sessions().length) })} +
+ +
+
+
+ {language.t("migration.error.sessionFailed")} +
+ +
+
{groupMessage("sessions")}
+
+
@@ -768,6 +805,7 @@ const MigrationWizard: Component = (props) => { {/* Cleanup option after done */} + {/*
@@ -787,6 +825,7 @@ const MigrationWizard: Component = (props) => {
+ */} @@ -818,6 +857,18 @@ const MigrationWizard: Component = (props) => { {language.t("migration.migrate.button")} + + +