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

This commit is contained in:
Dax
2026-04-17 04:23:30 +00:00
committed by GitHub
parent dfaae14544
commit 4bd5a158a5
9 changed files with 45 additions and 36 deletions
@@ -420,12 +420,8 @@ function App(props: { onSnapshot?: () => Promise<string[]> }) {
aliases: ["clear"], aliases: ["clear"],
}, },
onSelect: () => { 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({ route.navigate({
type: "home", type: "home",
initialPrompt: currentPrompt,
}) })
dialog.clear() dialog.clear()
}, },
@@ -12,7 +12,7 @@ import { useRoute } from "@tui/context/route"
import { useSync } from "@tui/context/sync" import { useSync } from "@tui/context/sync"
import { useEvent } from "@tui/context/event" import { useEvent } from "@tui/context/event"
import { MessageID, PartID } from "@/session/schema" 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 { useKeybind } from "@tui/context/keybind"
import { usePromptHistory, type PromptInfo } from "./history" import { usePromptHistory, type PromptInfo } from "./history"
import { assign } from "./part" import { assign } from "./part"
@@ -75,6 +75,8 @@ function randomIndex(count: number) {
return Math.floor(Math.random() * count) return Math.floor(Math.random() * count)
} }
let stashed: { prompt: PromptInfo; cursor: number } | undefined
export function Prompt(props: PromptProps) { export function Prompt(props: PromptProps) {
let input: TextareaRenderable let input: TextareaRenderable
let anchor: BoxRenderable 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(() => { onCleanup(() => {
if (store.prompt.input) {
stashed = { prompt: unwrap(store.prompt), cursor: input.cursorOffset }
}
props.ref?.(undefined) props.ref?.(undefined)
}) })
@@ -1,16 +1,16 @@
import { createStore } from "solid-js/store" import { createStore, reconcile } from "solid-js/store"
import { createSimpleContext } from "./helper" import { createSimpleContext } from "./helper"
import type { PromptInfo } from "../component/prompt/history" import type { PromptInfo } from "../component/prompt/history"
export type HomeRoute = { export type HomeRoute = {
type: "home" type: "home"
initialPrompt?: PromptInfo prompt?: PromptInfo
} }
export type SessionRoute = { export type SessionRoute = {
type: "session" type: "session"
sessionID: string sessionID: string
initialPrompt?: PromptInfo prompt?: PromptInfo
} }
export type PluginRoute = { export type PluginRoute = {
@@ -37,7 +37,7 @@ export const { use: useRoute, provider: RouteProvider } = createSimpleContext({
return store return store
}, },
navigate(route: Route) { navigate(route: Route) {
setStore(route) setStore(reconcile(route))
}, },
} }
}, },
@@ -91,7 +91,7 @@ function routeCurrent(route: ReturnType<typeof useRoute>): TuiPluginApi["route"]
name: "session", name: "session",
params: { params: {
sessionID: route.data.sessionID, sessionID: route.data.sessionID,
initialPrompt: route.data.initialPrompt, prompt: route.data.prompt,
}, },
} }
} }
@@ -10,7 +10,6 @@ import { usePromptRef } from "../context/prompt"
import { useLocal } from "../context/local" import { useLocal } from "../context/local"
import { TuiPluginRuntime } from "../plugin" import { TuiPluginRuntime } from "../plugin"
// TODO: what is the best way to do this?
let once = false let once = false
const placeholder = { const placeholder = {
normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"], 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) setRef(r)
promptRef.set(r) promptRef.set(r)
if (once || !r) return if (once || !r) return
if (route.initialPrompt) { if (route.prompt) {
r.set(route.initialPrompt) r.set(route.prompt)
once = true once = true
return return
} }
@@ -38,7 +38,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
messageID: message.id, messageID: message.id,
}) })
const parts = sync.data.part[message.id] ?? [] const parts = sync.data.part[message.id] ?? []
const initialPrompt = parts.reduce( const prompt = parts.reduce(
(agg, part) => { (agg, part) => {
if (part.type === "text") { if (part.type === "text") {
if (!part.synthetic) agg.input += part.text if (!part.synthetic) agg.input += part.text
@@ -51,7 +51,7 @@ export function DialogForkFromTimeline(props: { sessionID: string; onMove: (mess
route.navigate({ route.navigate({
sessionID: forked.data!.id, sessionID: forked.data!.id,
type: "session", type: "session",
initialPrompt, prompt,
}) })
dialog.clear() dialog.clear()
}, },
@@ -81,25 +81,23 @@ export function DialogMessage(props: {
sessionID: props.sessionID, sessionID: props.sessionID,
messageID: props.messageID, messageID: props.messageID,
}) })
const initialPrompt = (() => { const msg = message()
const msg = message() const prompt = msg
if (!msg) return undefined ? sync.data.part[msg.id].reduce(
const parts = sync.data.part[msg.id] (agg, part) => {
return parts.reduce( if (part.type === "text") {
(agg, part) => { if (!part.synthetic) agg.input += part.text
if (part.type === "text") { }
if (!part.synthetic) agg.input += part.text if (part.type === "file") agg.parts.push(part)
} return agg
if (part.type === "file") agg.parts.push(part) },
return agg { input: "", parts: [] as PromptInfo["parts"] },
}, )
{ input: "", parts: [] as PromptInfo["parts"] }, : undefined
)
})()
route.navigate({ route.navigate({
sessionID: result.data!.id, sessionID: result.data!.id,
type: "session", type: "session",
initialPrompt, prompt,
}) })
dialog.clear() dialog.clear()
}, },
@@ -207,8 +207,6 @@ export function Session() {
if (scroll) scroll.scrollBy(100_000) if (scroll) scroll.scrollBy(100_000)
}) })
// Handle initial prompt from fork
let seeded = false
let lastSwitch: string | undefined = undefined let lastSwitch: string | undefined = undefined
event.on("message.part.updated", (evt) => { event.on("message.part.updated", (evt) => {
const part = evt.properties.part const part = evt.properties.part
@@ -226,14 +224,15 @@ export function Session() {
} }
}) })
let seeded = false
let scroll: ScrollBoxRenderable let scroll: ScrollBoxRenderable
let prompt: PromptRef | undefined let prompt: PromptRef | undefined
const bind = (r: PromptRef | undefined) => { const bind = (r: PromptRef | undefined) => {
prompt = r prompt = r
promptRef.set(r) promptRef.set(r)
if (seeded || !route.initialPrompt || !r) return if (seeded || !route.prompt || !r) return
seeded = true seeded = true
r.set(route.initialPrompt) r.set(route.prompt)
} }
const keybind = useKeybind() const keybind = useKeybind()
const dialog = useDialog() const dialog = useDialog()
+1 -1
View File
@@ -29,7 +29,7 @@ export type TuiRouteCurrent =
name: "session" name: "session"
params: { params: {
sessionID: string sessionID: string
initialPrompt?: unknown prompt?: unknown
} }
} }
| { | {