Просмотр исходного кода

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
Dax Raad 7 месяцев назад
Родитель
Сommit
69795efdcd

+ 19 - 0
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()

+ 32 - 29
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 }) {
                   )
                 }}
               </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>

+ 1 - 0
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",

+ 4 - 3
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

+ 2 - 2
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

+ 63 - 68
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: {},
     }
   },
 })

+ 5 - 0
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<PermissionRule>
 
 export type Session = {
   id: string
+  slug: string
   projectID: string
   directory: string
   parentID?: string