Pārlūkot izejas kodu

tui: convert handoff from text command to dedicated mode with slash command

Dax Raad 2 mēneši atpakaļ
vecāks
revīzija
f689fc7f75

+ 65 - 30
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -119,7 +119,7 @@ export function Prompt(props: PromptProps) {
 
   const [store, setStore] = createStore<{
     prompt: PromptInfo
-    mode: "normal" | "shell"
+    mode: "normal" | "shell" | "handoff"
     extmarkToPartIndex: Map<number, number>
     interrupt: number
     placeholder: number
@@ -338,6 +338,20 @@ export function Prompt(props: PromptProps) {
           ))
         },
       },
+      {
+        title: "Handoff",
+        value: "prompt.handoff",
+        disabled: props.sessionID === undefined,
+        category: "Prompt",
+        slash: {
+          name: "handoff",
+        },
+        onSelect: () => {
+          input.clear()
+          setStore("mode", "handoff")
+          setStore("prompt", { input: "", parts: [] })
+        },
+      },
     ]
   })
 
@@ -515,17 +529,45 @@ export function Prompt(props: PromptProps) {
   async function submit() {
     if (props.disabled) return
     if (autocomplete?.visible) return
+    const selectedModel = local.model.current()
+    if (!selectedModel) {
+      promptModelWarning()
+      return
+    }
+
+    if (store.mode === "handoff") {
+      const result = await sdk.client.session.handoff({
+        sessionID: props.sessionID!,
+        goal: store.prompt.input,
+        model: {
+          providerID: selectedModel.providerID,
+          modelID: selectedModel.modelID,
+        },
+      })
+      if (result.data) {
+        route.navigate({
+          type: "home",
+          initialPrompt: {
+            input: result.data.text,
+            parts:
+              result.data.files.map((file) => ({
+                type: "file",
+                url: file,
+                filename: file,
+                mime: "text/plain",
+              })) ?? [],
+          },
+        })
+      }
+      return
+    }
+
     if (!store.prompt.input) return
     const trimmed = store.prompt.input.trim()
     if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
       exit()
       return
     }
-    const selectedModel = local.model.current()
-    if (!selectedModel) {
-      promptModelWarning()
-      return
-    }
     const sessionID = props.sessionID
       ? props.sessionID
       : await (async () => {
@@ -569,27 +611,6 @@ export function Prompt(props: PromptProps) {
         command: inputText,
       })
       setStore("mode", "normal")
-    } else if (inputText.startsWith("/handoff ")) {
-      // Handle handoff command specially - call endpoint and replace prompt
-      const goal = inputText.slice(9).trim() // Remove "/handoff " prefix
-      if (goal) {
-        const result = await sdk.client.session.handoff({
-          sessionID,
-          goal,
-          model: {
-            providerID: selectedModel.providerID,
-            modelID: selectedModel.modelID,
-          },
-        })
-        if (result.data) {
-          // Replace prompt with the handoff text
-          const handoffText = result.data.text
-          input.setText(handoffText)
-          setStore("prompt", { input: handoffText, parts: [] })
-          // Don't submit yet - let user review and submit manually
-          return
-        }
-      }
     } else if (
       inputText.startsWith("/") &&
       iife(() => {
@@ -747,6 +768,7 @@ export function Prompt(props: PromptProps) {
   const highlight = createMemo(() => {
     if (keybind.leader) return theme.border
     if (store.mode === "shell") return theme.primary
+    if (store.mode === "handoff") return theme.warning
     return local.agent.color(local.agent.current().name)
   })
 
@@ -818,7 +840,11 @@ export function Prompt(props: PromptProps) {
             flexGrow={1}
           >
             <textarea
-              placeholder={props.sessionID ? undefined : `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`}
+              placeholder={iife(() => {
+                if (store.mode === "handoff") return "Goal for the new session"
+                if (props.sessionID) return undefined
+                return `Ask anything... "${PLACEHOLDERS[store.placeholder]}"`
+              })}
               textColor={keybind.leader ? theme.textMuted : theme.text}
               focusedTextColor={keybind.leader ? theme.textMuted : theme.text}
               minHeight={1}
@@ -875,7 +901,7 @@ export function Prompt(props: PromptProps) {
                   e.preventDefault()
                   return
                 }
-                if (store.mode === "shell") {
+                if (store.mode === "shell" || store.mode === "handoff") {
                   if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
                     setStore("mode", "normal")
                     e.preventDefault()
@@ -996,7 +1022,11 @@ export function Prompt(props: PromptProps) {
             />
             <box flexDirection="row" flexShrink={0} paddingTop={1} gap={1}>
               <text fg={highlight()}>
-                {store.mode === "shell" ? "Shell" : Locale.titlecase(local.agent.current().name)}{" "}
+                <Switch>
+                  <Match when={store.mode === "normal"}>{Locale.titlecase(local.agent.current().name)}</Match>
+                  <Match when={store.mode === "shell"}>Shell</Match>
+                  <Match when={store.mode === "handoff"}>Handoff</Match>
+                </Switch>
               </text>
               <Show when={store.mode === "normal"}>
                 <box flexDirection="row" gap={1}>
@@ -1143,6 +1173,11 @@ export function Prompt(props: PromptProps) {
                     esc <span style={{ fg: theme.textMuted }}>exit shell mode</span>
                   </text>
                 </Match>
+                <Match when={store.mode === "handoff"}>
+                  <text fg={theme.text}>
+                    esc <span style={{ fg: theme.textMuted }}>exit handoff mode</span>
+                  </text>
+                </Match>
               </Switch>
             </box>
           </Show>