diff --git a/.changeset/plan-followup-refine.md b/.changeset/plan-followup-refine.md new file mode 100644 index 00000000000..fab675a9906 --- /dev/null +++ b/.changeset/plan-followup-refine.md @@ -0,0 +1,6 @@ +--- +"@kilocode/cli": patch +"kilo-code": patch +--- + +Show an interactive Implement / Keep refining panel when Plan mode is ready instead of asking users to type a numbered choice. diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 4c267f26727..13170b89c6d 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -112,6 +112,8 @@ export const dict = { "plan.followup.answer.newSession.description": "نفّذ في جلسة جديدة بسياق نظيف", "plan.followup.answer.continue": "المتابعة هنا", "plan.followup.answer.continue.description": "نفّذ الخطة في هذه الجلسة", + "plan.followup.answer.keepRefining": "واصل التحسين", + "plan.followup.answer.keepRefining.description": "واصل التخطيط دون التنفيذ الآن", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "اللقطة بطيئة", diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index 1f816170e58..3e91b3d765e 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -113,6 +113,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implementar em uma nova sessão com contexto limpo", "plan.followup.answer.continue": "Continuar aqui", "plan.followup.answer.continue.description": "Implementar o plano nesta sessão", + "plan.followup.answer.keepRefining": "Continuar refinando", + "plan.followup.answer.keepRefining.description": "Continuar planejando sem implementar ainda", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Snapshot está lento", diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index 619faf6357f..d1aece561ee 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -118,6 +118,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implementiraj u novoj sesiji s čistim kontekstom", "plan.followup.answer.continue": "Nastavi ovdje", "plan.followup.answer.continue.description": "Implementiraj plan u ovoj sesiji", + "plan.followup.answer.keepRefining": "Nastavi dorađivati", + "plan.followup.answer.keepRefining.description": "Nastavi planirati bez implementacije za sada", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Snapshot je spor", diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index 457b3f97d23..3147ec1eee1 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -113,6 +113,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implementér i en ny session med ren kontekst", "plan.followup.answer.continue": "Fortsæt her", "plan.followup.answer.continue.description": "Implementér planen i denne session", + "plan.followup.answer.keepRefining": "Fortsæt med at finpudse", + "plan.followup.answer.keepRefining.description": "Fortsæt planlægningen uden at implementere endnu", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Snapshot er langsomt", diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index 08808907e25..8e4fe468a5c 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -115,6 +115,8 @@ export const dict = { "plan.followup.answer.newSession.description": "In einer neuen Sitzung mit leerem Kontext umsetzen", "plan.followup.answer.continue": "Hier fortfahren", "plan.followup.answer.continue.description": "Den Plan in dieser Sitzung umsetzen", + "plan.followup.answer.keepRefining": "Weiter verfeinern", + "plan.followup.answer.keepRefining.description": "Weiter planen, ohne jetzt zu implementieren", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Snapshot ist langsam", diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index 6c79b7dc8d4..9eb679e3a03 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -115,6 +115,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implement in a fresh session with a clean context", "plan.followup.answer.continue": "Continue here", "plan.followup.answer.continue.description": "Implement the plan in this session", + "plan.followup.answer.keepRefining": "Keep refining", + "plan.followup.answer.keepRefining.description": "Keep planning without implementing yet", // Slow-repo snapshot prompt. The English strings here are the canonical // labels sent by the backend and must stay in sync with diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index bcbf615f04a..a72ba48094c 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -114,6 +114,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implementar en una sesión nueva con contexto limpio", "plan.followup.answer.continue": "Continuar aquí", "plan.followup.answer.continue.description": "Implementar el plan en esta sesión", + "plan.followup.answer.keepRefining": "Seguir refinando", + "plan.followup.answer.keepRefining.description": "Seguir planificando sin implementar todavía", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "La instantánea es lenta", diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index 895b9cff15a..6f654770827 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -115,6 +115,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implémenter dans une nouvelle session avec un contexte vierge", "plan.followup.answer.continue": "Continuer ici", "plan.followup.answer.continue.description": "Implémenter le plan dans cette session", + "plan.followup.answer.keepRefining": "Continuer à affiner", + "plan.followup.answer.keepRefining.description": "Continuer à planifier sans implémenter pour l'instant", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Instantané lent", diff --git a/packages/kilo-i18n/src/it.ts b/packages/kilo-i18n/src/it.ts index 8224cf69102..229be83c14a 100644 --- a/packages/kilo-i18n/src/it.ts +++ b/packages/kilo-i18n/src/it.ts @@ -115,6 +115,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implementa in una nuova sessione con contesto vuoto", "plan.followup.answer.continue": "Continua qui", "plan.followup.answer.continue.description": "Implementa il piano in questa sessione", + "plan.followup.answer.keepRefining": "Continua a rifinire", + "plan.followup.answer.keepRefining.description": "Continua a pianificare senza implementare per ora", "snapshot.slowRepo.header": "Snapshot lento", "snapshot.slowRepo.question": diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 0092971f308..c1699834b6d 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -112,6 +112,8 @@ export const dict = { "plan.followup.answer.newSession.description": "クリーンなコンテキストの新しいセッションで実装する", "plan.followup.answer.continue": "ここで続行", "plan.followup.answer.continue.description": "このセッションで計画を実装する", + "plan.followup.answer.keepRefining": "さらに調整する", + "plan.followup.answer.keepRefining.description": "まだ実装せずに計画を続ける", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "スナップショットが遅い", diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 09fbfa7cf7f..a0e760dd541 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -112,6 +112,8 @@ export const dict = { "plan.followup.answer.newSession.description": "깨끗한 컨텍스트의 새 세션에서 구현", "plan.followup.answer.continue": "여기서 계속하기", "plan.followup.answer.continue.description": "이 세션에서 계획 구현", + "plan.followup.answer.keepRefining": "계속 다듬기", + "plan.followup.answer.keepRefining.description": "아직 구현하지 않고 계획을 계속 진행", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "스냅샷이 느립니다", diff --git a/packages/kilo-i18n/src/nl.ts b/packages/kilo-i18n/src/nl.ts index 9e5fc2210b1..bf4f116fd47 100644 --- a/packages/kilo-i18n/src/nl.ts +++ b/packages/kilo-i18n/src/nl.ts @@ -116,6 +116,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implementeren in een nieuwe sessie met een lege context", "plan.followup.answer.continue": "Hier doorgaan", "plan.followup.answer.continue.description": "Het plan in deze sessie implementeren", + "plan.followup.answer.keepRefining": "Blijven verfijnen", + "plan.followup.answer.keepRefining.description": "Blijven plannen zonder nu te implementeren", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Snapshot is traag", diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index 9600db9243d..c28bfb16cda 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -113,6 +113,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Implementer i en ny økt med ren kontekst", "plan.followup.answer.continue": "Fortsett her", "plan.followup.answer.continue.description": "Implementer planen i denne økten", + "plan.followup.answer.keepRefining": "Fortsett å finpusse", + "plan.followup.answer.keepRefining.description": "Fortsett planleggingen uten å implementere ennå", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Snapshot er tregt", diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index 07845f6f113..6764a61f265 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -114,6 +114,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Wdróż w nowej sesji z czystym kontekstem", "plan.followup.answer.continue": "Kontynuuj tutaj", "plan.followup.answer.continue.description": "Wdróż plan w tej sesji", + "plan.followup.answer.keepRefining": "Dalej dopracowuj", + "plan.followup.answer.keepRefining.description": "Kontynuuj planowanie bez wdrażania na razie", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Snapshot jest wolny", diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index 6a3e9c14d53..d426d1c0f1f 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -115,6 +115,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Реализовать в новой сессии с чистым контекстом", "plan.followup.answer.continue": "Продолжить здесь", "plan.followup.answer.continue.description": "Реализовать план в этой сессии", + "plan.followup.answer.keepRefining": "Продолжить уточнение", + "plan.followup.answer.keepRefining.description": "Продолжить планирование без реализации пока что", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Снимок выполняется медленно", diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index 13a1dbd0d7f..11a2f317c9a 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -113,6 +113,8 @@ export const dict = { "plan.followup.answer.newSession.description": "ดำเนินการในเซสชันใหม่ที่มีบริบทว่างเปล่า", "plan.followup.answer.continue": "ดำเนินการต่อที่นี่", "plan.followup.answer.continue.description": "ดำเนินการตามแผนในเซสชันนี้", + "plan.followup.answer.keepRefining": "ปรับแผนต่อ", + "plan.followup.answer.keepRefining.description": "วางแผนต่อโดยยังไม่ดำเนินการ", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "สแน็ปช็อตช้า", diff --git a/packages/kilo-i18n/src/tr.ts b/packages/kilo-i18n/src/tr.ts index 9b0724277a8..a787e69f62c 100644 --- a/packages/kilo-i18n/src/tr.ts +++ b/packages/kilo-i18n/src/tr.ts @@ -114,6 +114,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Temiz bir bağlamla yeni bir oturumda uygula", "plan.followup.answer.continue": "Burada devam et", "plan.followup.answer.continue.description": "Planı bu oturumda uygula", + "plan.followup.answer.keepRefining": "İyileştirmeye devam et", + "plan.followup.answer.keepRefining.description": "Henüz uygulamadan planlamaya devam et", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Anlık görüntü yavaş", diff --git a/packages/kilo-i18n/src/uk.ts b/packages/kilo-i18n/src/uk.ts index d3a885bf03d..b95a8240da7 100644 --- a/packages/kilo-i18n/src/uk.ts +++ b/packages/kilo-i18n/src/uk.ts @@ -115,6 +115,8 @@ export const dict = { "plan.followup.answer.newSession.description": "Реалізувати в новій сесії з чистим контекстом", "plan.followup.answer.continue": "Продовжити тут", "plan.followup.answer.continue.description": "Реалізувати план у цій сесії", + "plan.followup.answer.keepRefining": "Продовжити уточнення", + "plan.followup.answer.keepRefining.description": "Продовжити планування без реалізації наразі", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "Знімок виконується повільно", diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index 96587a53742..8baddc98f38 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -108,6 +108,8 @@ export const dict = { "plan.followup.answer.newSession.description": "在具有干净上下文的新会话中实现", "plan.followup.answer.continue": "在此继续", "plan.followup.answer.continue.description": "在本会话中实现计划", + "plan.followup.answer.keepRefining": "继续完善", + "plan.followup.answer.keepRefining.description": "继续规划,暂不实现", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "快照速度较慢", diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 547fb96436f..fdb58ffcaf7 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -108,6 +108,8 @@ export const dict = { "plan.followup.answer.newSession.description": "在具有乾淨上下文的新工作階段中實作", "plan.followup.answer.continue": "在此繼續", "plan.followup.answer.continue.description": "在本工作階段中實作計畫", + "plan.followup.answer.keepRefining": "繼續完善", + "plan.followup.answer.keepRefining.description": "繼續規劃,暫不實作", // Slow-repo snapshot prompt "snapshot.slowRepo.header": "快照速度較慢", diff --git a/packages/kilo-telemetry/src/telemetry.ts b/packages/kilo-telemetry/src/telemetry.ts index 21ea9043c81..009fc204a8d 100644 --- a/packages/kilo-telemetry/src/telemetry.ts +++ b/packages/kilo-telemetry/src/telemetry.ts @@ -186,7 +186,10 @@ export namespace Telemetry { track(TelemetryEvent.AGENT_USED, { agent, sessionId }) } - export function trackPlanFollowup(sessionId: string, choice: "new_session" | "continue" | "custom" | "dismissed") { + export function trackPlanFollowup( + sessionId: string, + choice: "new_session" | "continue" | "keep_refining" | "custom" | "dismissed", + ) { track(TelemetryEvent.PLAN_FOLLOWUP, { sessionId, choice }) } diff --git a/packages/kilo-vscode/tests/unit/plan-followup-locale-keys.test.ts b/packages/kilo-vscode/tests/unit/plan-followup-locale-keys.test.ts index afdaccef76d..86b4ef96dd4 100644 --- a/packages/kilo-vscode/tests/unit/plan-followup-locale-keys.test.ts +++ b/packages/kilo-vscode/tests/unit/plan-followup-locale-keys.test.ts @@ -50,6 +50,8 @@ const keys = [ "plan.followup.answer.newSession.description", "plan.followup.answer.continue", "plan.followup.answer.continue.description", + "plan.followup.answer.keepRefining", + "plan.followup.answer.keepRefining.description", ] describe("plan follow-up i18n keys", () => { diff --git a/packages/opencode/src/kilocode/plan-followup.ts b/packages/opencode/src/kilocode/plan-followup.ts index a32466ae199..2bc91c18ea6 100644 --- a/packages/opencode/src/kilocode/plan-followup.ts +++ b/packages/opencode/src/kilocode/plan-followup.ts @@ -160,6 +160,7 @@ export namespace PlanFollowup { export const PLAN_PREFIX = "Implement the following plan:" export const ANSWER_NEW_SESSION = "Start new session" export const ANSWER_CONTINUE = "Continue here" + export const ANSWER_KEEP_REFINING = "Keep refining" export function abort(sessionID: SessionID) { const ctl = pending.get(sessionID) @@ -320,6 +321,13 @@ export namespace PlanFollowup { descriptionKey: "plan.followup.answer.continue.description", mode: "code", }, + { + label: ANSWER_KEEP_REFINING, + labelKey: "plan.followup.answer.keepRefining", + description: "Keep planning without implementing yet", + descriptionKey: "plan.followup.answer.keepRefining.description", + mode: "plan", + }, ], }, ], @@ -540,6 +548,18 @@ export namespace PlanFollowup { return "continue" } + if (answer === ANSWER_KEEP_REFINING) { + Telemetry.trackPlanFollowup(input.sessionID, "keep_refining") + const msg = await inject({ + sessionID: input.sessionID, + agent: "plan", + model: user.model, + text: "Continue refining the plan. Do not implement yet.", + }) + KiloSessionPromptQueue.retarget(input.sessionID, msg.id) + return "continue" + } + Telemetry.trackPlanFollowup(input.sessionID, "custom") const msg = await inject({ sessionID: input.sessionID, diff --git a/packages/opencode/src/kilocode/session/native-plan-prompt.txt b/packages/opencode/src/kilocode/session/native-plan-prompt.txt index f6cfdf2ece3..6cfc492684c 100644 --- a/packages/opencode/src/kilocode/session/native-plan-prompt.txt +++ b/packages/opencode/src/kilocode/session/native-plan-prompt.txt @@ -24,8 +24,7 @@ Your job is to gather context, challenge assumptions, resolve design questions, - Follow the latest Plan File reminder for the target plan location. - If no exact plan file path is provided, follow the latest Plan File reminder for the directory and generated filename pattern. - Use repo-root `plans/`, `.plans/`, or `.opencode/plans/` only when requested or required by the repo/client and your permissions allow it. -- Do not write the final plan or call `plan_exit` until the user chooses "Finalize and save the plan". -- After final approval, write the final plan to the chosen plan file, then call `plan_exit` with the saved plan path. +- When the plan is implementation-ready, write the final plan to the chosen plan file, then call `plan_exit` with the saved plan path. - Do not edit source files or non-plan documentation files. - Do not run mutating commands. - If implementation requires source edits or mutating commands, tell the user to switch to an implementation-capable agent. @@ -34,13 +33,9 @@ Your job is to gather context, challenge assumptions, resolve design questions, - Keep planning until the important design decisions are resolved or explicitly marked out of scope. - If material uncertainty remains, keep the plan open: summarize the current state, identify the most important unresolved decision, and ask exactly one next question with your recommended answer. -- If the plan is implementation-ready but not saved, do not print the full plan in chat. Give a concise draft-ready summary, then ask exactly one question with these choices: - 1. Finalize and save the plan - 2. Continue refining -- Recommend "Finalize and save the plan" only when the goal, constraints, affected boundaries, data flow, failure modes, rollout or migration path, and validation plan are addressed or explicitly out of scope. -- If the user chooses "Finalize and save the plan", write the complete finalized Markdown plan to the chosen plan file, then call `plan_exit` as described above. -- If the user chooses "Continue refining", keep planning and do not write the final plan or call `plan_exit`. -- After `plan_exit`, rely on the client follow-up to ask whether the user wants to implement the saved plan in a new session. +- If the plan is implementation-ready, write the complete finalized Markdown plan to the chosen plan file, then call `plan_exit` as described above. +- Call `plan_exit` only when the goal, constraints, affected boundaries, data flow, failure modes, rollout or migration path, and validation plan are addressed or explicitly out of scope. +- Follow the latest Plan File reminder for whether to confirm with the user before finalizing, and for what happens after `plan_exit`. - Do not implement source or documentation changes as this agent. Saved plans should be concise and actionable. Prefer a clear ordered task list over a lengthy design document. Include only the context, decisions, risks, validation steps, and open questions another implementation-capable agent needs to execute safely. diff --git a/packages/opencode/src/kilocode/session/prompt.ts b/packages/opencode/src/kilocode/session/prompt.ts index 98c814c64f8..0f7946f0d33 100644 --- a/packages/opencode/src/kilocode/session/prompt.ts +++ b/packages/opencode/src/kilocode/session/prompt.ts @@ -40,6 +40,10 @@ export namespace KiloSessionPrompt { return id === "architect" || name === "plan" || name === "architect" } + function supportsPlanFollowup() { + return ["cli", "vscode", "jetbrains"].includes(Flag.KILO_CLIENT) + } + /** * Determines whether the plan follow-up prompt should be shown. * Checks if the plan_exit tool was called in the last assistant turn. @@ -47,7 +51,7 @@ export namespace KiloSessionPrompt { */ export function shouldAskPlanFollowup(input: { messages: MessageV2.WithParts[]; abort: AbortSignal }) { if (input.abort.aborted) return false - if (!["cli", "vscode", "jetbrains"].includes(Flag.KILO_CLIENT)) return false + if (!supportsPlanFollowup()) return false const idx = input.messages.findLastIndex((m) => m.info.role === "user") return input.messages .slice(idx + 1) @@ -299,7 +303,9 @@ export namespace KiloSessionPrompt { info, "Use the chosen plan path as the main plan file. Do not write or edit other files unless the user explicitly asks and your permissions allow it.", "Project/user instructions about plan location (for example plans/ or .plans/) are authorized when permissions allow them; they do not conflict with this reminder. When finalizing, call plan_exit with the path of the plan file you wrote.", - 'Before creating or updating the plan file, or calling plan_exit, ask the user to choose exactly one of: "Finalize and save the plan" or "Continue refining". If the user chooses to finalize, write the main plan file, then call plan_exit.', + supportsPlanFollowup() + ? "When the plan is implementation-ready, write the main plan file and call plan_exit. Do not ask the user to choose between finalizing and refining in chat; the client follow-up after plan_exit asks whether to implement the saved plan or keep refining." + : 'Before creating or updating the plan file, or calling plan_exit, ask the user to choose exactly one of: "Finalize and save the plan" or "Continue refining". If the user chooses to finalize, write the main plan file, then call plan_exit.', ].join("\n") add(`\n${body}\n`) } diff --git a/packages/opencode/test/kilocode/plan-exit-detection.test.ts b/packages/opencode/test/kilocode/plan-exit-detection.test.ts index 5b99cd19921..cb11daeb23e 100644 --- a/packages/opencode/test/kilocode/plan-exit-detection.test.ts +++ b/packages/opencode/test/kilocode/plan-exit-detection.test.ts @@ -149,6 +149,29 @@ async function waitQuestion(sessionID: string) { } } +function userMessage(input: { sessionID: SessionID; agent: string; text: string }) { + const id = MessageID.ascending() + return { + info: { + id, + role: "user", + sessionID: input.sessionID, + time: { created: Date.now() }, + agent: input.agent, + model, + }, + parts: [ + { + id: PartID.ascending(), + messageID: id, + sessionID: input.sessionID, + type: "text", + text: input.text, + }, + ], + } satisfies MessageV2.WithParts +} + function content(message: MessageV2.WithParts) { return message.parts .filter((part): part is MessageV2.TextPart => part.type === "text") @@ -338,10 +361,14 @@ describe("plan_exit detection", () => { expect(question.questions[0].options.map((item) => item.label)).toEqual([ PlanFollowup.ANSWER_NEW_SESSION, PlanFollowup.ANSWER_CONTINUE, + PlanFollowup.ANSWER_KEEP_REFINING, ]) expect(question.questions[0].options.find((item) => item.label === PlanFollowup.ANSWER_CONTINUE)?.mode).toBe( "code", ) + expect( + question.questions[0].options.find((item) => item.label === PlanFollowup.ANSWER_KEEP_REFINING)?.mode, + ).toBe("plan") await questions.reject(question.id) await expect(pending).resolves.toBe("break") } finally { @@ -685,6 +712,42 @@ describe("plan_exit detection", () => { await expect(fs.stat(dir).then((stat) => stat.isDirectory())).resolves.toBe(true) })) + test("native plan reminder keeps in-chat approval for clients without follow-up support", () => + withInstance(async () => { + const prev = process.env.KILO_CLIENT + try { + const session = await sessions.create({}) + + process.env.KILO_CLIENT = "vscode" + const supported = userMessage({ sessionID: session.id, agent: "plan", text: "Create a plan." }) + await KiloSessionPrompt.insertPlanReminders({ + agent: { name: "plan", options: {} }, + session, + userMessage: supported, + messages: [supported], + }) + const supportedText = content(supported) + expect(supportedText).toContain("client follow-up after plan_exit asks whether to implement") + expect(supportedText).not.toContain("Finalize and save the plan") + + process.env.KILO_CLIENT = "acp" + const acp = userMessage({ sessionID: session.id, agent: "plan", text: "Create a plan." }) + await KiloSessionPrompt.insertPlanReminders({ + agent: { name: "plan", options: {} }, + session, + userMessage: acp, + messages: [acp], + }) + const text = content(acp) + expect(text).toContain("Finalize and save the plan") + expect(text).toContain("Continue refining") + expect(text).not.toContain("client follow-up after plan_exit asks") + } finally { + if (prev === undefined) delete process.env.KILO_CLIENT + else process.env.KILO_CLIENT = prev + } + })) + test("native plan reminder prefers project plan path instructions over fallback", () => withInstance(async () => { const session = await sessions.create({}) diff --git a/packages/opencode/test/kilocode/plan-followup.test.ts b/packages/opencode/test/kilocode/plan-followup.test.ts index 06664e7c219..f0142d82570 100644 --- a/packages/opencode/test/kilocode/plan-followup.test.ts +++ b/packages/opencode/test/kilocode/plan-followup.test.ts @@ -1,5 +1,8 @@ import { describe, expect, spyOn, test } from "bun:test" import { Effect } from "effect" +import { Telemetry } from "@kilocode/kilo-telemetry" +import { Global } from "@opencode-ai/core/global" +import * as Log from "@opencode-ai/core/util/log" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { TuiEvent } from "../../src/cli/cmd/tui/event" @@ -8,7 +11,6 @@ import { SessionID, MessageID, PartID } from "../../src/session/schema" import { ModelID, ProviderID } from "../../src/provider/schema" import { formatTodos, generateHandover, PlanFollowup, PlanFollowupRuntime } from "../../src/kilocode/plan-followup" import { Instance } from "../../src/kilocode/instance" -import { provideTestInstance } from "../fixture/fixture" import { Provider } from "../../src/provider/provider" import { Question } from "../../src/question" import { Session } from "../../src/session/session" @@ -17,11 +19,9 @@ import { AppRuntime } from "../../src/effect/app-runtime" import { makeRuntime } from "../../src/effect/run-service" import { SessionStatus } from "../../src/session/status" import { Todo } from "../../src/session/todo" -import { Global } from "@opencode-ai/core/global" -import * as Log from "@opencode-ai/core/util/log" import path from "path" import fs from "fs/promises" -import { tmpdir } from "../fixture/fixture" +import { provideTestInstance, tmpdir } from "../fixture/fixture" Log.init({ print: false }) process.env.KILO_CLIENT = "cli" @@ -309,13 +309,14 @@ describe("plan follow-up", () => { expect(q.options.map((item) => item.label)).toEqual([ PlanFollowup.ANSWER_NEW_SESSION, PlanFollowup.ANSWER_CONTINUE, + PlanFollowup.ANSWER_KEEP_REFINING, ]) await question.reject(item.id) await expect(pending).resolves.toBe("break") })) - test("ask - Continue here option carries mode: code so VS Code picker updates immediately", () => + test("ask - follow-up options carry modes so the picker updates immediately", () => withInstance(async () => { const seeded = await seed({ text: "1. Build" }) const pending = PlanFollowup.ask({ @@ -335,6 +336,9 @@ describe("plan follow-up", () => { const continueOpt = q.options.find((o) => o.label === PlanFollowup.ANSWER_CONTINUE) expect(continueOpt?.mode).toBe("code") + const refineOpt = q.options.find((o) => o.label === PlanFollowup.ANSWER_KEEP_REFINING) + expect(refineOpt?.mode).toBe("plan") + // Start new session should not carry a mode (it opens a new session — the // current picker is irrelevant once the session switches). const newOpt = q.options.find((o) => o.label === PlanFollowup.ANSWER_NEW_SESSION) @@ -400,15 +404,21 @@ describe("plan follow-up", () => { expect(q.options.map((o) => o.labelKey)).toEqual([ "plan.followup.answer.newSession", "plan.followup.answer.continue", + "plan.followup.answer.keepRefining", ]) expect(q.options.map((o) => o.descriptionKey)).toEqual([ "plan.followup.answer.newSession.description", "plan.followup.answer.continue.description", + "plan.followup.answer.keepRefining.description", ]) // Canonical English labels stay on the wire — the server still matches on `label`, // so translating the UI must not change the reply format. - expect(q.options.map((o) => o.label)).toEqual([PlanFollowup.ANSWER_NEW_SESSION, PlanFollowup.ANSWER_CONTINUE]) + expect(q.options.map((o) => o.label)).toEqual([ + PlanFollowup.ANSWER_NEW_SESSION, + PlanFollowup.ANSWER_CONTINUE, + PlanFollowup.ANSWER_KEEP_REFINING, + ]) await question.reject(item.id) await expect(pending).resolves.toBe("break") @@ -467,6 +477,45 @@ describe("plan follow-up", () => { expect(part.synthetic).toBe(true) })) + test("ask - returns continue and creates plan message on Keep refining", () => + withInstance(async () => { + const track = spyOn(Telemetry, "trackPlanFollowup").mockImplementation(() => {}) + using _ = { + [Symbol.dispose]() { + track.mockRestore() + }, + } + const seeded = await seed({ text: "1. Build\n2. Test" }) + const pending = PlanFollowup.ask({ + question, + sessionID: seeded.sessionID, + messages: seeded.messages, + abort: AbortSignal.any([]), + }) + + const item = await waitQuestion(seeded.sessionID) + expect(item).toBeDefined() + if (!item) return + await question.reply({ + requestID: item.id, + answers: [[PlanFollowup.ANSWER_KEEP_REFINING]], + }) + + await expect(pending).resolves.toBe("continue") + expect(track).toHaveBeenCalledWith(seeded.sessionID, "keep_refining") + + const user = await latestUser(seeded.sessionID) + expect(user?.info.role).toBe("user") + if (!user || user.info.role !== "user") return + expect(user.info.agent).toBe("plan") + + const part = user.parts.find((item) => item.type === "text") + expect(part?.type).toBe("text") + if (!part || part.type !== "text") return + expect(part.text).toBe("Continue refining the plan. Do not implement yet.") + expect(part.synthetic).toBe(true) + })) + test("ask - returns continue and creates plan message for custom text", () => withInstance(async () => { const seeded = await seed({ text: "1. Build\n2. Test" })