summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorDax <[email protected]>2026-04-17 00:23:30 -0400
committerGitHub <[email protected]>2026-04-17 04:23:30 +0000
commit4bd5a158a5cbc09ac52df8dc7001fb3dc4110506 (patch)
tree73d29bb8324c65c8e637f749eef6847a01af81d6 /packages
parentdfaae1454454f02db7879ad7bdfd8a18feba83b5 (diff)
downloadopencode-4bd5a158a5cbc09ac52df8dc7001fb3dc4110506.tar.gz
opencode-4bd5a158a5cbc09ac52df8dc7001fb3dc4110506.zip
fix: preserve prompt input across unmount/remount cycles (#22508)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx4
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx19
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/route.tsx8
-rw-r--r--packages/opencode/src/cli/cmd/tui/plugin/api.tsx2
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/home.tsx5
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx4
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx30
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx7
-rw-r--r--packages/plugin/src/tui.ts2
9 files changed, 45 insertions, 36 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index 8255c007d..7e883ec0e 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/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()
},
diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
index 82c4a7222..82cdefebc 100644
--- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
+++ b/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)
})
diff --git a/packages/opencode/src/cli/cmd/tui/context/route.tsx b/packages/opencode/src/cli/cmd/tui/context/route.tsx
index e9f463a13..6db824759 100644
--- a/packages/opencode/src/cli/cmd/tui/context/route.tsx
+++ b/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))
},
}
},
diff --git a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx b/packages/opencode/src/cli/cmd/tui/plugin/api.tsx
index d2b495ca3..5bea48380 100644
--- a/packages/opencode/src/cli/cmd/tui/plugin/api.tsx
+++ b/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,
},
}
}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx
index 1cce7fb39..2f0ff07e9 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx
+++ b/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
}
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx
index 0ce33a59a..8d1e4438c 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx
+++ b/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()
},
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
index 412b4d87e..aeea2f52a 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/dialog-message.tsx
+++ b/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()
},
diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
index 70a4b73b9..ccca4d1eb 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/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()
diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts
index 099cf2758..1c57a71ab 100644
--- a/packages/plugin/src/tui.ts
+++ b/packages/plugin/src/tui.ts
@@ -29,7 +29,7 @@ export type TuiRouteCurrent =
name: "session"
params: {
sessionID: string
- initialPrompt?: unknown
+ prompt?: unknown
}
}
| {