Merge pull request #6240 from Kilo-Org/fix/fix-plan-mode-suggestions

fix(plan): Show implementation suggestions only when LLM has all the information
This commit is contained in:
Marian Alexandru Alecu
2026-02-25 12:18:04 +02:00
committed by GitHub
10 changed files with 579 additions and 72 deletions
@@ -149,26 +149,24 @@ export function Prompt(props: PromptProps) {
),
)
// Initialize agent/model/variant from last user message when session changes
let syncedSessionID: string | undefined
// kilocode_change start - sync local agent/model whenever newest user message changes
let syncedKey: string | undefined
createEffect(() => {
const sessionID = props.sessionID
const msg = lastUserMessage()
if (!sessionID || !msg) return
if (sessionID !== syncedSessionID) {
if (!sessionID || !msg) return
const key = [sessionID, msg.id].join(":")
if (key === syncedKey) return
syncedKey = key
syncedSessionID = sessionID
// Only set agent if it's a primary agent (not a subagent)
const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent)
if (msg.agent && isPrimaryAgent) {
local.agent.set(msg.agent)
if (msg.model) local.model.set(msg.model)
if (msg.variant) local.model.variant.set(msg.variant)
}
}
const isPrimaryAgent = local.agent.list().some((x) => x.name === msg.agent)
if (!msg.agent || !isPrimaryAgent) return
local.agent.set(msg.agent)
if (msg.model) local.model.set(msg.model)
if (msg.variant) local.model.variant.set(msg.variant)
})
// kilocode_change end
command.register(() => {
return [
@@ -213,10 +213,8 @@ export function Session() {
if (part.state.status !== "completed") return
if (part.id === lastSwitch) return
if (part.tool === "plan_exit") {
local.agent.set("build")
lastSwitch = part.id
} else if (part.tool === "plan_enter") {
// kilocode_change - plan_exit no longer switches agent; PlanFollowup handles it
if (part.tool === "plan_enter") {
local.agent.set("plan")
lastSwitch = part.id
}
@@ -111,6 +111,30 @@ export namespace PlanFollowup {
export const ANSWER_NEW_SESSION = "Start new session"
export const ANSWER_CONTINUE = "Continue here"
async function resolvePlan(input: { assistant?: MessageV2.WithParts; messages: MessageV2.WithParts[]; sessionID: string }) {
// Fast path: check the last assistant message's text first (avoids array scanning)
if (input.assistant) {
const text = toText(input.assistant)
if (text) return text
}
// Fallback: scan all assistant messages after the last user message (handles
// cases where plan text is on an earlier assistant and the last one is empty)
const lastUserIdx = input.messages.findLastIndex((m) => m.info.role === "user")
const assistantMessages = input.messages
.slice(lastUserIdx + 1)
.filter((m) => m.info.role === "assistant")
const text = assistantMessages.map(toText).filter(Boolean).join("\n\n").trim()
if (text) return text
// Fall back to plan file on disk
const session = await Session.get(input.sessionID)
const file = Bun.file(Session.plan(session))
const plan = await file.text().catch(() => "")
return plan.trim()
}
async function inject(input: {
sessionID: string
agent: string
@@ -231,7 +255,7 @@ export namespace PlanFollowup {
const assistant = latest.find((msg) => msg.info.role === "assistant")
if (!assistant) return "break"
const plan = toText(assistant)
const plan = await resolvePlan({ assistant, messages: input.messages, sessionID: input.sessionID })
if (!plan) return "break"
const user = latest.find((msg) => msg.info.role === "user")?.info
+15 -2
View File
@@ -61,6 +61,19 @@ IMPORTANT:
const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`
export namespace SessionPrompt {
// kilocode_change start - share follow-up trigger logic with tests
export function shouldAskPlanFollowup(input: { messages: MessageV2.WithParts[]; abort: AbortSignal }) {
if (input.abort.aborted) return false
if (!["cli", "vscode"].includes(Flag.KILO_CLIENT)) return false
const lastUserIdx = input.messages.findLastIndex((m) => m.info.role === "user")
return input.messages
.slice(lastUserIdx + 1)
.some((msg) =>
msg.parts.some((p) => p.type === "tool" && p.tool === "plan_exit" && p.state.status === "completed"),
)
}
// kilocode_change end
const log = Log.create({ service: "session.prompt" })
const state = Instance.state(
@@ -346,8 +359,8 @@ export namespace SessionPrompt {
!["tool-calls", "unknown"].includes(lastAssistant.finish) &&
lastUser.id < lastAssistant.id
) {
// kilocode_change start - ask follow-up after plan agent completes
if (lastUser.agent === "plan" && !abort.aborted && ["cli", "vscode"].includes(Flag.KILO_CLIENT)) {
// kilocode_change start - ask follow-up when plan_exit tool was called
if (shouldAskPlanFollowup({ messages: msgs, abort })) {
const action = await PlanFollowup.ask({ sessionID, messages: msgs, abort })
if (action === "continue") continue
}
@@ -23,4 +23,6 @@ Ask the user clarifying questions or ask for their opinion when weighing tradeof
## Important
The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
When you have finalized your plan and are confident it is ready for implementation, call the plan_exit tool to signal completion. Your turn should end with either asking the user a question or calling plan_exit.
</system-reminder>
+2 -2
View File
@@ -1,6 +1,6 @@
Use this tool when you have completed the planning phase and are ready to exit plan agent.
Signal that planning is complete and the plan is ready for implementation.
This tool will ask the user if they want to switch to build agent to start implementing the plan.
Call this tool once you have finalized the plan file and are confident it is ready. This ends your planning turn and hands control back to the user.
Call this tool:
- After you have written a complete plan to the plan file
+5 -48
View File
@@ -17,64 +17,21 @@ async function getLastModel(sessionID: string) {
return Provider.defaultModel()
}
// kilocode_change start - simplified plan_exit: readiness signal only, no user prompt
export const PlanExitTool = Tool.define("plan_exit", {
description: EXIT_DESCRIPTION,
parameters: z.object({}),
async execute(_params, ctx) {
const session = await Session.get(ctx.sessionID)
const plan = path.relative(Instance.worktree, Session.plan(session))
const answers = await Question.ask({
sessionID: ctx.sessionID,
questions: [
{
// kilocode_change start
question: `Plan at ${plan} is complete. Would you like to switch to the code agent and start implementing?`,
header: "Code Agent",
custom: false,
options: [
{ label: "Yes", description: "Switch to code agent and start implementing the plan" },
{ label: "No", description: "Stay with plan agent to continue refining the plan" },
],
// kilocode_change end
},
],
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
})
const answer = answers[0]?.[0]
if (answer === "No") throw new Question.RejectedError()
const model = await getLastModel(ctx.sessionID)
const userMsg: MessageV2.User = {
id: Identifier.ascending("message"),
sessionID: ctx.sessionID,
role: "user",
time: {
created: Date.now(),
},
agent: "code", // kilocode_change - renamed from "build" to "code"
model,
}
await Session.updateMessage(userMsg)
await Session.updatePart({
id: Identifier.ascending("part"),
messageID: userMsg.id,
sessionID: ctx.sessionID,
type: "text",
text: `The plan at ${plan} has been approved, you can now edit files. Execute the plan`,
synthetic: true,
} satisfies MessageV2.TextPart)
// kilocode_change start
return {
title: "Switching to code agent",
output: "User approved switching to code agent. Wait for further instructions.",
metadata: {},
title: "Planning complete",
output: `Plan is ready at ${plan}. Ending planning turn.`,
metadata: { plan },
}
// kilocode_change end
},
})
// kilocode_change end
export const PlanEnterTool = Tool.define("plan_enter", {
description: ENTER_DESCRIPTION,
+2 -1
View File
@@ -117,7 +117,8 @@ export namespace ToolRegistry {
ApplyPatchTool,
...(Flag.KILO_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
...(config.experimental?.batch_tool === true ? [BatchTool] : []),
...(Flag.KILO_EXPERIMENTAL_PLAN_MODE && Flag.KILO_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []),
PlanExitTool, // kilocode_change - always registered; gated by agent permission instead
...(Flag.KILO_EXPERIMENTAL_PLAN_MODE && Flag.KILO_CLIENT === "cli" ? [PlanEnterTool] : []),
...custom,
]
}