diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 10e340d7f8..96f223b63b 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -195,6 +195,25 @@ export function Session() {
}
})
+ let lastSwitch: string | undefined = undefined
+ sdk.event.on("message.part.updated", (evt) => {
+ const part = evt.properties.part
+ if (part.type !== "tool") return
+ if (part.sessionID !== route.sessionID) return
+ if (part.state.status !== "completed") return
+
+ const metadata = part.state.metadata as { switched?: boolean }
+ if (!metadata?.switched) return
+
+ if (part.tool === "plan_exit") {
+ local.agent.set("build")
+ lastSwitch = part.id
+ } else if (part.tool === "plan_enter") {
+ local.agent.set("plan")
+ lastSwitch = part.id
+ }
+ })
+
let scroll: ScrollBoxRenderable
let prompt: PromptRef
const keybind = useKeybind()
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
index ccc0e9b125..5e8ce23807 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/question.tsx
@@ -32,7 +32,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
const question = createMemo(() => questions()[store.tab])
const confirm = createMemo(() => !single() && store.tab === questions().length)
const options = createMemo(() => question()?.options ?? [])
- const other = createMemo(() => store.selected === options().length)
+ const custom = createMemo(() => question()?.custom !== false)
+ const other = createMemo(() => custom() && store.selected === options().length)
const input = createMemo(() => store.custom[store.tab] ?? "")
const multi = createMemo(() => question()?.multiple === true)
const customPicked = createMemo(() => {
@@ -203,7 +204,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
}
} else {
const opts = options()
- const total = opts.length + 1 // options + "Other"
+ const total = opts.length + (custom() ? 1 : 0)
if (evt.name === "up" || evt.name === "k") {
evt.preventDefault()
@@ -298,35 +299,37 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
)
}}
- moveTo(options().length)} onMouseUp={() => selectOption()}>
-
-
-
- {options().length + 1}. Type your own answer
-
+
+ moveTo(options().length)} onMouseUp={() => selectOption()}>
+
+
+
+ {options().length + 1}. Type your own answer
+
+
+ {customPicked() ? "✓" : ""}
- {customPicked() ? "✓" : ""}
+
+
+
+
+
+
+ {input()}
+
+
-
-
-
-
-
-
- {input()}
-
-
-
+
diff --git a/packages/opencode/src/question/index.ts b/packages/opencode/src/question/index.ts
index 23656b53aa..d18098a9c4 100644
--- a/packages/opencode/src/question/index.ts
+++ b/packages/opencode/src/question/index.ts
@@ -24,6 +24,7 @@ export namespace Question {
header: z.string().max(12).describe("Very short label (max 12 chars)"),
options: z.array(Option).describe("Available choices"),
multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
+ custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"),
})
.meta({
ref: "QuestionInfo",
diff --git a/packages/opencode/src/tool/plan-enter.txt b/packages/opencode/src/tool/plan-enter.txt
index a52629d5d7..2e6a69f1f5 100644
--- a/packages/opencode/src/tool/plan-enter.txt
+++ b/packages/opencode/src/tool/plan-enter.txt
@@ -1,6 +1,8 @@
-Use this tool to suggest entering plan mode when the user's request would benefit from planning before implementation.
+Use this tool to suggest switching to plan agent when the user's request would benefit from planning before implementation.
-This tool will ask the user if they want to switch to plan mode.
+If they explicitly mention wanting to create a plan ALWAYS call this tool first.
+
+This tool will ask the user if they want to switch to plan agent.
Call this tool when:
- The user's request is complex and would benefit from planning first
@@ -10,4 +12,3 @@ Call this tool when:
Do NOT call this tool:
- For simple, straightforward tasks
- When the user explicitly wants immediate implementation
-- When already in plan mode
diff --git a/packages/opencode/src/tool/plan-exit.txt b/packages/opencode/src/tool/plan-exit.txt
index a7889579b8..988821de3b 100644
--- a/packages/opencode/src/tool/plan-exit.txt
+++ b/packages/opencode/src/tool/plan-exit.txt
@@ -1,6 +1,6 @@
-Use this tool when you have completed the planning phase and are ready to exit plan mode.
+Use this tool when you have completed the planning phase and are ready to exit plan agent.
-This tool will ask the user if they want to switch to build mode to start implementing the plan.
+This tool will ask the user if they want to switch to build agent to start implementing the plan.
Call this tool:
- After you have written a complete plan to the plan file
diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts
index cfa94e6a24..19ab527fd5 100644
--- a/packages/opencode/src/tool/plan.ts
+++ b/packages/opencode/src/tool/plan.ts
@@ -1,10 +1,12 @@
import z from "zod"
+import path from "path"
import { Tool } from "./tool"
import { Question } from "../question"
import { Session } from "../session"
import { MessageV2 } from "../session/message-v2"
import { Identifier } from "../id/id"
import { Provider } from "../provider/provider"
+import { Instance } from "../project/instance"
import EXIT_DESCRIPTION from "./plan-exit.txt"
import ENTER_DESCRIPTION from "./plan-enter.txt"
@@ -19,15 +21,18 @@ 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: [
{
- question: "Planning is complete. Would you like to switch to build mode and start implementing?",
- header: "Build Mode",
+ question: `Plan at ${plan} is complete. Would you like to switch to the build agent and start implementing?`,
+ header: "Build Agent",
+ custom: false,
options: [
- { label: "Yes", description: "Switch to build mode and start implementing the plan" },
- { label: "No", description: "Stay in plan mode to continue refining the plan" },
+ { label: "Yes", description: "Switch to build agent and start implementing the plan" },
+ { label: "No", description: "Stay with plan agent to continue refining the plan" },
],
},
],
@@ -35,41 +40,34 @@ export const PlanExitTool = Tool.define("plan_exit", {
})
const answer = answers[0]?.[0]
- const shouldSwitch = answer === "Yes"
+ if (answer === "No") throw new Question.RejectedError()
- if (shouldSwitch) {
- const model = await getLastModel(ctx.sessionID)
+ const model = await getLastModel(ctx.sessionID)
- const userMsg: MessageV2.User = {
- id: Identifier.ascending("message"),
- sessionID: ctx.sessionID,
- role: "user",
- time: {
- created: Date.now(),
- },
- agent: "build",
- model,
- }
- await Session.updateMessage(userMsg)
- await Session.updatePart({
- id: Identifier.ascending("part"),
- messageID: userMsg.id,
- sessionID: ctx.sessionID,
- type: "text",
- text: "User has approved the plan. Switch to build mode and begin implementing the plan.",
- synthetic: true,
- } satisfies MessageV2.TextPart)
+ const userMsg: MessageV2.User = {
+ id: Identifier.ascending("message"),
+ sessionID: ctx.sessionID,
+ role: "user",
+ time: {
+ created: Date.now(),
+ },
+ agent: "build",
+ 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)
return {
- title: shouldSwitch ? "Switching to build mode" : "Staying in plan mode",
- output: shouldSwitch
- ? "User confirmed to switch to build mode. A new message has been created to switch you to build mode. Begin implementing the plan."
- : "User chose to stay in plan mode. Continue refining the plan or address any concerns.",
- metadata: {
- switchToBuild: shouldSwitch,
- answer,
- },
+ title: "Switching to build agent",
+ output: "User chose to continue planning. Wait for further instructions.",
+ metadata: {},
}
},
})
@@ -78,16 +76,19 @@ export const PlanEnterTool = Tool.define("plan_enter", {
description: ENTER_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: [
{
- question:
- "Would you like to switch to plan mode? In plan mode, the AI will only research and create a plan without making changes.",
+ question: `Would you like to switch to the plan agent and create a plan saved to ${plan}?`,
header: "Plan Mode",
+ custom: false,
options: [
- { label: "Yes", description: "Switch to plan mode for research and planning" },
- { label: "No", description: "Stay in build mode to continue making changes" },
+ { label: "Yes", description: "Switch to plan agent for research and planning" },
+ { label: "No", description: "Stay with build agent to continue making changes" },
],
},
],
@@ -95,41 +96,35 @@ export const PlanEnterTool = Tool.define("plan_enter", {
})
const answer = answers[0]?.[0]
- const shouldSwitch = answer === "Yes"
- if (shouldSwitch) {
- const model = await getLastModel(ctx.sessionID)
+ if (answer === "No") throw new Question.RejectedError()
- const userMsg: MessageV2.User = {
- id: Identifier.ascending("message"),
- sessionID: ctx.sessionID,
- role: "user",
- time: {
- created: Date.now(),
- },
- agent: "plan",
- model,
- }
- await Session.updateMessage(userMsg)
- await Session.updatePart({
- id: Identifier.ascending("part"),
- messageID: userMsg.id,
- sessionID: ctx.sessionID,
- type: "text",
- text: "User has requested to enter plan mode. Switch to plan mode and begin planning.",
- synthetic: true,
- } satisfies MessageV2.TextPart)
+ const model = await getLastModel(ctx.sessionID)
+
+ const userMsg: MessageV2.User = {
+ id: Identifier.ascending("message"),
+ sessionID: ctx.sessionID,
+ role: "user",
+ time: {
+ created: Date.now(),
+ },
+ agent: "plan",
+ model,
}
+ await Session.updateMessage(userMsg)
+ await Session.updatePart({
+ id: Identifier.ascending("part"),
+ messageID: userMsg.id,
+ sessionID: ctx.sessionID,
+ type: "text",
+ text: "User has requested to enter plan mode. Switch to plan mode and begin planning.",
+ synthetic: true,
+ } satisfies MessageV2.TextPart)
return {
- title: shouldSwitch ? "Switching to plan mode" : "Staying in build mode",
- output: shouldSwitch
- ? "User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. Begin planning."
- : "User chose to stay in build mode. Continue with the current task.",
- metadata: {
- switchToPlan: shouldSwitch,
- answer,
- },
+ title: "Switching to plan agent",
+ output: `User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. The plan file will be at ${plan}. Begin planning.`,
+ metadata: {},
}
},
})
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index e423fecea4..acc29d9b43 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -545,6 +545,10 @@ export type QuestionInfo = {
* Allow selecting multiple choices
*/
multiple?: boolean
+ /**
+ * Allow typing a custom answer (default: true)
+ */
+ custom?: boolean
}
export type QuestionRequest = {
@@ -706,6 +710,7 @@ export type PermissionRuleset = Array
export type Session = {
id: string
+ slug: string
projectID: string
directory: string
parentID?: string