import * as path from "node:path"; import * as vscode from "vscode"; import type { SessionSummary } from "@moonshot-ai/kimi-code-sdk"; import { Events, Methods } from "../../shared/bridge"; import type { SessionInfo } from "../../shared/legacy-sdk"; import type { BaselineSession } from "../managers/baseline.manager"; import { replaySessionToWebviewEvents } from "../runtime/replay-adapter"; import { areSameFsPath, isFsPathInsideOrEqual } from "../utils/fs-path"; import { isWorkspacePathContained, workDirUriFromPath, } from "../utils/workspace-path"; import type { Handler } from "./types"; const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; interface LoadHistoryParams { kimiSessionId: string; } interface DeleteSessionParams { sessionId: string; } interface ForkSessionParams { sessionId: string; turnIndex: number; } export const sessionHandlers: Record> = { [Methods.GetKimiSessions]: async (_, ctx): Promise => { if (!ctx.workDir) return []; return (await ctx.harness.listSessions({ workDir: ctx.workDir })).map(toSessionInfo); }, [Methods.GetAllKimiSessions]: async (_, ctx): Promise => { if (!ctx.workspaceRoot) return []; return (await ctx.harness.listSessions()) .filter((session) => isInsideOrEqual(ctx.workspaceRoot!, session.workDir)) .map(toSessionInfo); }, [Methods.GetRegisteredWorkDirs]: async (_, ctx): Promise => { if (!ctx.workspaceRoot) return []; const sessions = await ctx.harness.listSessions(); const candidates = [ctx.workspaceRoot, ctx.workDir, ...sessions.map((session) => session.workDir)]; return [ ...new Set( candidates.filter((workDir): workDir is string => workDir !== null && isInsideOrEqual(ctx.workspaceRoot!, workDir)), ), ].toSorted(); }, [Methods.SetWorkDir]: async (params: { workDir: string | null }, ctx) => { if (!ctx.workspaceRoot || !ctx.workspaceRootUri) return { ok: false }; const target = params.workDir; if (target) { const targetUri = workDirUriFromPath(ctx.workspaceRootUri, ctx.workspaceRoot, target); if (targetUri === undefined || !(await isWorkspacePathContained(ctx.workspaceRootUri, targetUri))) { return { ok: false }; } } try { await ctx.setCustomWorkDir(target); } catch { return { ok: false }; } return { ok: true, workDir: target ?? ctx.workspaceRoot }; }, [Methods.BrowseWorkDir]: async (_, ctx) => { if (!ctx.workspaceRoot || !ctx.workspaceRootUri) return { ok: false, workDir: null }; const workspaceUri = ctx.workspaceRootUri; let subdirectories: string[] = []; try { const entries = await vscode.workspace.fs.readDirectory(workspaceUri); subdirectories = entries .filter(([name, type]) => type === vscode.FileType.Directory && !name.startsWith(".")) .map(([name]) => name) .toSorted(); } catch { // The native picker remains available when directory enumeration fails. } const picked = await vscode.window.showQuickPick( [ { label: "$(folder) Browse...", description: "Open folder picker", alwaysShow: true }, { label: "", kind: vscode.QuickPickItemKind.Separator }, ...subdirectories.map((name) => ({ label: `$(folder) ${name}`, description: path.join(ctx.workspaceRoot!, name), })), ], { placeHolder: "Select a subdirectory or browse...", title: "Working Directory" }, ); if (!picked) return { ok: false, workDir: null }; let selectedUri: vscode.Uri; if (picked.label === "$(folder) Browse...") { const result = await vscode.window.showOpenDialog({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: workspaceUri, openLabel: "Select Working Directory", }); if (!result?.[0]) return { ok: false, workDir: null }; selectedUri = result[0]; } else if (picked.description) { const pickedUri = workDirUriFromPath(workspaceUri, ctx.workspaceRoot, picked.description); if (pickedUri === undefined) return { ok: false, workDir: null }; selectedUri = pickedUri; } else { return { ok: false, workDir: null }; } if (!(await isWorkspacePathContained(workspaceUri, selectedUri))) { await vscode.window.showWarningMessage("Selected directory must be within the workspace."); return { ok: false, workDir: null }; } const selected = selectedUri.fsPath; await ctx.setCustomWorkDir(selected === ctx.workspaceRoot ? null : selected); return { ok: true, workDir: selected }; }, [Methods.LoadKimiSessionHistory]: async (params: LoadHistoryParams, ctx) => { if (!ctx.workDir || !isSessionId(params.kimiSessionId)) return []; const runtime = await ctx.resumeSession(params.kimiSessionId); if (!areSameFsPath(runtime.session.workDir, ctx.workDir)) { await ctx.closeSession(); throw new Error("The selected session belongs to a different working directory."); } let history: ReturnType; try { const resumeState = runtime.session.getResumeState(); if (resumeState?.agents["main"] === undefined) { throw new Error("Session history is unavailable."); } history = replaySessionToWebviewEvents(resumeState, runtime.id); } catch (error) { await ctx.closeSession(); throw error; } ctx.fileManager.clearTracked(ctx.webviewId); const baseline = baselineSession(runtime.summary ?? { id: runtime.id, workDir: runtime.session.workDir, }); try { const changes = await ctx.baselineManager.getChanges(baseline); for (const change of changes) { ctx.fileManager.trackFile(ctx.webviewId, path.join(baseline.workDir, change.path)); } ctx.broadcast(Events.FileChangesUpdated, changes, ctx.webviewId); } catch (error) { ctx.logError("Unable to restore session file changes", error); ctx.broadcast(Events.FileChangesUpdated, [], ctx.webviewId); void Promise.resolve( vscode.window.showWarningMessage( "Kimi: This conversation opened, but its file change history is unavailable.", "Show Logs", ), ) .then((action) => { if (action === "Show Logs") ctx.showLogs(); }) .catch((noticeError: unknown) => { ctx.logError("Unable to show the file change warning", noticeError); }); } return history; }, [Methods.DeleteKimiSession]: async (params: DeleteSessionParams, ctx): Promise<{ ok: boolean }> => { if (!isSessionId(params.sessionId) || !ctx.workspaceRoot) return { ok: false }; const summary = (await ctx.harness.listSessions({ sessionId: params.sessionId }))[0]; if (summary === undefined || !isInsideOrEqual(ctx.workspaceRoot, summary.workDir)) { return { ok: false }; } const affectedViews = ctx.runtime.getSession(params.sessionId)?.subscribers ?? []; await ctx.runtime.deleteSession(params.sessionId); await ctx.baselineManager.deleteSession(params.sessionId); for (const webviewId of affectedViews) { ctx.fileManager.clearSession(webviewId); ctx.broadcast(Events.FileChangesUpdated, [], webviewId); if (webviewId !== ctx.webviewId) { ctx.broadcast(Events.NewConversation, {}, webviewId); } } return { ok: true }; }, [Methods.ForkKimiSession]: async (params: ForkSessionParams, ctx) => { if (!ctx.workDir || !isSessionId(params.sessionId) || !Number.isInteger(params.turnIndex) || params.turnIndex < 0) { return null; } const summaries = await ctx.harness.listSessions({ sessionId: params.sessionId }); const sourceSummary = summaries[0]; if ( sourceSummary === undefined || !ctx.workspaceRoot || !isInsideOrEqual(ctx.workspaceRoot, sourceSummary.workDir) ) return null; const forkSettledSession = async () => { const fork = await ctx.harness.forkSession({ id: params.sessionId, turnIndex: params.turnIndex }); const targetSummary = fork.summary; if (targetSummary === undefined) { await fork.close(); throw new Error("Forked session metadata is unavailable."); } let materializeError: unknown; try { await ctx.baselineManager.materializeToFork( baselineSession(sourceSummary), baselineSession(targetSummary), ); } catch (error) { materializeError = error; } try { await fork.close(); } catch (error) { ctx.logError("Unable to close a forked session", error); } if (materializeError !== undefined) { await ctx.harness.deleteSession(targetSummary.id).catch((error: unknown) => { ctx.logError(`Unable to remove failed fork "${targetSummary.id}"`, error); }); await ctx.baselineManager.deleteSession(targetSummary.id).catch((error: unknown) => { ctx.logError(`Unable to remove failed fork baseline "${targetSummary.id}"`, error); }); throw materializeError instanceof Error ? materializeError : new Error("Unable to materialize the forked session baseline.", { cause: materializeError, }); } return { sessionId: targetSummary.id }; }; const active = ctx.runtime.getSession(params.sessionId); return active === undefined ? forkSettledSession() : active.runExclusiveAfterCancelling(forkSettledSession); }, }; function toSessionInfo(summary: SessionSummary): SessionInfo { return { id: summary.id, workDir: summary.workDir, updatedAt: summary.updatedAt, brief: summary.title ?? summary.lastPrompt ?? "", }; } function baselineSession(summary: Pick): BaselineSession { return { id: summary.id, workDir: summary.workDir, metadata: summary.metadata }; } function isInsideOrEqual(root: string, candidate: string): boolean { return isFsPathInsideOrEqual(root, candidate); } function isSessionId(value: string): boolean { return SESSION_ID.test(value); }