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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user