Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8b5f2a6
feat(sdk): add Wire 1.5 plan mode and steer types to schema
tempurai Mar 18, 2026
30eee0b
feat(sdk): add sendSetPlanMode, sendSteer to ProtocolClient and bump …
tempurai Mar 18, 2026
8277298
feat(sdk): add Session.setPlanMode and Turn.steer for Wire 1.5
tempurai Mar 18, 2026
638c530
feat(sdk): export SteerInput and SetPlanModeResult types and schemas
tempurai Mar 18, 2026
63375e4
feat(vscode): add SetPlanMode and SteerChat bridge methods
tempurai Mar 18, 2026
22f535f
feat(vscode): wire up setPlanMode and steerChat chat handlers
tempurai Mar 18, 2026
9ca8195
feat(vscode): track plan mode state and handle SteerInput event
tempurai Mar 18, 2026
ef46816
feat(vscode): add PlanCard component for plan-mode step grouping
tempurai Mar 18, 2026
e70a123
feat(vscode): group plan-mode steps into PlanCard in ChatMessage
tempurai Mar 18, 2026
0f0b29b
feat(vscode): add PlanModeButton toggle in InputArea toolbar
tempurai Mar 18, 2026
cd21625
feat(vscode): add "Insert Now" steer button to QueuedMessagesPanel
tempurai Mar 18, 2026
2ae48e6
fix(vscode): restore step spacing broken by plan-mode grouping
tempurai Mar 18, 2026
da27773
feat(vscode): render SteerInput as inline user bubble in assistant turn
tempurai Mar 18, 2026
706ff59
feat(vscode): add collapsible toggle to PlanCard, default expanded
tempurai Mar 18, 2026
9e71cc6
feat(vscode): confirm before exiting plan mode during streaming
tempurai Mar 18, 2026
4278119
chore: bump version to 0.1.6 for kimi-agent-sdk and 0.4.5 for vscode …
tempurai Mar 18, 2026
0edebb0
feat(vscode): add Alpine Linux (alpine-x64, alpine-arm64) platform ta…
tempurai Mar 18, 2026
eb3ba0c
feat(vscode): add Alpine Linux (alpine-x64, alpine-arm64) platform ta…
tempurai Mar 18, 2026
26f3947
feat(vscode): add Alpine Linux (alpine-x64, alpine-arm64) platform ta…
tempurai Mar 18, 2026
9802393
fix(vscode): remove Alpine from vsix package targets
tempurai Mar 18, 2026
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
4 changes: 4 additions & 0 deletions node/agent_sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export type {
QuestionResponseSchema,
ClientInfo,
ServerInfo,
SteerInput,
SetPlanModeResult,
} from "./schema";

// Schemas
Expand All @@ -129,6 +131,8 @@ export {
SlashCommandInfoSchema,
parseEventPayload,
parseRequestPayload,
SteerInputSchema,
SetPlanModeResultSchema,
} from "./schema";

// Protocol
Expand Down
2 changes: 1 addition & 1 deletion node/agent_sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@moonshot-ai/kimi-agent-sdk",
"version": "0.1.5",
"version": "0.1.6",
"description": "SDK for interacting with Kimi Code CLI",
"license": "MIT",
"repository": {
Expand Down
20 changes: 19 additions & 1 deletion node/agent_sdk/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ import {
type ExternalTool,
type ToolCallRequest,
type ToolReturnValue,
SetPlanModeResultSchema,
type SetPlanModeResult,
} from "./schema";
import { TransportError, ProtocolError, CliError } from "./errors";
import { log } from "./logger";

const PROTOCOL_VERSION = "1.4";
const PROTOCOL_VERSION = "1.5";
const SDK_NAME = "kimi-agent-sdk";

declare const __SDK_VERSION__: string;
Expand Down Expand Up @@ -272,6 +274,21 @@ export class ProtocolClient {
return Promise.resolve();
}

sendSetPlanMode(enabled: boolean): Promise<SetPlanModeResult> {
return this.sendRequest("set_plan_mode", { enabled })
.then((res) => {
const parsed = SetPlanModeResultSchema.safeParse(res);
if (!parsed.success) {
throw new ProtocolError("SCHEMA_MISMATCH", `Invalid set_plan_mode response: ${parsed.error.message}`);
}
return parsed.data;
});
}

sendSteer(content: string | ContentPart[]): Promise<void> {
return this.sendRequest("steer", { user_input: content }).then(() => {});
}

private async sendInitialize(externalTools?: ExternalTool[], clientInfo?: ClientInfo): Promise<InitializeResult> {
let clientName = `${SDK_NAME}/${SDK_VERSION}`;
if (clientInfo?.name && clientInfo?.version) {
Expand All @@ -286,6 +303,7 @@ export class ProtocolClient {
},
capabilities: {
supports_question: true,
supports_plan_mode: true,
},
};

Expand Down
30 changes: 29 additions & 1 deletion node/agent_sdk/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,12 @@ export const ExternalToolsResultSchema = z.object({
});
export type ExternalToolsResult = z.infer<typeof ExternalToolsResultSchema>;

// Client capabilities (Wire 1.4)
// Client capabilities (Wire 1.4+)
export const ClientCapabilitiesSchema = z.object({
// Whether the client supports handling QuestionRequest messages
supports_question: z.boolean().optional(),
// Whether the client supports plan mode (Wire 1.5)
supports_plan_mode: z.boolean().optional(),
});
export type ClientCapabilities = z.infer<typeof ClientCapabilitiesSchema>;

Expand Down Expand Up @@ -345,6 +347,28 @@ export const QuestionResponseSchema = z.object({
});
export type QuestionResponse = z.infer<typeof QuestionResponseSchema>;

// ============================================================================
// Steer Input (Wire 1.5)
// ============================================================================

// Server→client echo event emitted after consuming each steer
export const SteerInputSchema = z.object({
// User steer input, can be plain text or array of content parts
user_input: z.union([z.string(), z.array(ContentPartSchema)]),
});
export type SteerInput = z.infer<typeof SteerInputSchema>;

// ============================================================================
// SetPlanModeResult (Wire 1.5)
// ============================================================================

// Result of a SetPlanMode request (status: "ok" only; failures use JSON-RPC error)
export const SetPlanModeResultSchema = z.object({
status: z.literal("ok"),
plan_mode: z.boolean(),
});
export type SetPlanModeResult = z.infer<typeof SetPlanModeResultSchema>;

// ============================================================================
// Wire Events
// ============================================================================
Expand Down Expand Up @@ -383,6 +407,8 @@ export const StatusUpdateSchema = z.object({
token_usage: TokenUsageSchema.nullable().optional(),
// Message ID for the current step
message_id: z.string().nullable().optional(),
// Whether plan mode is active (null = unchanged, undefined = not sent)
plan_mode: z.boolean().nullable().optional(),
});
export type StatusUpdate = z.infer<typeof StatusUpdateSchema>;

Expand Down Expand Up @@ -445,6 +471,7 @@ export type WireEvent =
| { type: "ToolCall"; payload: ToolCall }
| { type: "ToolCallPart"; payload: ToolCallPart }
| { type: "ToolResult"; payload: ToolResult }
| { type: "SteerInput"; payload: SteerInput }
| { type: "SubagentEvent"; payload: SubagentEvent }
| { type: "ApprovalResponse"; payload: ApprovalResponseEvent }
| { type: "ParseError"; payload: ParseErrorPayload };
Expand All @@ -468,6 +495,7 @@ export const EventSchemas: Record<string, z.ZodSchema> = {
ToolCallPart: ToolCallPartSchema,
ToolResult: ToolResultSchema,
ApprovalResponse: ApprovalResponseEventSchema,
SteerInput: SteerInputSchema,
};

// Request type -> schema mapping
Expand Down
30 changes: 30 additions & 0 deletions node/agent_sdk/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface Turn {
approve(requestId: string, response: ApprovalResponse): Promise<void>;
/** Respond to question request (Wire 1.4) */
respondQuestion(rpcRequestId: string, questionRequestId: string, answers: Record<string, string>): Promise<void>;
/** Steer the current turn with additional user input (Wire 1.5) */
steer(content: string | ContentPart[]): Promise<void>;
/** Promise of the result after the turn is completed */
readonly result: Promise<RunResult>;
}
Expand All @@ -53,6 +55,10 @@ export interface Session {
env: Record<string, string>;
// Exported external tools
externalTools: ExternalTool[];
/** Whether plan mode is currently enabled */
readonly planMode: boolean;
/** Toggle plan mode on/off */
setPlanMode(enabled: boolean): Promise<boolean>;
/** Send a message, returns a Turn object */
prompt(content: string | ContentPart[]): Turn;
/** Close the session, release resources */
Expand Down Expand Up @@ -135,6 +141,14 @@ class TurnImpl implements Turn {
}
return client.sendQuestionResponse(rpcRequestId, questionRequestId, answers);
}

async steer(content: string | ContentPart[]): Promise<void> {
const client = this.getCurrentClient();
if (!client?.isRunning) {
throw new SessionError("SESSION_CLOSED", "Cannot steer: no active client");
}
return client.sendSteer(content);
}
}

class SessionImpl implements Session {
Expand All @@ -152,6 +166,7 @@ class SessionImpl implements Session {
private _skillsDir?: string;
private _shareDir?: string;
private _slashCommands: SlashCommandInfo[] = [];
private _planMode: boolean = false;

private _state: SessionState = "idle";

Expand Down Expand Up @@ -225,6 +240,21 @@ class SessionImpl implements Session {
set externalTools(v: ExternalTool[]) {
this._externalTools = v;
}
get planMode(): boolean {
return this._planMode;
}

async setPlanMode(enabled: boolean): Promise<boolean> {
if (this._state === "closed") {
throw new SessionError("SESSION_CLOSED", "Session is closed");
}
if (!this.client?.isRunning) {
throw new SessionError("SESSION_CLOSED", "Cannot set plan mode: no active client");
}
const result = await this.client.sendSetPlanMode(enabled);
this._planMode = result.plan_mode;
return this._planMode;
}

prompt(content: string | ContentPart[]): Turn {
if (this._state === "closed") {
Expand Down
86 changes: 86 additions & 0 deletions node/agent_sdk/tests/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
RpcMessageSchema,
parseEventPayload,
parseRequestPayload,
SteerInputSchema,
SetPlanModeResultSchema,
ClientCapabilitiesSchema,
type ContentPart,
type DisplayBlock,
type UnknownBlock,
Expand Down Expand Up @@ -619,6 +622,89 @@ describe("SubagentEvent parsing", () => {
});
});

// ============================================================================
// SteerInput Tests
// ============================================================================
describe("SteerInputSchema", () => {
it("parses string user_input", () => {
const input = { user_input: "change the approach" };
const result = SteerInputSchema.safeParse(input);
expect(result.success).toBe(true);
expect(result.data).toEqual(input);
});
it("parses ContentPart[] user_input", () => {
const input = { user_input: [{ type: "text", text: "hello" }] };
const result = SteerInputSchema.safeParse(input);
expect(result.success).toBe(true);
});
});

// ============================================================================
// SetPlanModeResult Tests
// ============================================================================
describe("SetPlanModeResultSchema", () => {
it("parses valid result", () => {
const input = { status: "ok", plan_mode: true };
const result = SetPlanModeResultSchema.safeParse(input);
expect(result.success).toBe(true);
expect(result.data).toEqual(input);
});
it("rejects invalid status", () => {
const input = { status: "error", plan_mode: true };
const result = SetPlanModeResultSchema.safeParse(input);
expect(result.success).toBe(false);
});
});

// ============================================================================
// StatusUpdate plan_mode Tests
// ============================================================================
describe("StatusUpdateSchema plan_mode", () => {
it("parses with plan_mode true", () => {
const input = { plan_mode: true };
const result = StatusUpdateSchema.safeParse(input);
expect(result.success).toBe(true);
expect(result.data?.plan_mode).toBe(true);
});
it("parses with plan_mode null (unchanged)", () => {
const input = { plan_mode: null };
const result = StatusUpdateSchema.safeParse(input);
expect(result.success).toBe(true);
expect(result.data?.plan_mode).toBeNull();
});
it("parses without plan_mode (backwards compat)", () => {
const input = { context_usage: 0.5 };
const result = StatusUpdateSchema.safeParse(input);
expect(result.success).toBe(true);
expect(result.data?.plan_mode).toBeUndefined();
});
});

// ============================================================================
// parseEventPayload SteerInput Tests
// ============================================================================
describe("parseEventPayload SteerInput", () => {
it("parses SteerInput event", () => {
const result = parseEventPayload("SteerInput", { user_input: "redirect" });
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value.type).toBe("SteerInput");
}
});
});

// ============================================================================
// ClientCapabilities Tests
// ============================================================================
describe("ClientCapabilitiesSchema", () => {
it("parses with supports_plan_mode", () => {
const input = { supports_question: true, supports_plan_mode: true };
const result = ClientCapabilitiesSchema.safeParse(input);
expect(result.success).toBe(true);
expect(result.data?.supports_plan_mode).toBe(true);
});
});

// ============================================================================
// JSON Round-trip Tests
// ============================================================================
Expand Down
2 changes: 1 addition & 1 deletion node/vscode_extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publisher": "moonshot-ai",
"displayName": "Kimi Code (Technical Preview)",
"description": "Official Kimi Code plugin for VS Code",
"version": "0.4.4",
"version": "0.4.5",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand Down
3 changes: 3 additions & 0 deletions node/vscode_extension/scripts/download-cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const PLATFORMS = {
"darwin-x64": { target: "x86_64-apple-darwin-onedir", ext: "tar.gz" },
"linux-arm64": { target: "aarch64-unknown-linux-gnu-onedir", ext: "tar.gz" },
"linux-x64": { target: "x86_64-unknown-linux-gnu-onedir", ext: "tar.gz" },
"alpine-x64": { target: null, ext: null }, // No native CLI; UV fallback at runtime
"alpine-arm64": { target: null, ext: null }, // No native CLI; UV fallback at runtime
"win32-x64": { target: "x86_64-pc-windows-msvc-onedir", ext: "zip" },
"win32-arm64": { target: "aarch64-pc-windows-msvc-onedir", ext: "zip" },
};
Expand Down Expand Up @@ -45,6 +47,7 @@ async function buildManifest(release, bundledPlatform) {
const platforms = {};

for (const [key, info] of Object.entries(PLATFORMS)) {
if (!info.target) continue; // No native binary for this platform (e.g. Alpine)
const filename = `kimi-${version}-${info.target}.${info.ext}`;
const asset = release.assets.find((a) => a.name === filename);
const sha256Asset = release.assets.find((a) => a.name === `${filename}.sha256`);
Expand Down
2 changes: 2 additions & 0 deletions node/vscode_extension/shared/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const Methods = {
StreamChat: "streamChat",
AbortChat: "abortChat",
ResetSession: "resetSession",
SetPlanMode: "setPlanMode",
SteerChat: "steerChat",
RespondApproval: "respondApproval",

GetKimiSessions: "getKimiSessions",
Expand Down
28 changes: 28 additions & 0 deletions node/vscode_extension/src/handlers/chat.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,32 @@ const respondQuestion: Handler<RespondQuestionParams, { ok: boolean }> = async (
return { ok: true };
};

interface SetPlanModeParams {
enabled: boolean;
}

const setPlanMode: Handler<SetPlanModeParams, { ok: boolean; planMode: boolean }> = async (params, ctx) => {
const session = ctx.getSession();
if (!session) {
return { ok: false, planMode: false };
}
const planMode = await session.setPlanMode(params.enabled);
return { ok: true, planMode };
};

interface SteerChatParams {
content: string | ContentPart[];
}

const steerChat: Handler<SteerChatParams, { ok: boolean }> = async (params, ctx) => {
const turn = ctx.getTurn();
if (!turn) {
return { ok: false };
}
await turn.steer(params.content);
return { ok: true };
};

const resetSession: Handler<void, { ok: boolean }> = async (_, ctx) => {
const session = ctx.getSession();
if (session) {
Expand All @@ -298,5 +324,7 @@ export const chatHandlers: Record<string, Handler<any, any>> = {
[Methods.AbortChat]: abortChat,
[Methods.RespondApproval]: respondApproval,
[Methods.RespondQuestion]: respondQuestion,
[Methods.SetPlanMode]: setPlanMode,
[Methods.SteerChat]: steerChat,
[Methods.ResetSession]: resetSession,
};
Loading
Loading