浏览代码

fix: preserve prompt input across unmount/remount cycles (#22508)

Dax 22 小时之前
父节点
当前提交
4bd5a158a5

+ 0 - 4
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -420,12 +420,8 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
         aliases: ["clear"],
       },
       onSelect: () => {
-        const current = promptRef.current
-        // Don't require focus - if there's any text, preserve it
-        const currentPrompt = current?.current?.input ? current.current : undefined
         route.navigate({
           type: "home",
-          initialPrompt: currentPrompt,
         })
         dialog.clear()
       },

+ 18 - 1
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

@@ -12,7 +12,7 @@ import { useRoute } from "@tui/context/route"
 import { useSync } from "@tui/context/sync"
 import { useEvent } from "@tui/context/event"
 import { MessageID, PartID } from "@/session/schema"
-import { createStore, produce } from "solid-js/store"
+import { createStore, produce, unwrap } from "solid-js/store"
 import { useKeybind } from "@tui/context/keybind"
 import { usePromptHistory, type PromptInfo } from "./history"
 import { assign } from "./part"
@@ -75,6 +75,8 @@ function randomIndex(count: number) {
   return Math.floor(Math.random() * count)
 }
 
+let stashed: { prompt: PromptInfo; cursor: number } | undefined
+
 export function Prompt(props: PromptProps) {
   let input: TextareaRenderable
   let anchor: BoxRenderable
@@ -433,7 +435,22 @@ export function Prompt(props: PromptProps) {
     },
   }
 
+  onMount(() => {
+    const saved = stashed
+    stashed = undefined
+    if (store.prompt.input) return
+    if (saved && saved.prompt.input) {
+      input.setText(saved.prompt.input)
+      setStore("prompt", saved.prompt)
+      restoreExtmarksFromParts(saved.prompt.parts)
+      input.cursorOffset = saved.cursor
+    }
+  })
+
   onCleanup(() => {
+    if (store.prompt.input) {
+      stashed = { prompt: unwrap(store.prompt), cursor: input.cursorOffset }
+    }
     props.ref?.(undefined)
   })
 

+ 4 - 4
packages/opencode/src/cli/cmd/tui/context/route.tsx

@@ -1,16 +1,16 @@
-import { createStore } from "solid-js/store"
+import { createStore, reconcile } from "solid-js/store"
 import { createSimpleContext } from "./helper"
 import type { PromptInfo } from "../component/prompt/history"
 
 export type HomeRoute = {
   type: "home"
-  initialPrompt?: PromptInfo
+  prompt?: PromptInfo
 }
 
 export type SessionRoute = {
   type: "session"
   sessionID: string
-  initialPrompt?: PromptInfo
+  prompt?: PromptInfo
 }
 
 export type PluginRoute = {
@@ -37,7 +37,7 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
         return store
       },
       navigate(route: Route) {
-        setStore(route)
+        setStore(reconcile(route))
       },
     }
   },

+ 1 - 1
packages/opencode/src/cli/cmd/tui/plugin/api.tsx

@@ -91,7 +91,7 @@ function routeCurrent(route: ReturnType<typeof useRoute>): TuiPluginApi["route"]
       name: "session",
       params: {
         sessionID: route.data.sessionID,
-        initialPrompt: route.data.initialPrompt,
+        prompt: route.data.prompt,
       },
     }
   }

+ 2 - 3
packages/opencode/src/cli/cmd/tui/routes/home.tsx

@@ -10,7 +10,6 @@ import { usePromptRef } from "../context/prompt"
 import { useLocal } from "../context/local"
 import { TuiPluginRuntime } from "../plugin"
 
-// TODO: what is the best way to do this?
 let once = false
 const placeholder = {
   normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
@@ -31,8 +30,8 @@ export function Home() {
     setRef(r)
     promptRef.set(r)
     if (once || !r) return
-    if (route.initialPrompt) {
-      r.set(route.initialPrompt)
+    if (route.prompt) {
+      r.set(route.prompt)
       once = true
       return
     }

+ 2 - 2
packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx

@@ -38,7 +38,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
             messageID: message.id,
           })
           const parts = sync.data.part[message.id] ?? []
-          const initialPrompt = parts.reduce(
+          const prompt = parts.reduce(
             (agg, part) => {
               if (part.type === "text") {
                 if (!part.synthetic) agg.input += part.text
@@ -51,7 +51,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
           route.navigate({
             sessionID: forked.data!.id,
             type: "session",
-            initialPrompt,
+            prompt,
           })
           dialog.clear()
         },

+ 14 - 16
packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx

@@ -81,25 +81,23 @@ export function DialogMessage(props: {
               sessionID: props.sessionID,
               messageID: props.messageID,
             })
-            const initialPrompt = (() => {
-              const msg = message()
-              if (!msg) return undefined
-              const parts = sync.data.part[msg.id]
-              return parts.reduce(
-                (agg, part) => {
-                  if (part.type === "text") {
-                    if (!part.synthetic) agg.input += part.text
-                  }
-                  if (part.type === "file") agg.parts.push(part)
-                  return agg
-                },
-                { input: "", parts: [] as PromptInfo["parts"] },
-              )
-            })()
+            const msg = message()
+            const prompt = msg
+              ? sync.data.part[msg.id].reduce(
+                  (agg, part) => {
+                    if (part.type === "text") {
+                      if (!part.synthetic) agg.input += part.text
+                    }
+                    if (part.type === "file") agg.parts.push(part)
+                    return agg
+                  },
+                  { input: "", parts: [] as PromptInfo["parts"] },
+                )
+              : undefined
             route.navigate({
               sessionID: result.data!.id,
               type: "session",
-              initialPrompt,
+              prompt,
             })
             dialog.clear()
           },

+ 3 - 4
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

@@ -207,8 +207,6 @@ export function Session() {
     if (scroll) scroll.scrollBy(100_000)
   })
 
-  // Handle initial prompt from fork
-  let seeded = false
   let lastSwitch: string | undefined = undefined
   event.on("message.part.updated", (evt) => {
     const part = evt.properties.part
@@ -226,14 +224,15 @@ export function Session() {
     }
   })
 
+  let seeded = false
   let scroll: ScrollBoxRenderable
   let prompt: PromptRef | undefined
   const bind = (r: PromptRef | undefined) => {
     prompt = r
     promptRef.set(r)
-    if (seeded || !route.initialPrompt || !r) return
+    if (seeded || !route.prompt || !r) return
     seeded = true
-    r.set(route.initialPrompt)
+    r.set(route.prompt)
   }
   const keybind = useKeybind()
   const dialog = useDialog()

+ 1 - 1
packages/plugin/src/tui.ts

@@ -29,7 +29,7 @@ export type TuiRouteCurrent =
       name: "session"
       params: {
         sessionID: string
-        initialPrompt?: unknown
+        prompt?: unknown
       }
     }
   | {