Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/vscode/shared/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const Methods = {
GetProjectFiles: "getProjectFiles",
PickMedia: "pickMedia",
OpenFile: "openFile",
OpenPlanFile: "openPlanFile",
CheckFileExists: "checkFileExists",
CheckFilesExist: "checkFilesExist",
OpenFileDiff: "openFileDiff",
Expand Down Expand Up @@ -211,6 +212,8 @@ function validateParams(method: RpcMethod, params: unknown): boolean {
case Methods.CheckFileExists:
case Methods.GetImageDataUri:
return hasString(params, "filePath");
case Methods.OpenPlanFile:
return hasNonEmptyString(params, "reference");
case Methods.CheckFilesExist:
case Methods.TrackFiles:
return hasStringArray(params, "paths");
Expand Down
15 changes: 14 additions & 1 deletion apps/vscode/shared/legacy-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,26 @@ export interface ShellBlock {
command: string;
}

export interface PlanBlock {
type: 'plan';
plan: string;
path?: string;
reference?: string;
}

export interface UnknownBlock {
type: string;
data?: Record<string, unknown>;
[key: string]: unknown;
}

export type DisplayBlock = BriefBlock | DiffBlock | TodoBlock | ShellBlock | UnknownBlock;
export type DisplayBlock =
| BriefBlock
| DiffBlock
| TodoBlock
| ShellBlock
| PlanBlock
| UnknownBlock;

export interface ToolCall {
type: 'function';
Expand Down
16 changes: 16 additions & 0 deletions apps/vscode/src/handlers/file.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ interface GetProjectFilesParams {
}
interface PickMediaParams { maxCount?: number; includeVideo?: boolean }
interface FilePathParams { filePath: string }
interface PlanReferenceParams { reference: string }
interface OptionalFilePathParams { filePath?: string }
interface PathsParams { paths: string[] }
interface CheckFilesExistParams { paths: string[] }
Expand Down Expand Up @@ -75,6 +76,20 @@ const openFile: Handler<FilePathParams, { ok: boolean }> = async ({ filePath },
return { ok: true };
};

const openPlanFile: Handler<PlanReferenceParams, { ok: boolean }> = async ({ reference }, ctx) => {
const planPath = ctx.runtime.resolvePlanFile(reference);
if (planPath === undefined) return { ok: false };

const uri = vscode.Uri.file(planPath);
try {
await vscode.workspace.fs.stat(uri);
} catch {
return { ok: false };
}
await vscode.commands.executeCommand("vscode.open", uri);
return { ok: true };
};

const openFileDiff: Handler<FilePathParams, { ok: boolean }> = async ({ filePath }, ctx) => {
const sessionId = ctx.getSessionId();
const resolved = await resolveExistingWorkspaceFile(ctx.requireWorkDirUri(), filePath);
Expand Down Expand Up @@ -180,6 +195,7 @@ export const fileHandlers: Record<string, Handler<any, any>> = {
[Methods.GetProjectFiles]: getProjectFiles,
[Methods.PickMedia]: pickMedia,
[Methods.OpenFile]: openFile,
[Methods.OpenPlanFile]: openPlanFile,
[Methods.OpenFileDiff]: openFileDiff,
[Methods.TrackFiles]: trackFiles,
[Methods.ClearTrackedFiles]: clearTrackedFiles,
Expand Down
6 changes: 5 additions & 1 deletion apps/vscode/src/handlers/session.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,11 @@ export const sessionHandlers: Record<string, Handler<any, any>> = {
if (resumeState?.agents["main"] === undefined) {
throw new Error("Session history is unavailable.");
}
history = replaySessionToWebviewEvents(resumeState, runtime.id);
history = replaySessionToWebviewEvents(
resumeState,
runtime.id,
(filePath) => ctx.runtime.registerPlanFile(filePath),
);
} catch (error) {
await ctx.closeSession();
throw error;
Expand Down
15 changes: 12 additions & 3 deletions apps/vscode/src/runtime/event-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type {
TurnBegin,
} from '../../shared/legacy-sdk';
import type { ErrorPhase, UIStreamEvent } from '../../shared/types';
import { toLegacyDisplay } from './tool-display';
import { toLegacyDisplay, type PlanFileRegistrar } from './tool-display';

const DEFAULT_MAIN_AGENT_ID = 'main';

Expand Down Expand Up @@ -66,6 +66,7 @@ export interface AdaptSdkEventOptions {
readonly pendingInput?: TurnBegin['user_input'];
readonly mainAgentId?: string;
readonly errorPhase?: ErrorPhase;
readonly registerPlanFile?: PlanFileRegistrar;
}

export function createEventAdapterState(): EventAdapterState {
Expand Down Expand Up @@ -154,7 +155,12 @@ export function adaptSdkEvent(
};
}

const mapped = mapLegacyWireEvent(state, sdkEvent, mainAgentId);
const mapped = mapLegacyWireEvent(
state,
sdkEvent,
mainAgentId,
options.registerPlanFile,
);
if (mapped.event === undefined) return { state: mapped.state };

const routed = routeSubagentEvent(
Expand Down Expand Up @@ -197,6 +203,7 @@ function mapLegacyWireEvent(
state: EventAdapterState,
sdkEvent: Event,
mainAgentId: string,
registerPlanFile?: PlanFileRegistrar,
): MappedLegacyWireEvent {
switch (sdkEvent.type) {
case 'turn.step.started':
Expand Down Expand Up @@ -245,7 +252,9 @@ function mapLegacyWireEvent(
sdkEvent.toolCallId,
mainAgentId,
);
const display = sdkEvent.display === undefined ? undefined : toLegacyDisplay(sdkEvent.display);
const display = sdkEvent.display === undefined
? undefined
: toLegacyDisplay(sdkEvent.display, registerPlanFile);
return {
state: display === undefined
? state
Expand Down
21 changes: 21 additions & 0 deletions apps/vscode/src/runtime/kimi-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { randomUUID } from "node:crypto";

import {
createKimiHarness,
type KimiHarness,
Expand Down Expand Up @@ -49,6 +51,8 @@ export class KimiRuntime {
private readonly log: KimiRuntimeOptions["log"];
private readonly sessions = new Map<string, SessionRuntime>();
private readonly sessionByView = new Map<string, string>();
private readonly planFiles = new Map<string, string>();
private readonly planReferences = new Map<string, string>();
private closed = false;

constructor(options: KimiRuntimeOptions) {
Expand Down Expand Up @@ -76,6 +80,20 @@ export class KimiRuntime {
return this.sessions.get(id);
}

registerPlanFile(filePath: string): string {
const existing = this.planReferences.get(filePath);
if (existing !== undefined) return existing;

const reference = randomUUID();
this.planFiles.set(reference, filePath);
this.planReferences.set(filePath, reference);
return reference;
}

resolvePlanFile(reference: string): string | undefined {
return this.planFiles.get(reference);
}

async openSession(options: OpenSessionOptions): Promise<SessionRuntime> {
this.ensureOpen();
const current = this.getSessionForView(options.webviewId);
Expand Down Expand Up @@ -218,6 +236,8 @@ export class KimiRuntime {
await Promise.all([...this.sessions.values()].map((session) => session.close()));
this.sessions.clear();
this.sessionByView.clear();
this.planFiles.clear();
this.planReferences.clear();
await this.harness.close();
}

Expand All @@ -227,6 +247,7 @@ export class KimiRuntime {
legacyApproval,
broadcast: this.broadcast,
captureBaseline: this.captureBaseline,
registerPlanFile: (filePath) => this.registerPlanFile(filePath),
log: this.log,
});
this.sessions.set(session.id, runtime);
Expand Down
32 changes: 26 additions & 6 deletions apps/vscode/src/runtime/replay-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {
} from "../../shared/legacy-sdk";
import type { UIStreamEvent } from "../../shared/types";
import { toLegacyToolName } from "./event-adapter";
import { toLegacyDisplay } from "./tool-display";
import { toLegacyDisplay, type PlanFileRegistrar } from "./tool-display";

interface SubagentReplayInvocation {
readonly parentAgentId: string;
Expand All @@ -35,24 +35,32 @@ interface SubagentReplayIndex {
export function replaySessionToWebviewEvents(
state: ResumedSessionState,
sessionId: string,
registerPlanFile?: PlanFileRegistrar,
): UIStreamEvent[] {
const main = state.agents["main"];
if (main === undefined) throw new Error("Session history is unavailable.");
return replayAgentToWebviewEvents(main, sessionId, buildSubagentReplayIndex(state));
return replayAgentToWebviewEvents(
main,
sessionId,
buildSubagentReplayIndex(state),
registerPlanFile,
);
}

/** Projects the public SDK resume replay into the released Webview protocol. */
export function replayToWebviewEvents(
agent: ResumedAgentState,
sessionId: string,
registerPlanFile?: PlanFileRegistrar,
): UIStreamEvent[] {
return replayAgentToWebviewEvents(agent, sessionId);
return replayAgentToWebviewEvents(agent, sessionId, undefined, registerPlanFile);
}

function replayAgentToWebviewEvents(
agent: ResumedAgentState,
sessionId: string,
subagents?: SubagentReplayIndex,
registerPlanFile?: PlanFileRegistrar,
): UIStreamEvent[] {
const events: UIStreamEvent[] = [];
let turnOpen = false;
Expand Down Expand Up @@ -130,7 +138,7 @@ function replayAgentToWebviewEvents(
for (const call of message.toolCalls) {
const display = message.toolCallDisplays?.[call.id];
if (display !== undefined) {
toolDisplays.set(call.id, toLegacyDisplay(display));
toolDisplays.set(call.id, toLegacyDisplay(display, registerPlanFile));
}
const toolCall: ToolCall = {
type: "function",
Expand All @@ -148,6 +156,7 @@ function replayAgentToWebviewEvents(
call.id,
[],
new Set(),
registerPlanFile,
)) {
events.push(withSession(nested, sessionId));
}
Expand Down Expand Up @@ -314,6 +323,7 @@ function renderSubagentInvocations(
parentToolCallId: string,
parentChain: readonly SubagentReplayInvocation[],
visited: ReadonlySet<string>,
registerPlanFile?: PlanFileRegistrar,
): LegacyWireEvent[] {
const result: LegacyWireEvent[] = [];
const invocations = index.byParentCall.get(parentCallKey(parentAgentId, parentToolCallId)) ?? [];
Expand All @@ -322,7 +332,13 @@ function renderSubagentInvocations(
if (visited.has(invocationKey)) continue;
const nextVisited = new Set([...visited, invocationKey]);
result.push(
...renderSubagentInvocation(index, invocation, [invocation, ...parentChain], nextVisited),
...renderSubagentInvocation(
index,
invocation,
[invocation, ...parentChain],
nextVisited,
registerPlanFile,
),
);
}
return result;
Expand All @@ -333,6 +349,7 @@ function renderSubagentInvocation(
invocation: SubagentReplayInvocation,
chain: readonly SubagentReplayInvocation[],
visited: ReadonlySet<string>,
registerPlanFile?: PlanFileRegistrar,
): LegacyWireEvent[] {
const events: LegacyWireEvent[] = [];
const toolDisplays = new Map<string, readonly DisplayBlock[]>();
Expand All @@ -359,7 +376,9 @@ function renderSubagentInvocation(
for (const call of message.toolCalls) {
const toolCallId = scopedReplayToolCallId(invocation.childAgentId, call.id);
const display = message.toolCallDisplays?.[call.id];
if (display !== undefined) toolDisplays.set(toolCallId, toLegacyDisplay(display));
if (display !== undefined) {
toolDisplays.set(toolCallId, toLegacyDisplay(display, registerPlanFile));
}
emit({
type: "ToolCall",
payload: {
Expand All @@ -378,6 +397,7 @@ function renderSubagentInvocation(
call.id,
chain,
visited,
registerPlanFile,
),
);
}
Expand Down
4 changes: 4 additions & 0 deletions apps/vscode/src/runtime/session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface SessionRuntimeOptions {
filePath: string,
webviewIds: readonly string[],
) => void;
readonly registerPlanFile?: (filePath: string) => string;
readonly log: (message: string, error?: unknown) => void;
}

Expand Down Expand Up @@ -72,6 +73,7 @@ export class SessionRuntime {

private readonly broadcast: RuntimeBroadcast;
private readonly captureBaseline: SessionRuntimeOptions["captureBaseline"];
private readonly registerPlanFile: SessionRuntimeOptions["registerPlanFile"];
private readonly log: SessionRuntimeOptions["log"];
private readonly webviewIds = new Set<string>();
private readonly reverseRpc: ReverseRpcController;
Expand All @@ -94,6 +96,7 @@ export class SessionRuntime {
this.session = options.session;
this.broadcast = options.broadcast;
this.captureBaseline = options.captureBaseline;
this.registerPlanFile = options.registerPlanFile;
this.log = options.log;
this.legacyApproval = options.legacyApproval;
this.reverseRpc = new ReverseRpcController((event) => this.emitStreamEvent(event));
Expand Down Expand Up @@ -470,6 +473,7 @@ export class SessionRuntime {
const adapted = adaptSdkEvent(this.adapterState, event, {
pendingInput,
errorPhase: this.activePrompt?.started === false ? "preflight" : "runtime",
registerPlanFile: this.registerPlanFile,
});
this.adapterState = adapted.state;

Expand Down
17 changes: 15 additions & 2 deletions apps/vscode/src/runtime/tool-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { ToolInputDisplay } from "@moonshot-ai/kimi-code-sdk";

import type { DisplayBlock } from "../../shared/legacy-sdk";

export type PlanFileRegistrar = (filePath: string) => string;

export function describeToolDisplay(display: ToolInputDisplay): string {
switch (display.kind) {
case "command":
Expand Down Expand Up @@ -33,7 +35,10 @@ export function describeToolDisplay(display: ToolInputDisplay): string {
}
}

export function toLegacyDisplay(display: ToolInputDisplay): DisplayBlock[] {
export function toLegacyDisplay(
display: ToolInputDisplay,
registerPlanFile?: PlanFileRegistrar,
): DisplayBlock[] {
switch (display.kind) {
case "command":
return [{ type: "shell", language: display.language ?? "bash", command: display.command }];
Expand Down Expand Up @@ -67,9 +72,17 @@ export function toLegacyDisplay(display: ToolInputDisplay): DisplayBlock[] {
case "skill_call":
case "task":
case "task_stop":
case "plan_review":
case "goal_start":
case "generic":
return [{ type: "brief", text: describeToolDisplay(display) }];
case "plan_review":
return [{
type: "plan",
plan: display.plan,
path: display.path,
...(display.path === undefined || registerPlanFile === undefined
? {}
: { reference: registerPlanFile(display.path) }),
}];
}
}
Loading