Skip to content

Commit 4bd5a15

Browse files
authored
fix: preserve prompt input across unmount/remount cycles (#22508)
1 parent dfaae14 commit 4bd5a15

9 files changed

Lines changed: 45 additions & 36 deletions

File tree

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,8 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
420420
aliases: ["clear"],
421421
},
422422
onSelect: () => {
423-
const current = promptRef.current
424-
// Don't require focus - if there's any text, preserve it
425-
const currentPrompt = current?.current?.input ? current.current : undefined
426423
route.navigate({
427424
type: "home",
428-
initialPrompt: currentPrompt,
429425
})
430426
dialog.clear()
431427
},

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useRoute } from "@tui/context/route"
1212
import { useSync } from "@tui/context/sync"
1313
import { useEvent } from "@tui/context/event"
1414
import { MessageID, PartID } from "@/session/schema"
15-
import { createStore, produce } from "solid-js/store"
15+
import { createStore, produce, unwrap } from "solid-js/store"
1616
import { useKeybind } from "@tui/context/keybind"
1717
import { usePromptHistory, type PromptInfo } from "./history"
1818
import { assign } from "./part"
@@ -75,6 +75,8 @@ function randomIndex(count: number) {
7575
return Math.floor(Math.random() * count)
7676
}
7777

78+
let stashed: { prompt: PromptInfo; cursor: number } | undefined
79+
7880
export function Prompt(props: PromptProps) {
7981
let input: TextareaRenderable
8082
let anchor: BoxRenderable
@@ -433,7 +435,22 @@ export function Prompt(props: PromptProps) {
433435
},
434436
}
435437

438+
onMount(() => {
439+
const saved = stashed
440+
stashed = undefined
441+
if (store.prompt.input) return
442+
if (saved && saved.prompt.input) {
443+
input.setText(saved.prompt.input)
444+
setStore("prompt", saved.prompt)
445+
restoreExtmarksFromParts(saved.prompt.parts)
446+
input.cursorOffset = saved.cursor
447+
}
448+
})
449+
436450
onCleanup(() => {
451+
if (store.prompt.input) {
452+
stashed = { prompt: unwrap(store.prompt), cursor: input.cursorOffset }
453+
}
437454
props.ref?.(undefined)
438455
})
439456

packages/opencode/src/cli/cmd/tui/context/route.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import { createStore } from "solid-js/store"
1+
import { createStore, reconcile } from "solid-js/store"
22
import { createSimpleContext } from "./helper"
33
import type { PromptInfo } from "../component/prompt/history"
44

55
export type HomeRoute = {
66
type: "home"
7-
initialPrompt?: PromptInfo
7+
prompt?: PromptInfo
88
}
99

1010
export type SessionRoute = {
1111
type: "session"
1212
sessionID: string
13-
initialPrompt?: PromptInfo
13+
prompt?: PromptInfo
1414
}
1515

1616
export type PluginRoute = {
@@ -37,7 +37,7 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
3737
return store
3838
},
3939
navigate(route: Route) {
40-
setStore(route)
40+
setStore(reconcile(route))
4141
},
4242
}
4343
},

packages/opencode/src/cli/cmd/tui/plugin/api.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function routeCurrent(route: ReturnType<typeof useRoute>): TuiPluginApi["route"]
9191
name: "session",
9292
params: {
9393
sessionID: route.data.sessionID,
94-
initialPrompt: route.data.initialPrompt,
94+
prompt: route.data.prompt,
9595
},
9696
}
9797
}

packages/opencode/src/cli/cmd/tui/routes/home.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { usePromptRef } from "../context/prompt"
1010
import { useLocal } from "../context/local"
1111
import { TuiPluginRuntime } from "../plugin"
1212

13-
// TODO: what is the best way to do this?
1413
let once = false
1514
const placeholder = {
1615
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() {
3130
setRef(r)
3231
promptRef.set(r)
3332
if (once || !r) return
34-
if (route.initialPrompt) {
35-
r.set(route.initialPrompt)
33+
if (route.prompt) {
34+
r.set(route.prompt)
3635
once = true
3736
return
3837
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
3838
messageID: message.id,
3939
})
4040
const parts = sync.data.part[message.id] ?? []
41-
const initialPrompt = parts.reduce(
41+
const prompt = parts.reduce(
4242
(agg, part) => {
4343
if (part.type === "text") {
4444
if (!part.synthetic) agg.input += part.text
@@ -51,7 +51,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
5151
route.navigate({
5252
sessionID: forked.data!.id,
5353
type: "session",
54-
initialPrompt,
54+
prompt,
5555
})
5656
dialog.clear()
5757
},

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

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -81,25 +81,23 @@ export function DialogMessage(props: {
8181
sessionID: props.sessionID,
8282
messageID: props.messageID,
8383
})
84-
const initialPrompt = (() => {
85-
const msg = message()
86-
if (!msg) return undefined
87-
const parts = sync.data.part[msg.id]
88-
return parts.reduce(
89-
(agg, part) => {
90-
if (part.type === "text") {
91-
if (!part.synthetic) agg.input += part.text
92-
}
93-
if (part.type === "file") agg.parts.push(part)
94-
return agg
95-
},
96-
{ input: "", parts: [] as PromptInfo["parts"] },
97-
)
98-
})()
84+
const msg = message()
85+
const prompt = msg
86+
? sync.data.part[msg.id].reduce(
87+
(agg, part) => {
88+
if (part.type === "text") {
89+
if (!part.synthetic) agg.input += part.text
90+
}
91+
if (part.type === "file") agg.parts.push(part)
92+
return agg
93+
},
94+
{ input: "", parts: [] as PromptInfo["parts"] },
95+
)
96+
: undefined
9997
route.navigate({
10098
sessionID: result.data!.id,
10199
type: "session",
102-
initialPrompt,
100+
prompt,
103101
})
104102
dialog.clear()
105103
},

packages/opencode/src/cli/cmd/tui/routes/session/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,6 @@ export function Session() {
207207
if (scroll) scroll.scrollBy(100_000)
208208
})
209209

210-
// Handle initial prompt from fork
211-
let seeded = false
212210
let lastSwitch: string | undefined = undefined
213211
event.on("message.part.updated", (evt) => {
214212
const part = evt.properties.part
@@ -226,14 +224,15 @@ export function Session() {
226224
}
227225
})
228226

227+
let seeded = false
229228
let scroll: ScrollBoxRenderable
230229
let prompt: PromptRef | undefined
231230
const bind = (r: PromptRef | undefined) => {
232231
prompt = r
233232
promptRef.set(r)
234-
if (seeded || !route.initialPrompt || !r) return
233+
if (seeded || !route.prompt || !r) return
235234
seeded = true
236-
r.set(route.initialPrompt)
235+
r.set(route.prompt)
237236
}
238237
const keybind = useKeybind()
239238
const dialog = useDialog()

packages/plugin/src/tui.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export type TuiRouteCurrent =
2929
name: "session"
3030
params: {
3131
sessionID: string
32-
initialPrompt?: unknown
32+
prompt?: unknown
3333
}
3434
}
3535
| {

0 commit comments

Comments
 (0)