mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
tui: improve plan mode switching and question handling
- Add automatic agent switching when plan tools complete - Add custom option control to question interface - Update plan tool descriptions to clarify agent switching - Show plan file path in exit/exit prompts - Remove custom answer option when not needed
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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 }) {
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
<box onMouseOver={() => moveTo(options().length)} onMouseUp={() => selectOption()}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
||||
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
||||
{options().length + 1}. Type your own answer
|
||||
</text>
|
||||
<Show when={custom()}>
|
||||
<box onMouseOver={() => moveTo(options().length)} onMouseUp={() => selectOption()}>
|
||||
<box flexDirection="row" gap={1}>
|
||||
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
||||
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
||||
{options().length + 1}. Type your own answer
|
||||
</text>
|
||||
</box>
|
||||
<text fg={theme.success}>{customPicked() ? "✓" : ""}</text>
|
||||
</box>
|
||||
<text fg={theme.success}>{customPicked() ? "✓" : ""}</text>
|
||||
<Show when={store.editing}>
|
||||
<box paddingLeft={3}>
|
||||
<textarea
|
||||
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||
focused
|
||||
initialValue={input()}
|
||||
placeholder="Type your own answer"
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.primary}
|
||||
keyBindings={bindings()}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={!store.editing && input()}>
|
||||
<box paddingLeft={3}>
|
||||
<text fg={theme.textMuted}>{input()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
<Show when={store.editing}>
|
||||
<box paddingLeft={3}>
|
||||
<textarea
|
||||
ref={(val: TextareaRenderable) => (textarea = val)}
|
||||
focused
|
||||
initialValue={input()}
|
||||
placeholder="Type your own answer"
|
||||
textColor={theme.text}
|
||||
focusedTextColor={theme.text}
|
||||
cursorColor={theme.primary}
|
||||
keyBindings={bindings()}
|
||||
/>
|
||||
</box>
|
||||
</Show>
|
||||
<Show when={!store.editing && input()}>
|
||||
<box paddingLeft={3}>
|
||||
<text fg={theme.textMuted}>{input()}</text>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</Show>
|
||||
</box>
|
||||
</box>
|
||||
</Show>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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<PermissionRule>
|
||||
|
||||
export type Session = {
|
||||
id: string
|
||||
slug: string
|
||||
projectID: string
|
||||
directory: string
|
||||
parentID?: string
|
||||
|
||||
Reference in New Issue
Block a user