fix(vscode,cli): make plan_exit "Continue here" work again

Restore custom: false on the plan follow-up question — the "Type your own answer" row was redundant because the main prompt already routes typed text as a question reply. Auto-submit single-question single-select option picks in the VS Code QuestionDock so the button behaves like the TUI instead of silently waiting for a second Submit click.
This commit is contained in:
Alex Alecu
2026-04-20 14:56:40 +03:00
parent 18442f695d
commit ec5ac2e19f
6 changed files with 93 additions and 3 deletions
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Fix the "Continue here" button not submitting after a plan is finished. Picking an option on a single-question prompt now sends the reply immediately — matching the CLI behaviour — and the redundant "Type your own answer" row no longer appears on the plan follow-up question.
@@ -1,5 +1,6 @@
import { describe, it, expect } from "bun:test"
import {
pickOutcome,
resolveOptimisticQuestionAgent,
resolveQuestionMode,
resolveSelectedQuestionMode,
@@ -128,3 +129,25 @@ describe("resolveOptimisticQuestionAgent", () => {
expect(result).toEqual({ base: "ask", agent: "architect" })
})
})
describe("pickOutcome", () => {
it("submits immediately on a single-question single-select option pick", () => {
expect(pickOutcome({ single: true, multi: false, custom: false })).toEqual({ kind: "submit" })
})
it("advances to the next tab on a multi-question single-select option pick", () => {
expect(pickOutcome({ single: false, multi: false, custom: false })).toEqual({ kind: "advance" })
})
it("stays on the current tab for a multi-select pick", () => {
expect(pickOutcome({ single: true, multi: true, custom: false })).toEqual({ kind: "stay" })
})
it("defers submission for a single-select custom-input pick (handleCustomSubmit owns the submit)", () => {
expect(pickOutcome({ single: true, multi: false, custom: true })).toEqual({ kind: "advance" })
})
it("stays on the current tab for a multi-select custom-input pick", () => {
expect(pickOutcome({ single: false, multi: true, custom: true })).toEqual({ kind: "stay" })
})
})
@@ -12,7 +12,12 @@ import { Icon } from "@kilocode/kilo-ui/icon"
import { useSession } from "../../context/session"
import { useLanguage } from "../../context/language"
import type { QuestionRequest } from "../../types/messages"
import { resolveOptimisticQuestionAgent, resolveSelectedQuestionMode, toggleAnswer } from "./question-dock-utils"
import {
pickOutcome,
resolveOptimisticQuestionAgent,
resolveSelectedQuestionMode,
toggleAnswer,
} from "./question-dock-utils"
export const QuestionDock: Component<{ request: QuestionRequest }> = (props) => {
const session = useSession()
@@ -119,7 +124,14 @@ export const QuestionDock: Component<{ request: QuestionRequest }> = (props) =>
syncAgent(answers, kinds)
if (!single() && !multi()) {
const outcome = pickOutcome({ single: single(), multi: multi(), custom })
if (outcome.kind === "submit") {
// Mirror TUI behaviour: a single-question single-select option pick submits immediately.
// handleCustomSubmit covers the custom-input path via its own submit() call.
reply([[answer]])
return
}
if (outcome.kind === "advance") {
setStore("tab", store.tab + 1)
}
}
@@ -1,5 +1,21 @@
import type { QuestionOption } from "../../types/messages"
export type PickOutcome = { kind: "submit" } | { kind: "advance" } | { kind: "stay" }
/**
* Decide what should happen after a user picks an option in the question dock.
*
* - Multi-select prompts: the pick only toggles local state; no tab change, no submit.
* - Single-question single-select, option pick: submit immediately (matches the TUI).
* - Multi-question single-select, option pick: advance to the next tab.
* - Custom-input path for a single-select is handled separately in handleCustomSubmit.
*/
export function pickOutcome(input: { single: boolean; multi: boolean; custom: boolean }): PickOutcome {
if (input.multi) return { kind: "stay" }
if (input.single && !input.custom) return { kind: "submit" }
return { kind: "advance" }
}
export function toggleAnswer(existing: string[], answer: string): string[] {
const next = [...existing]
const index = next.indexOf(answer)
@@ -264,7 +264,11 @@ export namespace PlanFollowup {
{
question: "Ready to implement?",
header: "Implement",
custom: true,
// Keep false: the main prompt input already routes typed text as a question reply,
// so "Type your own answer" would be redundant. This was set to false intentionally
// in 65566af7f8 and got flipped back to true during the v1.4.4 upstream merge —
// do not change without updating that history.
custom: false,
options: [
{
label: ANSWER_NEW_SESSION,
@@ -266,6 +266,36 @@ describe("plan follow-up", () => {
await expect(pending).resolves.toBe("break")
}))
test("ask - emits a non-custom single-select question with the canonical answers", () =>
withInstance(async () => {
const seeded = await seed({ text: "1. Build" })
const pending = PlanFollowup.ask({
sessionID: seeded.sessionID,
messages: seeded.messages,
abort: AbortSignal.any([]),
})
const item = await waitQuestion(seeded.sessionID)
expect(item).toBeDefined()
if (!item) return
const q = item.questions[0]
expect(q).toBeDefined()
if (!q) return
// custom must stay false — "Type your own answer" is redundant because the main prompt
// input already routes typed text as a question reply. Regressed once during the v1.4.4
// upstream merge, so pin it here.
expect(q.custom).toBe(false)
expect(q.multiple).not.toBe(true)
expect(q.options.map((item) => item.label)).toEqual([
PlanFollowup.ANSWER_NEW_SESSION,
PlanFollowup.ANSWER_CONTINUE,
])
await question.reject(item.id)
await expect(pending).resolves.toBe("break")
}))
test("ask - returns continue and creates code message on Continue here", () =>
withInstance(async () => {
const get = spyOn(PlanFollowupRuntime, "agent").mockImplementation(async (name: string) => {