Skip to content
Closed
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
12 changes: 12 additions & 0 deletions .changeset/fix-plan-exit-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@kilocode/cli": patch
---

fix(plan): prevent "Ready to implement?" popup from repeating after selecting "Continue here"

When a user selects "Continue here" on the plan follow-up prompt, a synthetic user message
is injected with `agent: "code"`. The `shouldAskPlanFollowup` check now skips triggering
the prompt when the last user message has already transitioned away from the plan agent,
preventing the popup from appearing repeatedly.

Fixes #9144
5 changes: 5 additions & 0 deletions packages/opencode/src/kilocode/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export namespace KiloSessionPrompt {
if (input.abort.aborted) return false
if (!["cli", "vscode"].includes(Flag.KILO_CLIENT)) return false
const idx = input.messages.findLastIndex((m) => m.info.role === "user")
if (idx === -1) return false
// Skip if the last user message already transitioned away from plan mode
// (e.g. "Continue here" injects a user message with agent="code")
const lastUser = input.messages[idx].info
if (lastUser.role === "user" && lastUser.agent !== "plan") return false
return input.messages
.slice(idx + 1)
.some((msg) =>
Expand Down
87 changes: 87 additions & 0 deletions packages/opencode/test/kilocode/plan-exit-detection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,4 +491,91 @@ describe("plan_exit detection", () => {
})
await expect(pending).resolves.toBe("continue")
}))

test("shouldAskPlanFollowup returns false after 'Continue here' (last user agent is code)", () =>
withInstance(async () => {
const session = await Session.create({})
const now = Date.now()

// Original user message in plan mode
const user1 = await Session.updateMessage({
id: MessageID.ascending(),
role: "user",
sessionID: session.id,
time: { created: now },
agent: "plan",
model,
})
await Session.updatePart({
id: PartID.ascending(),
messageID: user1.id,
sessionID: session.id,
type: "text",
text: "Create a plan",
})

// Assistant calls plan_exit
const assistant1: MessageV2.Assistant = {
id: MessageID.ascending(),
role: "assistant",
sessionID: session.id,
time: { created: now + 1 },
parentID: user1.id,
modelID: model.modelID,
providerID: model.providerID,
mode: "plan",
agent: "plan",
path: { cwd: Instance.directory, root: Instance.worktree },
cost: 0,
tokens: { total: 0, input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
finish: "end_turn",
}
await Session.updateMessage(assistant1)
await Session.updatePart({
id: PartID.ascending(),
messageID: assistant1.id,
sessionID: session.id,
type: "text",
text: "Here is the plan",
})
await Session.updatePart({
id: PartID.ascending(),
messageID: assistant1.id,
sessionID: session.id,
type: "tool",
callID: Identifier.ascending("tool"),
tool: "plan_exit",
state: {
status: "completed",
input: {},
output: "Plan is ready. Ending planning turn.",
title: "plan_exit",
metadata: {},
time: { start: now + 1, end: now + 1 },
},
} satisfies MessageV2.ToolPart)

// Simulate "Continue here" — injected user message with agent="code"
const user2 = await Session.updateMessage({
id: MessageID.ascending(),
role: "user",
sessionID: session.id,
time: { created: now + 2 },
agent: "code",
model,
})
await Session.updatePart({
id: PartID.ascending(),
messageID: user2.id,
sessionID: session.id,
type: "text",
text: "Implement the plan above.",
synthetic: true,
} satisfies MessageV2.TextPart)

const messages = await Session.messages({ sessionID: session.id })

// After "Continue here", the popup should NOT trigger again
expect(SessionPrompt.shouldAskPlanFollowup({ messages, abort: AbortSignal.any([]) })).toBe(false)
}))
})