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

This commit is contained in:
Dax
2026-04-17 00:23:30 -04:00
committed by GitHub
parent dfaae14544
commit 4bd5a158a5
9 changed files with 45 additions and 36 deletions

View File

@@ -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()
},

View File

@@ -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)
})

View File

@@ -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))
},
}
},

View File

@@ -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,
},
}
}

View File

@@ -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
}

View File

@@ -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()
},

View File

@@ -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()
},

View File

@@ -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()

View File

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