feat(kilo-vscode): migrate legacy sessions into new extension (#7924)

* feat(vscode): detect sessions in old extension

* feat(vscode): added session types

* feat(cli): add endpoints for inserting a session

* feat(vscode): use session types from sdk for parsing

* feat(kilo-vscode): parsing session project

* feat(vscode): parse legacy session details

* feat(kilo-vscode): refactoring

* feat(vscode): parse legacy session messages

* feat(vscode): parse legacy session parts

* feat(vscode): added testing files and id creation

* feat(vscode): merge tool parts on imported session

* feat(vscode): session message part refactoring

* feat(vscode): cover provider-specific reasoning parts on session migration

* feat(kilo-vscode): handle completion result parts

* feat: type hardening on session migration

* feat(kilo-vscode): create tool part correctly

* feat(kilo-vscode): adjust session part processing

* feat(vscode): persist migrated session in kilo.db

* feat(vscode): add error handling to session migration

* feat(vscode): add directory field to session migration

* feat(kilo-vscode): display completion check on session migration

* feat(vscode): prevent migrating sessions twice

* feat(cli): use correct project id when inserting migrated session

* feat(kilo-vscode): Use correct project id for session migration

* feat(kilo-vscode): load sessions on migration complete

* feat(kilo-vscode): ignore environment details user part when migrating sessions

* feat(cli): create project details following current backend logic

* feat(kilo-vscode): cleaner messages without environment_details parts

* feat(kilo-vscode): refactoring

* feat(kilo-vscode): refactoring and organization

* feat(kilo-vscode): method renaming to improve clarity

* feat(kilo-vscode): added session migration test strategy

* test(kilo-vscode): implement legacy migration unit tests for all migration modules

Replace todo stubs with full test implementations across messages,
parts, tools, reasoning, parser, migrate, project, and session test
files. Add new project.test.ts and session.test.ts covering project
construction, session metadata, and deterministic ID generation.

* style(kilo-vscode): apply prettier formatting across legacy migration source and test files

* fix(legacy-migration): filter out non-user/assistant entries before parsing messages

Exclude system and other non-conversational roles from the conversation
array prior to mapping, ensuring parentID references remain consistent
across imported messages when skipped entries exist.

* refactor(parts): skip part generation for non-conversational message roles

Entries with roles other than user or assistant should not produce
parts, keeping part output consistent with the messages that are
actually imported during legacy migration.

* fix(ids): use semantic kind strings to prevent part ID collisions for reasoning entries

Replace numeric indices with descriptive kind strings ("reasoning",
"provider-reasoning") in extra part ID generation to avoid collisions
between reasoning parts and regular content parts within the same
message.

Introduce `createExtraPartID` to distinguish reasoning-specific IDs
from standard sequential part IDs produced by `createPartID`.

* refactor(session-import): guard against empty worktree in legacy project import

Prevent silent misattribution of migrated sessions by rejecting empty
worktree paths before resolving the project directory. Without this
check, an empty string would fall back to the current process directory,
causing the imported session to be attached to the wrong project.

* perf(cli): cache MCP listTools results to avoid redundant RPCs per loop step (#7922)

MCP.tools() called client.listTools() for every connected server on
every agent loop iteration. With multiple servers this added hundreds
of milliseconds of overhead per step (measured ~300-750ms for remote
servers like Vercel).

Cache listTools results in a module-level Map and return them on
subsequent calls. The cache is invalidated on:
- ToolListChangedNotification from the MCP server
- Server add/connect/disconnect
- Instance dispose (config change, project switch, shutdown)

* fix(agent-manager): add retries to worktree cleanup for macOS .app bundles (#7921)

* chore: hide non-English localization files in GitHub PR diffs

Mark all non-English i18n locale files as linguist-generated=true so
they are collapsed by default in GitHub PR diffs, reducing noise when
reviewing changes that touch translation files.

* feat(vscode): add Browse Marketplace button to MCP settings

Add a "Browse Marketplace" button to the MCP Servers subtab in Agent
Behaviour settings, guiding users to discover and install MCPs from the
marketplace rather than only configuring them manually.

- Add button above the MCP server list that opens the Marketplace panel
- Add openMarketplacePanel webview message type and handler
- Add translations for all 18 supported languages

* fix(vscode): preserve project directory when opening marketplace from settings

Pass the current panel's project directory through to the marketplace
panel so project-scoped MCP installs work correctly in multi-root
workspaces. Without this, the marketplace panel would fall back to
null project directory since there is no active text editor when
clicking from a webview.

* feat(vscode): add Browse Marketplace button to agents and skills tabs

Add the Browse Marketplace button to the Agents and Skills subtabs in
Agent Behaviour settings, matching the existing button in the MCP
Servers subtab. This lets users discover marketplace items from any
of the three configuration tabs.

- Agents tab: added as a ghost button in the header row alongside
  Import and Create Mode buttons
- Skills tab: added as a secondary button at the top-right, matching
  the MCP tab layout
- Extracted the browse handler to the component scope for reuse

* chore: update kilo-vscode visual regression baselines

* perf(agent-manager): extend slimPart to strip heavy tool metadata for faster session switching (#7927)

Extend slim-metadata.ts to handle apply_patch, multiedit, write, and bash
tools in addition to edit. Previously only edit tool parts had their heavy
metadata stripped before IPC serialization. apply_patch parts carried full
file before/after contents per file, write parts carried the entire written
file, and bash parts carried up to 30KB of output — all serialized on every
session switch.

This reduces session-switch time by ~2x for sessions with many tool calls.

* style(legacy-migration): reformat long reasoning push call for readability

* refactor(legacy-migration): filter non-user/assistant entries before indexing

Move the role filter from inside `parseParts` to `parsePartsFromConversation`
so that message indices are derived only from the filtered list. This ensures
part message IDs stay aligned with the imported messages and skipped entries
(e.g. system role) do not cause index gaps in the generated IDs.

* fix(legacy-migration): add `_migrated_` infix to generated IDs

Prefix all migrated entity IDs (sessions, messages, parts) with a
`_migrated_` segment so they are clearly distinguishable from IDs
produced by the regular runtime. Updates tests to assert the new
prefix format.

* refactor(legacy-migration): consolidate conversation parsing into `parseSession`

Remove the intermediate `createMessages` and `createParts` async
wrappers that each independently read and parsed the conversation
file. The file is now fetched and parsed once in `parseSession`,
with the resulting conversation passed directly to
`parseMessagesFromConversation` and `parsePartsFromConversation`,
eliminating redundant I/O.

* fix(parts-builder): remove `toTextWithinMessage` and use `toText` directly

`toTextWithinMessage` was a duplicate of `toText` with identical
behavior. Replace all call sites with `toText` and delete the
redundant function.

* fix(parts-builder): skip provider-specific reasoning when explicit reasoning entry exists

Guard `isProviderSpecificReasoning` with `!isReasoning` to prevent
duplicate reasoning parts when an entry carries both a `type:
"reasoning"` field and a provider-specific reasoning field (e.g.
`reasoning_content`). The explicit reasoning entry takes precedence.

* test(legacy-migration): add coverage for edge cases in parts and tools parsing

Add unit tests for two previously uncovered scenarios:

- Task block extraction: verify only the content inside `<task>` tags
  is kept when text exists outside the legacy task wrapper
- Empty tool result: verify the tool name is used as fallback output
  when `tool_result` carries no readable text content

Also replace the dynamic `FileType.Directory` workaround with the
direct `vscode.FileType.Directory` enum reference, and harden
`parseFile` to throw on non-array JSON instead of silently returning
an empty array.

* feat(legacy-migration): remove session migration tracking from global state

Drop the `readSessionsToMigrate` filter and the post-migration
`globalState` flag that tracked which sessions had already been
migrated. Detection now returns all sessions found in global storage
without filtering by prior migration status.

* fix(session-import): skip duplicate session imports and child data

Check for an existing session row before inserting and return a
`skipped: true` flag when the session already exists. The migration
client honours this flag to bypass message and part imports, avoiding
redundant work when migration is re-run.

Replace `onConflictDoUpdate` with `onConflictDoNothing` on the session
insert path to align with the new pre-check behaviour. Propagate the
optional `skipped` field through the result types and generated SDK
types for project, session, message, and part responses.

* refactor(parts-builder): mark legacy system error text parts as ignored

Tag text parts whose content begins with `[ERROR]` with `ignored: true`
and a `legacy-system-error` metadata source. Introduces a new
`isLegacySystemErrorText` utility in `parts-util` to encapsulate the
detection logic.

* feat(parts-util): extract feedback text from tool_result content into visible parts

Parse `<feedback>` tags embedded in `tool_result` content and emit a
dedicated text part so user feedback is surfaced as a visible message
rather than buried inside tool result data.

Introduces `getFeedbackText` utility to extract and trim the inner
content of `<feedback>` blocks from arbitrary input.

* fix: formatting

---------

Co-authored-by: Marius <marius@kilocode.ai>
Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
Co-authored-by: Mark IJbema <mark@kilocode.ai>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Imanol Maiztegui
2026-03-31 17:13:34 +02:00
committed by GitHub
co-authored by Marius kiloconnect[bot] Mark IJbema github-actions[bot]
parent 0d00e41da8
commit 80f187ebba
33 changed files with 3813 additions and 21 deletions
+1
View File
@@ -2802,6 +2802,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
set migrationCheckInFlight(val) {
self.migrationCheckInFlight = val
},
refreshSessions: () => this.refreshSessions(),
disposeGlobal: () => this.disposeGlobal(),
broadcastComplete: () => this.connectionService.notifyMigrationComplete(),
}
@@ -28,6 +28,7 @@ export interface MigrationContext {
readonly client: KiloClient | null
readonly extensionContext: MigrationExtensionContext | undefined
postMessage(msg: unknown): void
refreshSessions(): void
cachedLegacyData: LegacyMigrationData | null
migrationCheckInFlight: boolean
disposeGlobal(): Promise<void>
@@ -69,6 +70,7 @@ export async function checkAndShowMigrationWizard(ctx: MigrationContext): Promis
providers: data.providers,
mcpServers: data.mcpServers,
customModes: data.customModes,
sessions: data.sessions,
defaultModel: data.defaultModel,
settings: data.settings,
},
@@ -88,6 +90,7 @@ export async function handleRequestLegacyMigrationData(ctx: MigrationContext): P
providers: data.providers,
mcpServers: data.mcpServers,
customModes: data.customModes,
sessions: data.sessions,
defaultModel: data.defaultModel,
settings: data.settings,
},
@@ -125,6 +128,7 @@ export async function handleStartLegacyMigration(
"completed",
)
ctx.broadcastComplete()
ctx.refreshSessions()
}
ctx.postMessage({ type: "legacyMigrationComplete", results })
@@ -288,6 +288,7 @@ export interface LegacyMigrationData {
providers: MigrationProviderInfo[]
mcpServers: MigrationMcpServerInfo[]
customModes: MigrationCustomModeInfo[]
sessions?: string[]
defaultModel?: { provider: string; model: string }
settings?: LegacySettings
hasData: boolean
@@ -318,13 +319,14 @@ export interface MigrationSelections {
providers: string[]
mcpServers: string[]
customModes: string[]
sessions?: string[]
defaultModel: boolean
settings: MigrationSettingsSelections
}
export interface MigrationResultItem {
item: string
category: "provider" | "mcpServer" | "customMode" | "defaultModel" | "settings"
category: "provider" | "mcpServer" | "customMode" | "defaultModel" | "settings" | "session"
status: "success" | "warning" | "error"
message?: string
}
@@ -32,6 +32,7 @@ import type {
MigrationCustomModeInfo,
MigrationResultItem,
} from "./legacy-types"
import { migrate as migrateSession } from "./sessions/migrate"
// ---------------------------------------------------------------------------
// Constants
@@ -68,6 +69,7 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis
const mcpSettings = await readLegacyMcpSettings(context)
const customModes = await readLegacyCustomModes(context)
const settings = readLegacySettings(context)
const sessions = await readSessionsInGlobalStorage(context)
const oauthProviders = new Set<string>()
const codexRaw = await context.secrets.get(CODEX_OAUTH_SECRET_KEY)
@@ -92,18 +94,29 @@ export async function detectLegacyData(context: vscode.ExtensionContext): Promis
Boolean(settings.language) ||
Boolean(settings.autocomplete)
const hasData = providers.length > 0 || mcpServers.length > 0 || modes.length > 0 || hasSettings
const hasData =
providers.length > 0 || mcpServers.length > 0 || modes.length > 0 || hasSettings || sessions.length > 0
return {
providers,
mcpServers,
customModes: modes,
sessions: sessions.length > 0 ? sessions : undefined,
defaultModel,
settings: hasSettings ? settings : undefined,
hasData,
}
}
async function readSessionsInGlobalStorage(context: vscode.ExtensionContext) {
const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks")
const items = await vscode.workspace.fs.readDirectory(dir).then(
(items) => items,
() => [] as [string, vscode.FileType][],
)
return items.filter(([, type]) => type === vscode.FileType.Directory).map(([name]) => name)
}
// ---------------------------------------------------------------------------
// Migration
// ---------------------------------------------------------------------------
@@ -197,6 +210,24 @@ export async function migrate(
}
}
if (selections.sessions?.length) {
for (const id of selections.sessions) {
onProgress("Chat sessions", "migrating")
const result = await migrateSession(id, context, client)
results.push({
item: id,
category: "session",
status: result.ok ? "success" : "error",
message: result.ok ? "Session migrated" : "Session migration failed",
})
onProgress(
"Chat sessions",
result.ok ? "success" : "error",
result.ok ? "Session migrated" : "Session migration failed",
)
}
}
// Migrate default model
if (selections.defaultModel && profiles) {
const activeName = profiles.currentApiConfigName
@@ -0,0 +1,29 @@
import { createHash } from "node:crypto"
export function createProjectID(worktree?: string) {
return hash(worktree ?? "")
}
export function createSessionID(id: string) {
return prefixed("ses", id)
}
export function createMessageID(id: string, index: number) {
return prefixed("msg", `${id}:${index}`)
}
export function createPartID(id: string, index: number, part: number) {
return prefixed("prt", `${id}:${index}:${part}`)
}
export function createExtraPartID(id: string, index: number, kind: string) {
return prefixed("prt", `${id}:${index}:${kind}`)
}
function prefixed(prefix: string, value: string) {
return `${prefix}_migrated_${hash(value).slice(0, 26)}`
}
function hash(value: string) {
return createHash("sha1").update(value).digest("hex")
}
@@ -0,0 +1,17 @@
import * as path from "node:path"
import * as vscode from "vscode"
import type { LegacyApiMessage } from "./legacy-types"
export async function getApiConversationHistory(id: string, dir: string) {
const file = path.join(dir, id, "api_conversation_history.json")
return vscode.workspace.fs.readFile(vscode.Uri.file(file))
}
export function parseFile(file: Uint8Array): LegacyApiMessage[] {
const text = Buffer.from(file).toString("utf8")
const json = JSON.parse(text) as unknown
if (!Array.isArray(json)) {
throw new Error("Legacy conversation history must be a JSON array")
}
return json.filter((entry): entry is LegacyApiMessage => Boolean(entry && typeof entry === "object"))
}
@@ -0,0 +1,28 @@
import type { Anthropic } from "@anthropic-ai/sdk"
export interface LegacyHistoryItem {
id: string
rootTaskId?: string
parentTaskId?: string
ts?: number
task?: string
workspace?: string
mode?: string
}
export type LegacyApiMessage = Anthropic.MessageParam & {
ts?: number
isSummary?: boolean
id?: string
type?: "reasoning"
summary?: unknown[]
encrypted_content?: string
text?: string
reasoning_details?: unknown[]
reasoning_content?: string
condenseId?: string
condenseParent?: string
truncationId?: string
truncationParent?: string
isTruncationMarker?: boolean
}
@@ -0,0 +1,82 @@
import type { KilocodeSessionImportMessageData as Message } from "@kilocode/sdk/v2"
import type { LegacyApiMessage, LegacyHistoryItem } from "./legacy-types"
import { createMessageID, createSessionID } from "./ids"
type Body = NonNullable<Message["body"]>
type Data = Body["data"]
type User = Extract<Data, { role: "user" }>
type Assistant = Extract<Data, { role: "assistant" }>
export function parseMessagesFromConversation(
conversation: LegacyApiMessage[],
id: string,
item?: LegacyHistoryItem,
): Array<NonNullable<Message["body"]>> {
return conversation
.filter((entry) => entry.role === "user" || entry.role === "assistant")
.map((entry, index) => parseMessage(entry, index, id, item))
.filter((message): message is NonNullable<Message["body"]> => Boolean(message))
}
function parseMessage(
entry: LegacyApiMessage,
index: number,
id: string,
item?: LegacyHistoryItem,
): NonNullable<Message["body"]> | undefined {
const created = entry.ts ?? item?.ts ?? 0
if (entry.role === "user") {
const data: User = {
role: "user",
time: { created },
agent: "user",
model: {
providerID: "legacy",
modelID: "legacy",
},
}
return {
id: createMessageID(id, index),
sessionID: createSessionID(id),
timeCreated: created,
data,
}
}
if (entry.role === "assistant") {
const data: Assistant = {
role: "assistant",
time: { created, completed: created },
parentID: index > 0 ? createMessageID(id, index - 1) : createMessageID(id, index),
modelID: "legacy",
providerID: "legacy",
mode: item?.mode ?? "code",
agent: "main",
path: {
cwd: item?.workspace ?? "",
root: item?.workspace ?? "",
},
cost: 0,
tokens: {
input: 0,
output: 0,
reasoning: 0,
cache: {
read: 0,
write: 0,
},
},
}
return {
id: createMessageID(id, index),
sessionID: createSessionID(id),
timeCreated: created,
data,
}
}
return undefined
}
@@ -0,0 +1,66 @@
import type { KilocodeSessionImportPartData as Part } from "@kilocode/sdk/v2"
import type { LegacyApiMessage } from "../legacy-types"
import { getText, getToolUse, isToolResult, record } from "./parts-util"
type Body = NonNullable<Part["body"]>
type Data = Body["data"]
type Tool = Extract<Data, { type: "tool" }>
type ToolCompleted = Extract<Tool["state"], { status: "completed" }>
// This takes the "tool started" half and the matching "tool finished" half,
// and merges both so the migrated session keeps one complete tool action.
export function mergeToolUseAndResult(
partID: string,
messageID: string,
sessionID: string,
created: number,
conversation: LegacyApiMessage[],
result: { type?: string; tool_use_id?: string; content?: unknown },
): NonNullable<Part["body"]> | undefined {
const tool = getToolUseFromConversation(conversation, result.tool_use_id)
if (!tool) return undefined
const callID = typeof tool.id === "string" ? tool.id : partID
const name = typeof tool.name === "string" ? tool.name : "unknown"
const output = getText(result.content) ?? name
const state: ToolCompleted = {
status: "completed",
input: record(tool.input),
output,
title: name,
metadata: {},
time: {
start: created,
end: created,
},
}
const data: Tool = {
type: "tool",
callID,
tool: name,
state,
}
return {
id: partID,
messageID,
sessionID,
timeCreated: created,
data,
}
}
export function thereIsNoToolResult(conversation: LegacyApiMessage[], id: string | undefined) {
return !conversation.some(
(entry) =>
Array.isArray(entry.content) && entry.content.some((part) => isToolResult(part) && part.tool_use_id === id),
)
}
function getToolUseFromConversation(conversation: LegacyApiMessage[], id: string | undefined) {
for (const entry of conversation) {
const tool = getToolUse(entry, id)
if (tool) return tool
}
return undefined
}
@@ -0,0 +1,102 @@
import type { KilocodeSessionImportPartData as Part } from "@kilocode/sdk/v2"
import { cleanLegacyTaskText, isLegacySystemErrorText, record } from "./parts-util"
type Body = NonNullable<Part["body"]>
type Data = Body["data"]
type Text = Extract<Data, { type: "text" }>
type Reasoning = Extract<Data, { type: "reasoning" }>
type Tool = Extract<Data, { type: "tool" }>
type ToolCompleted = Extract<Tool["state"], { status: "completed" }>
export function toTool(
partID: string,
messageID: string,
sessionID: string,
created: number,
part: { type?: string; id?: string; name?: string; input?: unknown },
): NonNullable<Part["body"]> {
const tool = typeof part.name === "string" ? part.name : "unknown"
const state: ToolCompleted = {
status: "completed",
input: record(part.input),
output: tool,
title: tool,
metadata: {},
time: {
start: created,
end: created,
},
}
const data: Tool = {
type: "tool",
callID: part.id ?? partID,
tool,
state,
}
return {
id: partID,
messageID,
sessionID,
timeCreated: created,
data,
}
}
export function toText(
partID: string,
messageID: string,
sessionID: string,
created: number,
text: string,
): NonNullable<Part["body"]> {
const value = cleanLegacyTaskText(text)
const data: Text = {
type: "text",
text: value,
...(isLegacySystemErrorText(value) && {
ignored: true,
metadata: {
source: "legacy-system-error",
},
}),
time: {
start: created,
end: created,
},
}
return {
id: partID,
messageID,
sessionID,
timeCreated: created,
data,
}
}
export function toReasoning(
partID: string,
messageID: string,
sessionID: string,
created: number,
text: string,
): NonNullable<Part["body"]> {
const data: Reasoning = {
type: "reasoning",
text,
time: {
start: created,
end: created,
},
}
return {
id: partID,
messageID,
sessionID,
timeCreated: created,
data,
}
}
@@ -0,0 +1,132 @@
import type { LegacyApiMessage } from "../legacy-types"
type ToolUse = {
type?: string
id?: string
name?: string
input?: unknown
}
export function isSimpleText(input: LegacyApiMessage): input is LegacyApiMessage & { content: string } {
return typeof input.content === "string" && Boolean(input.content)
}
export function isReasoning(input: LegacyApiMessage): input is LegacyApiMessage & { type: "reasoning"; text: string } {
return input.type === "reasoning" && typeof input.text === "string" && Boolean(input.text)
}
export function isProviderSpecificReasoning(input: LegacyApiMessage) {
return Boolean(getReasoningText(input))
}
export function getReasoningText(input: LegacyApiMessage) {
if (typeof input.reasoning_content === "string" && input.reasoning_content.trim()) {
// Some providers store the model thinking outside normal content blocks, so we need to lift it manually.
return input.reasoning_content.trim()
}
if (!Array.isArray(input.reasoning_details)) return undefined
const text = input.reasoning_details
.flatMap((item) => {
if (!item || typeof item !== "object") return []
if (typeof (item as { text?: unknown }).text === "string") return [(item as { text: string }).text]
if (typeof (item as { reasoning?: unknown }).reasoning === "string")
return [(item as { reasoning: string }).reasoning]
return []
})
.join("\n")
.trim()
// reasoning_details can come as provider-specific arrays, so we collapse the readable text we can find.
return text || undefined
}
export function isSingleTextWithinMessage(input: unknown): input is { type?: string; text: string } {
return isText(input) && Boolean(input.text)
}
export function isEnvironmentDetails(input: string) {
return /^\s*<environment_details>[\s\S]*<\/environment_details>\s*$/i.test(input)
}
export function isCompletionResult(
input: unknown,
): input is { type?: string; name?: string; input: { result: string } } {
return Boolean(
input &&
typeof input === "object" &&
"type" in input &&
input.type === "tool_use" &&
"name" in input &&
input.name === "attempt_completion" &&
"input" in input &&
input.input &&
typeof input.input === "object" &&
"result" in input.input &&
typeof input.input.result === "string" &&
input.input.result,
)
}
export function record(input: unknown): Record<string, unknown> {
if (!input || typeof input !== "object" || Array.isArray(input)) return {}
return input as Record<string, unknown>
}
export function isToolUse(input: unknown): input is { type?: string; id?: string; name?: string; input?: unknown } {
return Boolean(input && typeof input === "object" && "type" in input && input.type === "tool_use")
}
export function isText(input: unknown): input is { type?: string; text?: string } {
return Boolean(input && typeof input === "object" && "type" in input && input.type === "text")
}
export function isToolResult(input: unknown): input is { type?: string; tool_use_id?: string; content?: unknown } {
return Boolean(input && typeof input === "object" && "type" in input && input.type === "tool_result")
}
// This looks through the blocks inside one legacy message and finds the tool_use whose id
// matches the tool_result we are processing, so we know which tool call produced that result.
export function getToolUse(input: LegacyApiMessage, id: string | undefined) {
if (!Array.isArray(input.content)) return undefined
return input.content.find((part) => isToolUse(part) && part.id === id) as ToolUse | undefined
}
export function getText(input: unknown) {
if (typeof input === "string") return input
if (!Array.isArray(input)) return undefined
const text = input
.flatMap((item) => {
if (isText(item) && item.text) return [item.text]
return []
})
.join("\n")
.trim()
return text || undefined
}
export function cleanLegacyTaskText(input: string) {
// Legacy sometimes stores the real user prompt wrapped inside <task>...</task>, followed by
// extra <environment_details>...</environment_details> prompt scaffolding. We only want the
// actual task text to appear in the migrated conversation, so if a <task> block exists we keep
// just that inner text and drop the wrapper plus the extra environment block.
const task = input.match(/<task>([\s\S]*?)<\/task>/i)?.[1]?.trim()
if (task) return task
if (isEnvironmentDetails(input)) return ""
return input
}
export function isLegacySystemErrorText(input: string) {
return input.trimStart().startsWith("[ERROR]")
}
export function getFeedbackText(input: unknown) {
const text = getText(input)
if (!text) return undefined
const match = text.match(/<feedback>([\s\S]*?)<\/feedback>/i)
const value = match?.[1]?.trim()
return value || undefined
}
@@ -0,0 +1,107 @@
import type { KilocodeSessionImportPartData as Part } from "@kilocode/sdk/v2"
import type { LegacyApiMessage, LegacyHistoryItem } from "../legacy-types"
import { createExtraPartID, createMessageID, createPartID, createSessionID } from "../ids"
import { toReasoning, toText, toTool } from "./parts-builder"
import {
getFeedbackText,
isCompletionResult,
isEnvironmentDetails,
getReasoningText,
isProviderSpecificReasoning,
isReasoning,
isSimpleText,
isSingleTextWithinMessage,
isToolResult,
isToolUse,
} from "./parts-util"
import { mergeToolUseAndResult, thereIsNoToolResult } from "./merge-tools"
export function parsePartsFromConversation(
conversation: LegacyApiMessage[],
id: string,
item?: LegacyHistoryItem,
): Array<NonNullable<Part["body"]>> {
const list = conversation.filter((entry) => entry.role === "user" || entry.role === "assistant")
return list.flatMap((entry, index) => parseParts(entry, index, id, list, item))
}
function parseParts(
entry: LegacyApiMessage,
index: number,
id: string,
conversation: LegacyApiMessage[],
item?: LegacyHistoryItem,
): Array<NonNullable<Part["body"]>> {
const messageID = createMessageID(id, index)
const sessionID = createSessionID(id)
const created = entry.ts ?? item?.ts ?? 0
if (isSimpleText(entry)) {
// Ignore raw <environment_details> blocks because they are legacy prompt scaffolding,
// not actual user-visible conversation content we want to preserve in the migrated session.
if (isEnvironmentDetails(entry.content)) return []
return [toText(createPartID(id, index, 0), messageID, sessionID, created, entry.content)]
}
if (!Array.isArray(entry.content)) return []
const parts: Array<NonNullable<Part["body"]>> = []
if (isReasoning(entry)) {
parts.push(toReasoning(createExtraPartID(id, index, "reasoning"), messageID, sessionID, created, entry.text))
}
// Some providers store thinking outside normal content blocks, so this handles those provider-specific fields.
if (!isReasoning(entry) && isProviderSpecificReasoning(entry)) {
const reasoning = getReasoningText(entry)
if (reasoning) {
parts.push(
toReasoning(createExtraPartID(id, index, "provider-reasoning"), messageID, sessionID, created, reasoning),
)
}
}
entry.content.forEach((part, partIndex) => {
const partID = createPartID(id, index, partIndex)
// Legacy can store a message as several pieces; this handles one text block inside that larger message.
if (isSingleTextWithinMessage(part)) {
// Ignore standalone <environment_details> text blocks for the same reason: they describe
// editor/runtime context for the old prompt, but they are not meaningful chat content.
if (isEnvironmentDetails(part.text)) return
parts.push(toText(partID, messageID, sessionID, created, part.text))
return
}
// The legacy session can contain a final completion message after an assistant interaction.
// Treat it like a regular assistant text part so the migrated session keeps that final visible answer.
if (isCompletionResult(part)) {
const text = part.input.result
parts.push(toText(partID, messageID, sessionID, created, text))
return
}
if (isToolUse(part) && thereIsNoToolResult(conversation, part.id)) {
parts.push(toTool(partID, messageID, sessionID, created, part))
return
}
if (isToolResult(part)) {
const feedback = getFeedbackText(part.content)
if (feedback) {
parts.push(
toText(createExtraPartID(id, index, `feedback-${partIndex}`), messageID, sessionID, created, feedback),
)
}
// tool_result usually lives in the following user message, while the matching tool_use lives
// in the earlier assistant message, so we need the whole conversation to reconcile both halves.
const tool = mergeToolUseAndResult(partID, messageID, sessionID, created, conversation, part)
if (!tool) return
parts.push(tool)
return
}
})
return parts
}
@@ -0,0 +1,29 @@
import type { KilocodeSessionImportProjectData as Project } from "@kilocode/sdk/v2"
import type { LegacyHistoryItem } from "./legacy-types"
import { createProjectID } from "./ids"
export function createProject(item?: LegacyHistoryItem): NonNullable<Project["body"]> {
const project = makeProject()
project.id = createProjectID(item?.workspace)
project.worktree = item?.workspace ?? ""
project.sandboxes = item?.workspace ? [item.workspace] : []
project.timeCreated = item?.ts ?? 0
project.timeUpdated = item?.ts ?? 0
return project
}
function makeProject(): NonNullable<Project["body"]> {
return {
id: "",
worktree: "",
sandboxes: [],
timeCreated: 0,
timeUpdated: 0,
}
}
@@ -0,0 +1,42 @@
import type { KilocodeSessionImportSessionData as Session } from "@kilocode/sdk/v2"
import type { LegacyHistoryItem } from "./legacy-types"
import { createSessionID } from "./ids"
export function createSession(
id: string,
item: LegacyHistoryItem | undefined,
projectID: string,
): NonNullable<Session["body"]> {
const session = makeSession()
session.id = createSessionID(id)
session.projectID = projectID
session.slug = id
session.directory = item?.workspace ?? ""
session.title = item?.task ?? id
session.version = "v2"
session.timeCreated = item?.ts ?? 0
session.timeUpdated = item?.ts ?? 0
return session
}
function makeSession(): NonNullable<Session["body"]> {
return {
id: "",
projectID: "",
slug: "",
directory: "",
title: "",
version: "",
timeCreated: 0,
timeUpdated: 0,
}
}
@@ -0,0 +1,52 @@
import * as vscode from "vscode"
import type { KiloClient } from "@kilocode/sdk/v2/client"
import type { LegacyHistoryItem } from "./lib/legacy-types"
import { parseSession } from "./parser"
export async function migrate(id: string, context: vscode.ExtensionContext, client: KiloClient) {
const dir = vscode.Uri.joinPath(context.globalStorageUri, "tasks").fsPath
const items = context.globalState.get<LegacyHistoryItem[]>("taskHistory", [])
const item = items.find((item) => item.id === id)
const payload = await parseSession(id, dir, item)
try {
const project = await client.kilocode.sessionImport.project(payload.project, { throwOnError: true })
const projectID = project.data?.id ?? payload.project.id
const session = await client.kilocode.sessionImport.session(
{
...payload.session,
projectID,
query_directory: payload.session.directory,
body_directory: payload.session.directory,
},
{ throwOnError: true },
)
// Skip child imports when the session already exists so rerunning migration only imports missing sessions.
if (session.data?.skipped) {
return {
ok: true,
skipped: true,
payload,
}
}
for (const msg of payload.messages) {
await client.kilocode.sessionImport.message(msg, { throwOnError: true })
}
for (const part of payload.parts) {
await client.kilocode.sessionImport.part(part, { throwOnError: true })
}
return {
ok: true,
payload,
}
} catch (error) {
return {
ok: false,
payload,
error,
}
}
}
@@ -0,0 +1,35 @@
import type { LegacyHistoryItem } from "./lib/legacy-types"
import type {
KilocodeSessionImportMessageData as Message,
KilocodeSessionImportPartData as Part,
KilocodeSessionImportProjectData as Project,
KilocodeSessionImportSessionData as Session,
} from "@kilocode/sdk/v2"
import { getApiConversationHistory, parseFile } from "./lib/legacy-conversation"
import { parseMessagesFromConversation } from "./lib/messages"
import { parsePartsFromConversation } from "./lib/parts/parts"
import { createProject } from "./lib/project"
import { createSession } from "./lib/session"
export interface NormalizedSession {
project: NonNullable<Project["body"]>
session: NonNullable<Session["body"]>
messages: Array<NonNullable<Message["body"]>>
parts: Array<NonNullable<Part["body"]>>
}
export async function parseSession(id: string, dir: string, item?: LegacyHistoryItem): Promise<NormalizedSession> {
const project = createProject(item)
const session = createSession(id, item, project.id)
const file = await getApiConversationHistory(id, dir)
const conversation = parseFile(file)
const messages = parseMessagesFromConversation(conversation, id, item)
const parts = parsePartsFromConversation(conversation, id, item)
return {
project,
session,
messages,
parts,
}
}
@@ -0,0 +1,106 @@
import { describe, expect, it } from "bun:test"
import { parseMessagesFromConversation } from "../../../src/legacy-migration/sessions/lib/messages"
import type { LegacyApiMessage } from "../../../src/legacy-migration/sessions/lib/legacy-types"
const id = "019d3df5-d5d9-73dc-bc2c-43a6304ac62c"
const item = {
id,
ts: 1774861014564,
task: "In this folder I need you to create 3 python files with random content, oriented towards web development",
workspace: "/workspace/testing-4",
mode: "code",
}
function sample(): LegacyApiMessage[] {
return [
{
role: "user",
content: [
{
type: "text",
text: "<task>\nIn this folder I need you to create 3 python files with random content, oriented towards web development\n</task>",
},
],
ts: 1774861014564,
},
{
role: "assistant",
content: [
{
type: "text",
text: "\n\nI'll create 3 Python files with web development-oriented content.",
},
],
ts: 1774861031791,
},
{
role: "system",
content: "ignored",
ts: 1774861032000,
} as unknown as LegacyApiMessage,
]
}
describe("legacy migration messages", () => {
it("parses a basic legacy conversation into ordered user and assistant messages with stable ids", () => {
const list = parseMessagesFromConversation(sample(), id, item)
expect(list).toHaveLength(2)
expect(list[0]?.data.role).toBe("user")
expect(list[1]?.data.role).toBe("assistant")
expect(list[0]?.id).toBeDefined()
expect(list[1]?.id).toBeDefined()
})
it("creates valid assistant message metadata for the SDK/backend shape", () => {
const list = parseMessagesFromConversation(sample(), id, item)
const msg = list.find((x) => x.data.role === "assistant")
expect(msg?.data.role).toBe("assistant")
if (msg?.data.role !== "assistant") throw new Error("assistant message not found")
expect(msg.data.mode).toBe("code")
expect(msg.data.path.cwd).toBe("/workspace/testing-4")
expect(msg.data.path.root).toBe("/workspace/testing-4")
expect(msg.data.tokens.input).toBe(0)
expect(msg.data.tokens.output).toBe(0)
})
it("ignores unsupported legacy entries instead of producing broken messages", () => {
const list = parseMessagesFromConversation(sample(), id, item)
expect(list).toHaveLength(2)
expect(list.some((x) => x.data.role !== "user" && x.data.role !== "assistant")).toBe(false)
})
it("keeps assistant parentID pointing to the previous imported message when skipped entries exist", () => {
const list = parseMessagesFromConversation(
[
{
role: "user",
content: "hello",
ts: 1,
},
{
role: "system",
content: "ignored",
ts: 2,
} as unknown as LegacyApiMessage,
{
role: "assistant",
content: "hi",
ts: 3,
},
],
id,
item,
)
const user = list[0]
const assistant = list[1]
expect(user?.id).toBeDefined()
expect(assistant?.data.role).toBe("assistant")
if (assistant?.data.role !== "assistant") throw new Error("assistant message not found")
expect(assistant.data.parentID).toBe(user?.id)
})
})
@@ -0,0 +1,212 @@
import { beforeEach, describe, expect, it, mock } from "bun:test"
const parseSession = mock(async () => ({
project: {
id: "project-local",
worktree: "/workspace/testing",
sandboxes: ["/workspace/testing"],
timeCreated: 1,
timeUpdated: 1,
},
session: {
id: "ses_local",
projectID: "project-local",
slug: "legacy-task-1",
directory: "/workspace/testing",
title: "Legacy task",
version: "v2",
timeCreated: 1,
timeUpdated: 1,
},
messages: [],
parts: [],
}))
mock.module("../../../src/legacy-migration/sessions/parser", () => ({
parseSession,
}))
const { migrate } = await import("../../../src/legacy-migration/sessions/migrate")
function ctx() {
return {
globalStorageUri: { path: "/storage" },
globalState: {
get: (key: string) => {
if (key === "taskHistory") return [{ id: "legacy-task-1", workspace: "/workspace/testing" }]
return undefined
},
update: async () => undefined,
},
}
}
function client() {
const calls: Array<{ name: string; body: unknown }> = []
return {
calls,
kilocode: {
sessionImport: {
project: async (body: unknown) => {
calls.push({ name: "project", body })
return { data: { id: "project-real" } }
},
session: async (body: unknown) => {
calls.push({ name: "session", body })
return { data: { ok: true } }
},
message: async (body: unknown) => {
calls.push({ name: "message", body })
return { data: { ok: true } }
},
part: async (body: unknown) => {
calls.push({ name: "part", body })
return { data: { ok: true } }
},
},
},
}
}
describe("legacy migration migrate", () => {
beforeEach(() => {
parseSession.mockClear()
})
it("inserts project then session then message then part in the correct order", async () => {
const calls: string[] = []
const api = {
kilocode: {
sessionImport: {
project: async () => {
calls.push("project")
return { data: { id: "project-real" } }
},
session: async () => {
calls.push("session")
return { data: { ok: true } }
},
message: async () => {
calls.push("message")
return { data: { ok: true } }
},
part: async () => {
calls.push("part")
return { data: { ok: true } }
},
},
},
}
parseSession.mockResolvedValueOnce({
project: {
id: "project-local",
worktree: "/workspace/testing",
sandboxes: ["/workspace/testing"],
timeCreated: 1,
timeUpdated: 1,
},
session: {
id: "ses_local",
projectID: "project-local",
slug: "legacy-task-1",
directory: "/workspace/testing",
title: "Legacy task",
version: "v2",
timeCreated: 1,
timeUpdated: 1,
},
messages: [
{
id: "msg_1",
sessionID: "ses_local",
timeCreated: 1,
data: {
role: "user",
time: { created: 1 },
agent: "user",
model: { providerID: "legacy", modelID: "legacy" },
},
},
] as never,
parts: [
{
id: "prt_1",
sessionID: "ses_local",
messageID: "msg_1",
timeCreated: 1,
data: {
type: "text",
text: "hello",
time: { start: 1, end: 1 },
},
},
] as never,
})
await migrate("legacy-task-1", ctx() as never, api as never)
expect(calls).toEqual(["project", "session", "message", "part"])
})
it("uses the projectID returned by backend when inserting the session", async () => {
const api = client()
await migrate("legacy-task-1", ctx() as never, api as never)
const call = api.calls.find((x) => x.name === "session")
expect(call).toBeDefined()
expect((call?.body as { projectID: string }).projectID).toBe("project-real")
})
it("returns ok false without throwing when one backend insert fails", async () => {
const api = {
kilocode: {
sessionImport: {
project: async () => ({ data: { id: "project-real" } }),
session: async () => {
throw new Error("session failed")
},
message: async () => ({ data: { ok: true } }),
part: async () => ({ data: { ok: true } }),
},
},
}
const result = await migrate("legacy-task-1", ctx() as never, api as never)
expect(result.ok).toBe(false)
})
it("skips message and part imports when backend reports the session already exists", async () => {
const calls: string[] = []
const api = {
kilocode: {
sessionImport: {
project: async () => {
calls.push("project")
return { data: { id: "project-real" } }
},
session: async () => {
calls.push("session")
return { data: { ok: true, skipped: true } }
},
message: async () => {
calls.push("message")
return { data: { ok: true } }
},
part: async () => {
calls.push("part")
return { data: { ok: true } }
},
},
},
}
const result = await migrate("legacy-task-1", ctx() as never, api as never)
expect(result.ok).toBe(true)
expect(result).toHaveProperty("skipped", true)
expect(calls).toEqual(["project", "session"])
})
})
@@ -0,0 +1,36 @@
import { describe, expect, it } from "bun:test"
import { createProject } from "../../../src/legacy-migration/sessions/lib/project"
import { createSession } from "../../../src/legacy-migration/sessions/lib/session"
import { createMessageID, createPartID } from "../../../src/legacy-migration/sessions/lib/ids"
const id = "019d3df5-d5d9-73dc-bc2c-43a6304ac62c"
const item = {
id,
ts: 1774861014564,
task: "In this folder I need you to create 3 python files with random content, oriented towards web development",
workspace: "/workspace/testing-4",
mode: "code",
}
describe("legacy migration parser", () => {
it("uses the final deterministic ids expected for migration", () => {
const project = createProject(item)
const session = createSession(id, item, project.id)
const msg1 = createMessageID(id, 0)
const msg2 = createMessageID(id, 1)
const prt1 = createPartID(id, 0, 0)
const prt2 = createPartID(id, 1, 0)
expect(project.id).toBe(createProject(item).id)
expect(session.id).toBe(createSession(id, item, project.id).id)
expect(session.id.startsWith("ses_migrated_")).toBe(true)
expect(msg1).toBe(createMessageID(id, 0))
expect(msg2).toBe(createMessageID(id, 1))
expect(msg1.startsWith("msg_migrated_")).toBe(true)
expect(msg2.startsWith("msg_migrated_")).toBe(true)
expect(prt1).toBe(createPartID(id, 0, 0))
expect(prt2).toBe(createPartID(id, 1, 0))
expect(prt1.startsWith("prt_migrated_")).toBe(true)
expect(prt2.startsWith("prt_migrated_")).toBe(true)
})
})
@@ -0,0 +1,224 @@
import { describe, expect, it } from "bun:test"
import { parsePartsFromConversation } from "../../../src/legacy-migration/sessions/lib/parts/parts"
import type { LegacyApiMessage } from "../../../src/legacy-migration/sessions/lib/legacy-types"
type Data = ReturnType<typeof parsePartsFromConversation>[number]["data"]
type Text = Extract<Data, { type: "text" }>
function text(list: ReturnType<typeof parsePartsFromConversation>) {
return list.filter((x): x is (typeof list)[number] & { data: Text } => x.data.type === "text").map((x) => x.data.text)
}
const id = "019d3df5-d5d9-73dc-bc2c-43a6304ac62c"
const item = {
id,
ts: 1774861014564,
task: "In this folder I need you to create 3 python files with random content, oriented towards web development",
workspace: "/workspace/testing-4",
mode: "code",
}
function sample(): LegacyApiMessage[] {
return [
{
role: "user",
content: [
{
type: "text",
text: "<task>\nIn this folder I need you to create 3 python files with random content, oriented towards web development\n</task>",
},
{
type: "text",
text: "<environment_details>\nCurrent time: 2026-03-30T12:54:59+02:00\n</environment_details>",
},
],
ts: 1774861014564,
},
{
role: "assistant",
content: [
{
type: "text",
text: "\n\nI'll create 3 Python files with web development-oriented content.",
},
],
ts: 1774861031791,
},
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_attempt_1",
name: "attempt_completion",
input: {
result: "Created 3 Python files with web development content.",
},
},
],
ts: 1774861079951,
},
]
}
describe("legacy migration parts", () => {
it("converts plain text and text blocks into visible text parts without losing content", async () => {
const list = parsePartsFromConversation(sample(), id, item)
const items = text(list)
expect(items.some((x) => x.includes("In this folder I need you to create 3 python files"))).toBe(true)
expect(items.some((x) => x.includes("I'll create 3 Python files with web development-oriented content."))).toBe(
true,
)
})
it("drops standalone environment_details blocks but keeps the real task text", async () => {
const list = parsePartsFromConversation(sample(), id, item)
const items = text(list)
expect(items.some((x) => x.includes("<environment_details>"))).toBe(false)
expect(items.some((x) => x.includes("In this folder I need you to create 3 python files"))).toBe(true)
})
it("keeps only the first task block content when text exists outside the legacy task wrapper", () => {
const list = parsePartsFromConversation(
[
{
role: "user",
content: [
{
type: "text",
text: "Some preamble\n<task>actual task</task>\nSome postamble",
},
],
ts: 1774861014564,
},
] as LegacyApiMessage[],
id,
item,
)
expect(text(list)).toEqual(["actual task"])
})
it("preserves attempt_completion input.result as assistant-visible text", async () => {
const list = parsePartsFromConversation(sample(), id, item)
const items = text(list)
expect(items.some((x) => x.includes("Created 3 Python files with web development content"))).toBe(true)
})
it("adds a visible user text part for feedback embedded in tool_result", () => {
const list = parsePartsFromConversation(
[
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_feedback_1",
name: "attempt_completion",
input: {
result: "Done.",
},
},
],
ts: 10,
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "toolu_feedback_1",
content: [
{
type: "text",
text: "The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\nAdd two more lines\n</feedback>",
},
],
},
],
ts: 11,
},
] as LegacyApiMessage[],
id,
item,
)
expect(text(list)).toContain("Add two more lines")
})
it("does not create parts for skipped legacy entries like system messages", () => {
const list = parsePartsFromConversation(
[
{
role: "system",
content: "ignored",
ts: 1774861015000,
} as unknown as LegacyApiMessage,
],
id,
item,
)
expect(list).toEqual([])
})
it("keeps part message ids aligned with imported message ids when skipped entries exist", () => {
const list = parsePartsFromConversation(
[
{
role: "user",
content: "hello",
ts: 1,
},
{
role: "system",
content: "ignored",
ts: 2,
} as unknown as LegacyApiMessage,
{
role: "assistant",
content: "hi",
ts: 3,
},
],
id,
item,
)
const ids = list.map((x) => x.messageID)
expect(ids.some((x) => x === "msg_legacy-task-1_2")).toBe(false)
})
it("uses non-colliding ids for reasoning and normal content parts in the same message", () => {
const list = parsePartsFromConversation(
[
{
role: "assistant",
type: "reasoning",
text: "Think first",
content: [
{
type: "text",
text: "Visible answer",
},
],
ts: 1774861015000,
} as unknown as LegacyApiMessage,
],
id,
item,
)
const ids = list.map((x) => x.id)
expect(ids).toHaveLength(2)
expect(new Set(ids).size).toBe(2)
})
})
@@ -0,0 +1,35 @@
import { describe, expect, it } from "bun:test"
import { createProject } from "../../../src/legacy-migration/sessions/lib/project"
describe("legacy migration project", () => {
it("builds a project from the legacy workspace and timestamp", () => {
const project = createProject({
id: "legacy-task-1",
ts: 1774861014564,
workspace: "/workspace/testing",
task: "Understand this project",
mode: "code",
})
expect(project.worktree).toBe("/workspace/testing")
expect(project.sandboxes).toEqual(["/workspace/testing"])
expect(project.timeCreated).toBe(1774861014564)
expect(project.timeUpdated).toBe(1774861014564)
})
it("creates a deterministic project id for the same workspace", () => {
const a = createProject({ id: "legacy-1", workspace: "/workspace/testing" })
const b = createProject({ id: "legacy-1", workspace: "/workspace/testing" })
expect(a.id).toBe(b.id)
})
it("falls back to empty defaults when legacy workspace data is missing", () => {
const project = createProject()
expect(project.worktree).toBe("")
expect(project.sandboxes).toEqual([])
expect(project.timeCreated).toBe(0)
expect(project.timeUpdated).toBe(0)
})
})
@@ -0,0 +1,99 @@
import { describe, expect, it } from "bun:test"
import { parsePartsFromConversation } from "../../../src/legacy-migration/sessions/lib/parts/parts"
import type { LegacyApiMessage } from "../../../src/legacy-migration/sessions/lib/legacy-types"
type Reasoning = Extract<ReturnType<typeof parsePartsFromConversation>[number]["data"], { type: "reasoning" }>
function reasoning(list: ReturnType<typeof parsePartsFromConversation>) {
return list
.filter((x): x is (typeof list)[number] & { data: Reasoning } => x.data.type === "reasoning")
.map((x) => x.data.text)
}
const id = "legacy-reasoning-1"
const item = {
id,
ts: 1774861014564,
task: "Reasoning parsing test",
workspace: "/workspace/testing",
mode: "code",
}
describe("legacy migration reasoning", () => {
it("converts type reasoning plus text into a reasoning part", () => {
const list = parsePartsFromConversation(
[
{
role: "assistant",
type: "reasoning",
text: "I should inspect the files first.",
content: [],
ts: 1774861015000,
} as unknown as LegacyApiMessage,
],
id,
item,
)
const out = reasoning(list)
expect(out).toEqual(["I should inspect the files first."])
})
it("extracts reasoning from reasoning_content", () => {
const list = parsePartsFromConversation(
[
{
role: "assistant",
content: [],
reasoning_content: "I should read the markdown before answering.",
ts: 1774861015000,
} as unknown as LegacyApiMessage,
],
id,
item,
)
const out = reasoning(list)
expect(out).toEqual(["I should read the markdown before answering."])
})
it("extracts readable reasoning text from reasoning_details", () => {
const list = parsePartsFromConversation(
[
{
role: "assistant",
content: [],
reasoning_details: [{ text: "First inspect the repo." }, { reasoning: "Then summarize the structure." }],
ts: 1774861015000,
} as unknown as LegacyApiMessage,
],
id,
item,
)
const out = reasoning(list)
expect(out).toHaveLength(1)
expect(out[0]).toContain("First inspect the repo.")
expect(out[0]).toContain("Then summarize the structure.")
})
it("prefers explicit reasoning entries over provider-specific reasoning fields when both exist", () => {
const list = parsePartsFromConversation(
[
{
role: "assistant",
type: "reasoning",
text: "I should inspect the files first.",
reasoning_content: "I should inspect the files first.",
content: [],
ts: 1774861015000,
} as unknown as LegacyApiMessage,
],
id,
item,
)
const out = reasoning(list)
expect(out).toEqual(["I should inspect the files first."])
})
})
@@ -0,0 +1,41 @@
import { describe, expect, it } from "bun:test"
import { createSession } from "../../../src/legacy-migration/sessions/lib/session"
describe("legacy migration session", () => {
it("builds a session from legacy task metadata and the resolved project id", () => {
const session = createSession(
"019d3df5-d5d9-73dc-bc2c-43a6304ac62c",
{
id: "019d3df5-d5d9-73dc-bc2c-43a6304ac62c",
ts: 1774861014564,
workspace: "/workspace/testing",
task: "Understand this project",
mode: "code",
},
"project-1",
)
expect(session.projectID).toBe("project-1")
expect(session.directory).toBe("/workspace/testing")
expect(session.title).toBe("Understand this project")
expect(session.version).toBe("v2")
expect(session.timeCreated).toBe(1774861014564)
expect(session.timeUpdated).toBe(1774861014564)
})
it("creates a deterministic session id from the legacy task id", () => {
const a = createSession("legacy-task-1", undefined, "project-1")
const b = createSession("legacy-task-1", undefined, "project-1")
expect(a.id).toBe(b.id)
expect(a.id.startsWith("ses_migrated_")).toBe(true)
})
it("falls back to the legacy id as title when task metadata is missing", () => {
const session = createSession("legacy-task-1", undefined, "project-1")
expect(session.slug).toBe("legacy-task-1")
expect(session.title).toBe("legacy-task-1")
expect(session.directory).toBe("")
})
})
@@ -0,0 +1,134 @@
import { describe, expect, it } from "bun:test"
import { parsePartsFromConversation } from "../../../src/legacy-migration/sessions/lib/parts/parts"
import type { LegacyApiMessage } from "../../../src/legacy-migration/sessions/lib/legacy-types"
type Tool = Extract<ReturnType<typeof parsePartsFromConversation>[number]["data"], { type: "tool" }>
function tools(list: ReturnType<typeof parsePartsFromConversation>) {
return list.filter((x): x is (typeof list)[number] & { data: Tool } => x.data.type === "tool").map((x) => x.data)
}
const id = "legacy-task-1"
const item = {
id,
ts: 1774861014564,
task: "Tool parsing test",
workspace: "/workspace/testing",
mode: "code",
}
function fallback(): LegacyApiMessage[] {
return [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_fallback_1",
name: "read_file",
input: { path: "app.py" },
},
],
ts: 1774861015000,
},
]
}
function merged(): LegacyApiMessage[] {
return [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_merge_1",
name: "read_file",
input: { path: "app.py" },
},
],
ts: 1774861015000,
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "toolu_merge_1",
content: [
{
type: "text",
text: "File: app.py\nHello world",
},
],
},
],
ts: 1774861016000,
},
]
}
function noTextResult(): LegacyApiMessage[] {
return [
{
role: "assistant",
content: [
{
type: "tool_use",
id: "toolu_merge_2",
name: "read_file",
input: { path: "app.py" },
},
],
ts: 1774861015000,
},
{
role: "user",
content: [
{
type: "tool_result",
tool_use_id: "toolu_merge_2",
content: [],
},
],
ts: 1774861016000,
},
]
}
describe("legacy migration tools", () => {
it("creates a fallback tool part from tool_use when there is no matching tool_result", () => {
const list = parsePartsFromConversation(fallback(), id, item)
const out = tools(list)
expect(out).toHaveLength(1)
expect(out[0]?.tool).toBe("read_file")
expect(out[0]?.state.status).toBe("completed")
})
it("merges tool_use and tool_result into one completed tool part using the correct tool id", () => {
const list = parsePartsFromConversation(merged(), id, item)
const out = tools(list)
expect(out).toHaveLength(1)
expect(out[0]?.callID).toBe("toolu_merge_1")
expect(out[0]?.tool).toBe("read_file")
if (out[0]?.state.status !== "completed") throw new Error("tool was not completed")
expect(out[0].state.output).toContain("File: app.py")
})
it("does not duplicate a tool part when a matching tool_result exists later in the conversation", () => {
const list = parsePartsFromConversation(merged(), id, item)
const out = tools(list)
expect(out).toHaveLength(1)
})
it("falls back to the tool name when tool_result has no readable text content", () => {
const list = parsePartsFromConversation(noTextResult(), id, item)
const out = tools(list)
expect(out).toHaveLength(1)
if (out[0]?.state.status !== "completed") throw new Error("tool was not completed")
expect(out[0].state.output).toBe("read_file")
})
})
@@ -6,7 +6,8 @@
* 2. Migrate — grouped selection, live in-place progress, cleanup
*/
import { Component, Show, createSignal, onMount, onCleanup, JSX } from "solid-js"
import { Show, createSignal, onMount, onCleanup } from "solid-js"
import type { Component, JSX } from "solid-js"
import { useVSCode } from "../../context/vscode"
import { useLanguage } from "../../context/language"
import type {
@@ -182,6 +183,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
const [providers, setProviders] = createSignal<MigrationProviderInfo[]>([])
const [mcpServers, setMcpServers] = createSignal<MigrationMcpServerInfo[]>([])
const [customModes, setCustomModes] = createSignal<MigrationCustomModeInfo[]>([])
const [sessions, setSessions] = createSignal<string[]>([])
const [defaultModel, setDefaultModel] = createSignal<{ provider: string; model: string } | undefined>(undefined)
const [legacySettings, setLegacySettings] = createSignal<LegacySettings | undefined>(undefined)
@@ -189,6 +191,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
const [migrateProviders, setMigrateProviders] = createSignal(true)
const [migrateMcpServers, setMigrateMcpServers] = createSignal(true)
const [migrateModes, setMigrateModes] = createSignal(true)
const [migrateSessions, setMigrateSessions] = createSignal(true)
const [migrateDefaultModel, setMigrateDefaultModel] = createSignal(true)
const [migrateAutoApproval, setMigrateAutoApproval] = createSignal(true)
const [migrateLanguage, setMigrateLanguage] = createSignal(true)
@@ -213,6 +216,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
setProviders(data.providers)
setMcpServers(data.mcpServers)
setCustomModes(data.customModes)
setSessions(data.sessions ?? [])
setDefaultModel(data.defaultModel)
setLegacySettings(data.settings)
@@ -220,6 +224,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
setMigrateProviders(data.providers.some((p) => p.supported && p.hasApiKey))
setMigrateMcpServers(data.mcpServers.length > 0)
setMigrateModes(data.customModes.length > 0)
setMigrateSessions((data.sessions?.length ?? 0) > 0)
setMigrateDefaultModel(Boolean(data.defaultModel))
const s = data.settings
@@ -318,6 +323,9 @@ const MigrationWizard: Component<MigrationWizardProps> = (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 }]
: []),
...(migrateDefaultModel() && defaultModel()
? [{ item: "Default model", group: "defaultModel", status: "pending" as const }]
: []),
@@ -356,6 +364,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
providers: selectedProviderNames,
mcpServers: selectedMcpNames,
customModes: selectedModesSlugs,
sessions: migrateSessions() ? sessions() : [],
defaultModel: migrateDefaultModel(),
settings: {
autoApproval,
@@ -398,11 +407,13 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
const hasLanguageData = () => Boolean(legacySettings()?.language)
const hasAutocompleteData = () => Boolean(legacySettings()?.autocomplete)
const hasSessions = () => sessions().length > 0
const hasAnySelection = () =>
(migrateProviders() && supportedProviderCount() > 0) ||
(migrateMcpServers() && mcpServers().length > 0) ||
(migrateModes() && customModes().length > 0) ||
(migrateSessions() && hasSessions()) ||
(migrateDefaultModel() && Boolean(defaultModel())) ||
(migrateAutoApproval() && hasAnyAutoApprovalData()) ||
(migrateLanguage() && hasLanguageData()) ||
@@ -412,6 +423,7 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
supportedProviderCount() === 0 &&
mcpServers().length === 0 &&
customModes().length === 0 &&
!hasSessions() &&
!defaultModel() &&
!hasAnyAutoApprovalData() &&
!hasLanguageData() &&
@@ -650,6 +662,27 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
</div>
</Show>
<Show when={hasSessions()}>
<div class="migration-wizard__item">
<Show when={phase() === "selecting"} fallback={<StatusIcon group="sessions" />}>
<label class="migration-wizard__checkbox">
<input
type="checkbox"
checked={migrateSessions()}
onChange={(e) => setMigrateSessions(e.currentTarget.checked)}
/>
<span class="migration-wizard__checkmark">
<CheckmarkSvg />
</span>
</label>
</Show>
<div class="migration-wizard__item-text">
<div class="label">Chat Sessions &amp; History</div>
<div class="desc">{sessions().length} sessions detected</div>
</div>
</div>
</Show>
{/* Default Model */}
<Show when={Boolean(defaultModel())}>
<div class="migration-wizard__item">
@@ -734,20 +767,6 @@ const MigrationWizard: Component<MigrationWizardProps> = (props) => {
</div>
</Show>
{/* Divider + Cannot be migrated */}
<div class="migration-wizard__divider" />
<div class="migration-wizard__section-label">{language.t("migration.migrate.cannotMigrate")}</div>
<div class="migration-wizard__item migration-wizard__item--disabled">
<label class="migration-wizard__checkbox">
<input type="checkbox" disabled />
<span class="migration-wizard__checkmark" />
</label>
<div class="migration-wizard__item-text">
<div class="label">{language.t("migration.migrate.chatHistory")}</div>
<div class="desc">{language.t("migration.migrate.chatHistoryDesc")}</div>
</div>
</div>
{/* Cleanup option after done */}
<Show when={phase() === "done"}>
<div class="migration-wizard__divider" />
@@ -1053,6 +1053,7 @@ export interface MigrationStateMessage {
providers: MigrationProviderInfo[]
mcpServers: MigrationMcpServerInfo[]
customModes: MigrationCustomModeInfo[]
sessions?: string[]
defaultModel?: { provider: string; model: string }
settings?: LegacySettings
}
@@ -1064,6 +1065,7 @@ export interface LegacyMigrationDataMessage {
providers: MigrationProviderInfo[]
mcpServers: MigrationMcpServerInfo[]
customModes: MigrationCustomModeInfo[]
sessions?: string[]
defaultModel?: { provider: string; model: string }
settings?: LegacySettings
}
@@ -1100,6 +1102,7 @@ export interface StartLegacyMigrationMessage {
providers: string[]
mcpServers: string[]
customModes: string[]
sessions?: string[]
defaultModel: boolean
settings: {
autoApproval: MigrationAutoApprovalSelections
@@ -0,0 +1,94 @@
import { Hono } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi"
import { lazy } from "../../util/lazy"
import { errors } from "../../server/error"
import { SessionImportService } from "./service"
import { SessionImportType } from "./types"
export const SessionImportRoutes = lazy(() =>
new Hono()
.post(
"/project",
describeRoute({
summary: "Insert project for session import",
description: "Insert or update a project row used by legacy session import.",
operationId: "kilocode.sessionImport.project",
responses: {
200: {
description: "Project import result",
content: {
"application/json": {
schema: resolver(SessionImportType.Result),
},
},
},
...errors(400),
},
}),
validator("json", SessionImportType.Project),
async (c) => c.json(await SessionImportService.project(c.req.valid("json"))),
)
.post(
"/session",
describeRoute({
summary: "Insert session for session import",
description: "Insert or update a session row used by legacy session import.",
operationId: "kilocode.sessionImport.session",
responses: {
200: {
description: "Session import result",
content: {
"application/json": {
schema: resolver(SessionImportType.Result),
},
},
},
...errors(400),
},
}),
validator("json", SessionImportType.Session),
async (c) => c.json(await SessionImportService.session(c.req.valid("json"))),
)
.post(
"/message",
describeRoute({
summary: "Insert message for session import",
description: "Insert or update a message row used by legacy session import.",
operationId: "kilocode.sessionImport.message",
responses: {
200: {
description: "Message import result",
content: {
"application/json": {
schema: resolver(SessionImportType.Result),
},
},
},
...errors(400),
},
}),
validator("json", SessionImportType.Message),
async (c) => c.json(await SessionImportService.message(c.req.valid("json"))),
)
.post(
"/part",
describeRoute({
summary: "Insert part for session import",
description: "Insert or update a part row used by legacy session import.",
operationId: "kilocode.sessionImport.part",
responses: {
200: {
description: "Part import result",
content: {
"application/json": {
schema: resolver(SessionImportType.Result),
},
},
},
...errors(400),
},
}),
validator("json", SessionImportType.Part),
async (c) => c.json(await SessionImportService.part(c.req.valid("json"))),
),
)
@@ -0,0 +1,97 @@
import { Database } from "../../storage/db"
import { SessionTable, MessageTable, PartTable } from "../../session/session.sql"
import { SessionImportType } from "./types"
import { Project } from "../../project/project"
import { eq } from "drizzle-orm"
const key = (input: unknown) => [input] as never
const target = (input: unknown) => input as never
export namespace SessionImportService {
export async function project(input: SessionImportType.Project): Promise<SessionImportType.Result> {
// Do not resolve an empty legacy worktree, because that would fall back to the current
// process directory and silently attach the migrated session to the wrong project.
if (!input.worktree.trim()) {
throw new Error("Legacy project import requires a non-empty worktree")
}
const result = await Project.fromDirectory(input.worktree)
return { ok: true, id: result.project.id }
}
export async function session(input: SessionImportType.Session): Promise<SessionImportType.Result> {
const row = Database.use((db) => db.select().from(SessionTable).where(eq(target(SessionTable.id), input.id)).get())
if (row) return { ok: true, id: input.id, skipped: true }
Database.use((db) => {
db.insert(SessionTable)
.values({
id: input.id,
project_id: input.projectID,
workspace_id: input.workspaceID,
parent_id: input.parentID,
slug: input.slug,
directory: input.directory,
title: input.title,
version: input.version,
share_url: input.shareURL,
summary_additions: input.summary?.additions,
summary_deletions: input.summary?.deletions,
summary_files: input.summary?.files,
summary_diffs: input.summary?.diffs as never,
revert: input.revert,
permission: input.permission as never,
time_created: input.timeCreated,
time_updated: input.timeUpdated,
time_compacting: input.timeCompacting,
time_archived: input.timeArchived,
})
.onConflictDoNothing({
target: key(SessionTable.id),
})
.run()
})
return { ok: true, id: input.id }
}
export async function message(input: SessionImportType.Message): Promise<SessionImportType.Result> {
Database.use((db) => {
db.insert(MessageTable)
.values({
id: input.id,
session_id: input.sessionID,
time_created: input.timeCreated,
data: input.data as never,
})
.onConflictDoUpdate({
target: key(MessageTable.id),
set: {
data: input.data as never,
},
})
.run()
})
return { ok: true, id: input.id }
}
export async function part(input: SessionImportType.Part): Promise<SessionImportType.Result> {
Database.use((db) => {
db.insert(PartTable)
.values({
id: input.id,
message_id: input.messageID,
session_id: input.sessionID,
time_created: input.timeCreated,
data: input.data as never,
})
.onConflictDoUpdate({
target: key(PartTable.id),
set: {
data: input.data as never,
},
})
.run()
})
return { ok: true, id: input.id }
}
}
@@ -0,0 +1,216 @@
import z from "zod"
export namespace SessionImportType {
export const UserMessageData = z.object({
role: z.literal("user"),
time: z.object({
created: z.number(),
}),
agent: z.string(),
model: z.object({
providerID: z.string(),
modelID: z.string(),
}),
tools: z.record(z.string(), z.boolean()).optional(),
})
export const AssistantMessageData = z.object({
role: z.literal("assistant"),
time: z.object({
created: z.number(),
completed: z.number().optional(),
}),
parentID: z.string(),
modelID: z.string(),
providerID: z.string(),
mode: z.string(),
agent: z.string(),
path: z.object({
cwd: z.string(),
root: z.string(),
}),
summary: z.boolean().optional(),
cost: z.number(),
tokens: z.object({
total: z.number().optional(),
input: z.number(),
output: z.number(),
reasoning: z.number(),
cache: z.object({
read: z.number(),
write: z.number(),
}),
}),
structured: z.unknown().optional(),
variant: z.string().optional(),
finish: z.string().optional(),
})
export const MessageData = z.discriminatedUnion("role", [UserMessageData, AssistantMessageData])
export const TextPartData = z.object({
type: z.literal("text"),
text: z.string(),
synthetic: z.boolean().optional(),
ignored: z.boolean().optional(),
time: z
.object({
start: z.number(),
end: z.number().optional(),
})
.optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
})
export const ReasoningPartData = z.object({
type: z.literal("reasoning"),
text: z.string(),
metadata: z.record(z.string(), z.unknown()).optional(),
time: z.object({
start: z.number(),
end: z.number().optional(),
}),
})
export const ToolStatePending = z.object({
status: z.literal("pending"),
input: z.record(z.string(), z.unknown()),
raw: z.string(),
})
export const ToolStateRunning = z.object({
status: z.literal("running"),
input: z.record(z.string(), z.unknown()),
title: z.string().optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
time: z.object({
start: z.number(),
}),
})
export const ToolStateCompleted = z.object({
status: z.literal("completed"),
input: z.record(z.string(), z.unknown()),
output: z.string(),
title: z.string(),
metadata: z.record(z.string(), z.unknown()),
time: z.object({
start: z.number(),
end: z.number(),
compacted: z.number().optional(),
}),
})
export const ToolStateError = z.object({
status: z.literal("error"),
input: z.record(z.string(), z.unknown()),
error: z.string(),
metadata: z.record(z.string(), z.unknown()).optional(),
time: z.object({
start: z.number(),
end: z.number(),
}),
})
export const ToolState = z.discriminatedUnion("status", [ToolStatePending, ToolStateRunning, ToolStateCompleted, ToolStateError])
export const ToolPartData = z.object({
type: z.literal("tool"),
callID: z.string(),
tool: z.string(),
state: ToolState,
metadata: z.record(z.string(), z.unknown()).optional(),
})
export const PartData = z.discriminatedUnion("type", [TextPartData, ReasoningPartData, ToolPartData])
export const Result = z.object({
ok: z.boolean(),
id: z.string(),
skipped: z.boolean().optional(),
})
export const Project = z.object({
id: z.string(),
worktree: z.string(),
vcs: z.string().optional(),
name: z.string().optional(),
iconUrl: z.string().optional(),
iconColor: z.string().optional(),
timeCreated: z.number(),
timeUpdated: z.number(),
timeInitialized: z.number().optional(),
sandboxes: z.array(z.string()),
commands: z
.object({
start: z.string().optional(),
})
.optional(),
})
export const Session = z.object({
id: z.string(),
projectID: z.string(),
workspaceID: z.string().optional(),
parentID: z.string().optional(),
slug: z.string(),
directory: z.string(),
title: z.string(),
version: z.string(),
shareURL: z.string().optional(),
summary: z
.object({
additions: z.number(),
deletions: z.number(),
files: z.number(),
diffs: z.array(z.record(z.string(), z.unknown())).optional(),
})
.optional(),
revert: z
.object({
messageID: z.string(),
partID: z.string().optional(),
snapshot: z.string().optional(),
diff: z.string().optional(),
})
.optional(),
permission: z.record(z.string(), z.unknown()).optional(),
timeCreated: z.number(),
timeUpdated: z.number(),
timeCompacting: z.number().optional(),
timeArchived: z.number().optional(),
})
export const Message = z.object({
id: z.string(),
sessionID: z.string(),
timeCreated: z.number(),
data: MessageData,
})
export const Part = z.object({
id: z.string(),
messageID: z.string(),
sessionID: z.string(),
timeCreated: z.number().optional(),
data: PartData,
})
export type Result = z.infer<typeof Result>
export type UserMessageData = z.infer<typeof UserMessageData>
export type AssistantMessageData = z.infer<typeof AssistantMessageData>
export type MessageData = z.infer<typeof MessageData>
export type TextPartData = z.infer<typeof TextPartData>
export type ReasoningPartData = z.infer<typeof ReasoningPartData>
export type ToolStatePending = z.infer<typeof ToolStatePending>
export type ToolStateRunning = z.infer<typeof ToolStateRunning>
export type ToolStateCompleted = z.infer<typeof ToolStateCompleted>
export type ToolStateError = z.infer<typeof ToolStateError>
export type ToolState = z.infer<typeof ToolState>
export type ToolPartData = z.infer<typeof ToolPartData>
export type PartData = z.infer<typeof PartData>
export type Project = z.infer<typeof Project>
export type Session = z.infer<typeof Session>
export type Message = z.infer<typeof Message>
export type Part = z.infer<typeof Part>
}
@@ -8,9 +8,11 @@ import { Skill } from "../../skill/skill"
import { Agent } from "../../agent/agent"
import { lazy } from "../../util/lazy"
import { errors } from "../error"
import { SessionImportRoutes } from "../../kilocode/session-import/routes"
export const KilocodeRoutes = lazy(() =>
new Hono()
.route("/session-import", SessionImportRoutes())
.post(
"/skill/remove",
describeRoute({
+395
View File
@@ -61,6 +61,14 @@ import type {
KilocodeRemoveAgentResponses,
KilocodeRemoveSkillErrors,
KilocodeRemoveSkillResponses,
KilocodeSessionImportMessageErrors,
KilocodeSessionImportMessageResponses,
KilocodeSessionImportPartErrors,
KilocodeSessionImportPartResponses,
KilocodeSessionImportProjectErrors,
KilocodeSessionImportProjectResponses,
KilocodeSessionImportSessionErrors,
KilocodeSessionImportSessionResponses,
KiloFimErrors,
KiloFimResponses,
KiloModesResponses,
@@ -3095,6 +3103,388 @@ export class EnhancePrompt extends HeyApiClient {
}
}
export class SessionImport extends HeyApiClient {
/**
* Insert project for session import
*
* Insert or update a project row used by legacy session import.
*/
public project<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
id?: string
worktree?: string
vcs?: string
name?: string
iconUrl?: string
iconColor?: string
timeCreated?: number
timeUpdated?: number
timeInitialized?: number
sandboxes?: Array<string>
commands?: {
start?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "id" },
{ in: "body", key: "worktree" },
{ in: "body", key: "vcs" },
{ in: "body", key: "name" },
{ in: "body", key: "iconUrl" },
{ in: "body", key: "iconColor" },
{ in: "body", key: "timeCreated" },
{ in: "body", key: "timeUpdated" },
{ in: "body", key: "timeInitialized" },
{ in: "body", key: "sandboxes" },
{ in: "body", key: "commands" },
],
},
],
)
return (options?.client ?? this.client).post<
KilocodeSessionImportProjectResponses,
KilocodeSessionImportProjectErrors,
ThrowOnError
>({
url: "/kilocode/session-import/project",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* Insert session for session import
*
* Insert or update a session row used by legacy session import.
*/
public session<ThrowOnError extends boolean = false>(
parameters?: {
query_directory?: string
workspace?: string
id?: string
projectID?: string
workspaceID?: string
parentID?: string
slug?: string
body_directory?: string
title?: string
version?: string
shareURL?: string
summary?: {
additions: number
deletions: number
files: number
diffs?: Array<{
[key: string]: unknown
}>
}
revert?: {
messageID: string
partID?: string
snapshot?: string
diff?: string
}
permission?: {
[key: string]: unknown
}
timeCreated?: number
timeUpdated?: number
timeCompacting?: number
timeArchived?: number
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{
in: "query",
key: "query_directory",
map: "directory",
},
{ in: "query", key: "workspace" },
{ in: "body", key: "id" },
{ in: "body", key: "projectID" },
{ in: "body", key: "workspaceID" },
{ in: "body", key: "parentID" },
{ in: "body", key: "slug" },
{
in: "body",
key: "body_directory",
map: "directory",
},
{ in: "body", key: "title" },
{ in: "body", key: "version" },
{ in: "body", key: "shareURL" },
{ in: "body", key: "summary" },
{ in: "body", key: "revert" },
{ in: "body", key: "permission" },
{ in: "body", key: "timeCreated" },
{ in: "body", key: "timeUpdated" },
{ in: "body", key: "timeCompacting" },
{ in: "body", key: "timeArchived" },
],
},
],
)
return (options?.client ?? this.client).post<
KilocodeSessionImportSessionResponses,
KilocodeSessionImportSessionErrors,
ThrowOnError
>({
url: "/kilocode/session-import/session",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* Insert message for session import
*
* Insert or update a message row used by legacy session import.
*/
public message<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
id?: string
sessionID?: string
timeCreated?: number
data?:
| {
role: "user"
time: {
created: number
}
agent: string
model: {
providerID: string
modelID: string
}
tools?: {
[key: string]: boolean
}
}
| {
role: "assistant"
time: {
created: number
completed?: number
}
parentID: string
modelID: string
providerID: string
mode: string
agent: string
path: {
cwd: string
root: string
}
summary?: boolean
cost: number
tokens: {
total?: number
input: number
output: number
reasoning: number
cache: {
read: number
write: number
}
}
structured?: unknown
variant?: string
finish?: string
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "id" },
{ in: "body", key: "sessionID" },
{ in: "body", key: "timeCreated" },
{ in: "body", key: "data" },
],
},
],
)
return (options?.client ?? this.client).post<
KilocodeSessionImportMessageResponses,
KilocodeSessionImportMessageErrors,
ThrowOnError
>({
url: "/kilocode/session-import/message",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
/**
* Insert part for session import
*
* Insert or update a part row used by legacy session import.
*/
public part<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
id?: string
messageID?: string
sessionID?: string
timeCreated?: number
data?:
| {
type: "text"
text: string
synthetic?: boolean
ignored?: boolean
time?: {
start: number
end?: number
}
metadata?: {
[key: string]: unknown
}
}
| {
type: "reasoning"
text: string
metadata?: {
[key: string]: unknown
}
time: {
start: number
end?: number
}
}
| {
type: "tool"
callID: string
tool: string
state:
| {
status: "pending"
input: {
[key: string]: unknown
}
raw: string
}
| {
status: "running"
input: {
[key: string]: unknown
}
title?: string
metadata?: {
[key: string]: unknown
}
time: {
start: number
}
}
| {
status: "completed"
input: {
[key: string]: unknown
}
output: string
title: string
metadata: {
[key: string]: unknown
}
time: {
start: number
end: number
compacted?: number
}
}
| {
status: "error"
input: {
[key: string]: unknown
}
error: string
metadata?: {
[key: string]: unknown
}
time: {
start: number
end: number
}
}
metadata?: {
[key: string]: unknown
}
}
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "id" },
{ in: "body", key: "messageID" },
{ in: "body", key: "sessionID" },
{ in: "body", key: "timeCreated" },
{ in: "body", key: "data" },
],
},
],
)
return (options?.client ?? this.client).post<
KilocodeSessionImportPartResponses,
KilocodeSessionImportPartErrors,
ThrowOnError
>({
url: "/kilocode/session-import/part",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}
export class Kilocode extends HeyApiClient {
/**
* Remove a skill
@@ -3173,6 +3563,11 @@ export class Kilocode extends HeyApiClient {
},
)
}
private _sessionImport?: SessionImport
get sessionImport(): SessionImport {
return (this._sessionImport ??= new SessionImport({ client: this.client }))
}
}
export class Organization extends HeyApiClient {
+319
View File
@@ -4529,6 +4529,325 @@ export type EnhancePromptEnhanceResponses = {
export type EnhancePromptEnhanceResponse = EnhancePromptEnhanceResponses[keyof EnhancePromptEnhanceResponses]
export type KilocodeSessionImportProjectData = {
body?: {
id: string
worktree: string
vcs?: string
name?: string
iconUrl?: string
iconColor?: string
timeCreated: number
timeUpdated: number
timeInitialized?: number
sandboxes: Array<string>
commands?: {
start?: string
}
}
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/session-import/project"
}
export type KilocodeSessionImportProjectErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type KilocodeSessionImportProjectError =
KilocodeSessionImportProjectErrors[keyof KilocodeSessionImportProjectErrors]
export type KilocodeSessionImportProjectResponses = {
/**
* Project import result
*/
200: {
ok: boolean
id: string
skipped?: boolean
}
}
export type KilocodeSessionImportProjectResponse =
KilocodeSessionImportProjectResponses[keyof KilocodeSessionImportProjectResponses]
export type KilocodeSessionImportSessionData = {
body?: {
id: string
projectID: string
workspaceID?: string
parentID?: string
slug: string
directory: string
title: string
version: string
shareURL?: string
summary?: {
additions: number
deletions: number
files: number
diffs?: Array<{
[key: string]: unknown
}>
}
revert?: {
messageID: string
partID?: string
snapshot?: string
diff?: string
}
permission?: {
[key: string]: unknown
}
timeCreated: number
timeUpdated: number
timeCompacting?: number
timeArchived?: number
}
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/session-import/session"
}
export type KilocodeSessionImportSessionErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type KilocodeSessionImportSessionError =
KilocodeSessionImportSessionErrors[keyof KilocodeSessionImportSessionErrors]
export type KilocodeSessionImportSessionResponses = {
/**
* Session import result
*/
200: {
ok: boolean
id: string
skipped?: boolean
}
}
export type KilocodeSessionImportSessionResponse =
KilocodeSessionImportSessionResponses[keyof KilocodeSessionImportSessionResponses]
export type KilocodeSessionImportMessageData = {
body?: {
id: string
sessionID: string
timeCreated: number
data:
| {
role: "user"
time: {
created: number
}
agent: string
model: {
providerID: string
modelID: string
}
tools?: {
[key: string]: boolean
}
}
| {
role: "assistant"
time: {
created: number
completed?: number
}
parentID: string
modelID: string
providerID: string
mode: string
agent: string
path: {
cwd: string
root: string
}
summary?: boolean
cost: number
tokens: {
total?: number
input: number
output: number
reasoning: number
cache: {
read: number
write: number
}
}
structured?: unknown
variant?: string
finish?: string
}
}
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/session-import/message"
}
export type KilocodeSessionImportMessageErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type KilocodeSessionImportMessageError =
KilocodeSessionImportMessageErrors[keyof KilocodeSessionImportMessageErrors]
export type KilocodeSessionImportMessageResponses = {
/**
* Message import result
*/
200: {
ok: boolean
id: string
skipped?: boolean
}
}
export type KilocodeSessionImportMessageResponse =
KilocodeSessionImportMessageResponses[keyof KilocodeSessionImportMessageResponses]
export type KilocodeSessionImportPartData = {
body?: {
id: string
messageID: string
sessionID: string
timeCreated?: number
data:
| {
type: "text"
text: string
synthetic?: boolean
ignored?: boolean
time?: {
start: number
end?: number
}
metadata?: {
[key: string]: unknown
}
}
| {
type: "reasoning"
text: string
metadata?: {
[key: string]: unknown
}
time: {
start: number
end?: number
}
}
| {
type: "tool"
callID: string
tool: string
state:
| {
status: "pending"
input: {
[key: string]: unknown
}
raw: string
}
| {
status: "running"
input: {
[key: string]: unknown
}
title?: string
metadata?: {
[key: string]: unknown
}
time: {
start: number
}
}
| {
status: "completed"
input: {
[key: string]: unknown
}
output: string
title: string
metadata: {
[key: string]: unknown
}
time: {
start: number
end: number
compacted?: number
}
}
| {
status: "error"
input: {
[key: string]: unknown
}
error: string
metadata?: {
[key: string]: unknown
}
time: {
start: number
end: number
}
}
metadata?: {
[key: string]: unknown
}
}
}
path?: never
query?: {
directory?: string
workspace?: string
}
url: "/kilocode/session-import/part"
}
export type KilocodeSessionImportPartErrors = {
/**
* Bad request
*/
400: BadRequestError
}
export type KilocodeSessionImportPartError = KilocodeSessionImportPartErrors[keyof KilocodeSessionImportPartErrors]
export type KilocodeSessionImportPartResponses = {
/**
* Part import result
*/
200: {
ok: boolean
id: string
skipped?: boolean
}
}
export type KilocodeSessionImportPartResponse =
KilocodeSessionImportPartResponses[keyof KilocodeSessionImportPartResponses]
export type KilocodeRemoveSkillData = {
body?: {
location: string
+1005 -4
View File
File diff suppressed because it is too large Load Diff