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:
parent
a5da5aabf5
commit
69795efdcd
7 changed files with 126 additions and 102 deletions
|
|
@ -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 scroll: ScrollBoxRenderable
|
||||||
let prompt: PromptRef
|
let prompt: PromptRef
|
||||||
const keybind = useKeybind()
|
const keybind = useKeybind()
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||||
const question = createMemo(() => questions()[store.tab])
|
const question = createMemo(() => questions()[store.tab])
|
||||||
const confirm = createMemo(() => !single() && store.tab === questions().length)
|
const confirm = createMemo(() => !single() && store.tab === questions().length)
|
||||||
const options = createMemo(() => question()?.options ?? [])
|
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 input = createMemo(() => store.custom[store.tab] ?? "")
|
||||||
const multi = createMemo(() => question()?.multiple === true)
|
const multi = createMemo(() => question()?.multiple === true)
|
||||||
const customPicked = createMemo(() => {
|
const customPicked = createMemo(() => {
|
||||||
|
|
@ -203,7 +204,7 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const opts = options()
|
const opts = options()
|
||||||
const total = opts.length + 1 // options + "Other"
|
const total = opts.length + (custom() ? 1 : 0)
|
||||||
|
|
||||||
if (evt.name === "up" || evt.name === "k") {
|
if (evt.name === "up" || evt.name === "k") {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
|
|
@ -298,35 +299,37 @@ export function QuestionPrompt(props: { request: QuestionRequest }) {
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
<box onMouseOver={() => moveTo(options().length)} onMouseUp={() => selectOption()}>
|
<Show when={custom()}>
|
||||||
<box flexDirection="row" gap={1}>
|
<box onMouseOver={() => moveTo(options().length)} onMouseUp={() => selectOption()}>
|
||||||
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
<box flexDirection="row" gap={1}>
|
||||||
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
<box backgroundColor={other() ? theme.backgroundElement : undefined}>
|
||||||
{options().length + 1}. Type your own answer
|
<text fg={other() ? theme.secondary : customPicked() ? theme.success : theme.text}>
|
||||||
</text>
|
{options().length + 1}. Type your own answer
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<text fg={theme.success}>{customPicked() ? "✓" : ""}</text>
|
||||||
</box>
|
</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>
|
</box>
|
||||||
<Show when={store.editing}>
|
</Show>
|
||||||
<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>
|
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ export namespace Question {
|
||||||
header: z.string().max(12).describe("Very short label (max 12 chars)"),
|
header: z.string().max(12).describe("Very short label (max 12 chars)"),
|
||||||
options: z.array(Option).describe("Available choices"),
|
options: z.array(Option).describe("Available choices"),
|
||||||
multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
|
multiple: z.boolean().optional().describe("Allow selecting multiple choices"),
|
||||||
|
custom: z.boolean().optional().describe("Allow typing a custom answer (default: true)"),
|
||||||
})
|
})
|
||||||
.meta({
|
.meta({
|
||||||
ref: "QuestionInfo",
|
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:
|
Call this tool when:
|
||||||
- The user's request is complex and would benefit from planning first
|
- 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:
|
Do NOT call this tool:
|
||||||
- For simple, straightforward tasks
|
- For simple, straightforward tasks
|
||||||
- When the user explicitly wants immediate implementation
|
- 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:
|
Call this tool:
|
||||||
- After you have written a complete plan to the plan file
|
- After you have written a complete plan to the plan file
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
|
import path from "path"
|
||||||
import { Tool } from "./tool"
|
import { Tool } from "./tool"
|
||||||
import { Question } from "../question"
|
import { Question } from "../question"
|
||||||
import { Session } from "../session"
|
import { Session } from "../session"
|
||||||
import { MessageV2 } from "../session/message-v2"
|
import { MessageV2 } from "../session/message-v2"
|
||||||
import { Identifier } from "../id/id"
|
import { Identifier } from "../id/id"
|
||||||
import { Provider } from "../provider/provider"
|
import { Provider } from "../provider/provider"
|
||||||
|
import { Instance } from "../project/instance"
|
||||||
import EXIT_DESCRIPTION from "./plan-exit.txt"
|
import EXIT_DESCRIPTION from "./plan-exit.txt"
|
||||||
import ENTER_DESCRIPTION from "./plan-enter.txt"
|
import ENTER_DESCRIPTION from "./plan-enter.txt"
|
||||||
|
|
||||||
|
|
@ -19,15 +21,18 @@ export const PlanExitTool = Tool.define("plan_exit", {
|
||||||
description: EXIT_DESCRIPTION,
|
description: EXIT_DESCRIPTION,
|
||||||
parameters: z.object({}),
|
parameters: z.object({}),
|
||||||
async execute(_params, ctx) {
|
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({
|
const answers = await Question.ask({
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
questions: [
|
questions: [
|
||||||
{
|
{
|
||||||
question: "Planning is complete. Would you like to switch to build mode and start implementing?",
|
question: `Plan at ${plan} is complete. Would you like to switch to the build agent and start implementing?`,
|
||||||
header: "Build Mode",
|
header: "Build Agent",
|
||||||
|
custom: false,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Yes", description: "Switch to build mode and start implementing the plan" },
|
{ label: "Yes", description: "Switch to build agent and start implementing the plan" },
|
||||||
{ label: "No", description: "Stay in plan mode to continue refining 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 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 = {
|
const userMsg: MessageV2.User = {
|
||||||
id: Identifier.ascending("message"),
|
id: Identifier.ascending("message"),
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
role: "user",
|
role: "user",
|
||||||
time: {
|
time: {
|
||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
},
|
},
|
||||||
agent: "build",
|
agent: "build",
|
||||||
model,
|
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)
|
|
||||||
}
|
}
|
||||||
|
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 {
|
return {
|
||||||
title: shouldSwitch ? "Switching to build mode" : "Staying in plan mode",
|
title: "Switching to build agent",
|
||||||
output: shouldSwitch
|
output: "User chose to continue planning. Wait for further instructions.",
|
||||||
? "User confirmed to switch to build mode. A new message has been created to switch you to build mode. Begin implementing the plan."
|
metadata: {},
|
||||||
: "User chose to stay in plan mode. Continue refining the plan or address any concerns.",
|
|
||||||
metadata: {
|
|
||||||
switchToBuild: shouldSwitch,
|
|
||||||
answer,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -78,16 +76,19 @@ export const PlanEnterTool = Tool.define("plan_enter", {
|
||||||
description: ENTER_DESCRIPTION,
|
description: ENTER_DESCRIPTION,
|
||||||
parameters: z.object({}),
|
parameters: z.object({}),
|
||||||
async execute(_params, ctx) {
|
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({
|
const answers = await Question.ask({
|
||||||
sessionID: ctx.sessionID,
|
sessionID: ctx.sessionID,
|
||||||
questions: [
|
questions: [
|
||||||
{
|
{
|
||||||
question:
|
question: `Would you like to switch to the plan agent and create a plan saved to ${plan}?`,
|
||||||
"Would you like to switch to plan mode? In plan mode, the AI will only research and create a plan without making changes.",
|
|
||||||
header: "Plan Mode",
|
header: "Plan Mode",
|
||||||
|
custom: false,
|
||||||
options: [
|
options: [
|
||||||
{ label: "Yes", description: "Switch to plan mode for research and planning" },
|
{ label: "Yes", description: "Switch to plan agent for research and planning" },
|
||||||
{ label: "No", description: "Stay in build mode to continue making changes" },
|
{ 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 answer = answers[0]?.[0]
|
||||||
const shouldSwitch = answer === "Yes"
|
|
||||||
|
|
||||||
if (shouldSwitch) {
|
if (answer === "No") throw new Question.RejectedError()
|
||||||
const model = await getLastModel(ctx.sessionID)
|
|
||||||
|
|
||||||
const userMsg: MessageV2.User = {
|
const model = await getLastModel(ctx.sessionID)
|
||||||
id: Identifier.ascending("message"),
|
|
||||||
sessionID: ctx.sessionID,
|
const userMsg: MessageV2.User = {
|
||||||
role: "user",
|
id: Identifier.ascending("message"),
|
||||||
time: {
|
sessionID: ctx.sessionID,
|
||||||
created: Date.now(),
|
role: "user",
|
||||||
},
|
time: {
|
||||||
agent: "plan",
|
created: Date.now(),
|
||||||
model,
|
},
|
||||||
}
|
agent: "plan",
|
||||||
await Session.updateMessage(userMsg)
|
model,
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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 {
|
return {
|
||||||
title: shouldSwitch ? "Switching to plan mode" : "Staying in build mode",
|
title: "Switching to plan agent",
|
||||||
output: shouldSwitch
|
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.`,
|
||||||
? "User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. Begin planning."
|
metadata: {},
|
||||||
: "User chose to stay in build mode. Continue with the current task.",
|
|
||||||
metadata: {
|
|
||||||
switchToPlan: shouldSwitch,
|
|
||||||
answer,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -545,6 +545,10 @@ export type QuestionInfo = {
|
||||||
* Allow selecting multiple choices
|
* Allow selecting multiple choices
|
||||||
*/
|
*/
|
||||||
multiple?: boolean
|
multiple?: boolean
|
||||||
|
/**
|
||||||
|
* Allow typing a custom answer (default: true)
|
||||||
|
*/
|
||||||
|
custom?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuestionRequest = {
|
export type QuestionRequest = {
|
||||||
|
|
@ -706,6 +710,7 @@ export type PermissionRuleset = Array<PermissionRule>
|
||||||
|
|
||||||
export type Session = {
|
export type Session = {
|
||||||
id: string
|
id: string
|
||||||
|
slug: string
|
||||||
projectID: string
|
projectID: string
|
||||||
directory: string
|
directory: string
|
||||||
parentID?: string
|
parentID?: string
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue