fix: preserve prompt input across unmount/remount cycles (#22508)
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export type TuiRouteCurrent =
|
|||||||
name: "session"
|
name: "session"
|
||||||
params: {
|
params: {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
initialPrompt?: unknown
|
prompt?: unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
Reference in New Issue
Block a user