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
5 changes: 5 additions & 0 deletions .changeset/fix-web-first-prompt-plan-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---

Fix Plan Mode not taking effect on the first prompt of a new Web session.
9 changes: 9 additions & 0 deletions packages/kap-server/src/routes/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
IBootstrapService,
IAgentLifecycleService,
IAgentPermissionModeService,
IAgentPlanService,
IAgentProfileService,
IAgentToolPolicyService,
IAgentPromptService,
Expand Down Expand Up @@ -119,6 +120,7 @@ async function resolvePromptFromSession(session: ISessionScopeHandle, agentId?:
profile: agent.accessor.get(IAgentProfileService),
toolPolicy: agent.accessor.get(IAgentToolPolicyService),
permissionMode: agent.accessor.get(IAgentPermissionModeService),
plan: agent.accessor.get(IAgentPlanService),
};
}

Expand Down Expand Up @@ -257,6 +259,13 @@ export function registerPromptsRoutes(app: PromptRouteHost, core: Scope): void {
if (req.body.thinking !== undefined && !thinkingConsumed)
resolved.profile.setThinking(req.body.thinking);
if (req.body.permission_mode !== undefined) resolved.permissionMode.setMode(req.body.permission_mode);
if (req.body.plan_mode !== undefined) {
const active = (await resolved.plan.status()) !== null;
if (active !== req.body.plan_mode) {
if (req.body.plan_mode) await resolved.plan.enter();
else resolved.plan.exit();
}
}
if (req.body.disabled_tools !== undefined) {
// A session denylist before bind throws `profile.not_bound` — map it
// onto 40001 like the profile-selection errors above.
Expand Down
42 changes: 42 additions & 0 deletions packages/kap-server/test/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,48 @@ describe('server-v2 /api/v1 prompts', () => {
expect(Array.isArray(list.body.data.queued)).toBe(true);
});

it('enters plan mode when the first prompt requests it', async () => {
const id = await createSession(home as string);

const submitted = await call<PromptItemWire>('POST', `/api/v1/sessions/${id}/prompts`, {
model: 'stub',
plan_mode: true,
content: [{ type: 'text', text: 'investigate the bug' }],
});
expect(submitted.body.code).toBe(0);

const status = await call<{ plan_mode: boolean }>(
'GET',
`/api/v1/sessions/${id}/status`,
);
expect(status.body.code).toBe(0);
expect(status.body.data.plan_mode).toBe(true);
});

it('exits plan mode when a prompt disables it', async () => {
const id = await createSession(home as string);
await createMainAgent(id);

const profile = await call<unknown>('POST', `/api/v1/sessions/${id}/profile`, {
agent_config: { plan_mode: true },
});
expect(profile.body.code).toBe(0);

const submitted = await call<PromptItemWire>('POST', `/api/v1/sessions/${id}/prompts`, {
model: 'stub',
plan_mode: false,
content: [{ type: 'text', text: 'implement the fix' }],
});
expect(submitted.body.code).toBe(0);

const status = await call<{ plan_mode: boolean }>(
'GET',
`/api/v1/sessions/${id}/status`,
);
expect(status.body.code).toBe(0);
expect(status.body.data.plan_mode).toBe(false);
});

it('rejects a stale file reference without creating the agent or mutating the model', async () => {
const id = await createSession(home as string);
const session = getLiveSessionById(server!.core.accessor, id);
Expand Down