diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 278c256898..77b72246ed 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -426,7 +426,6 @@ function App(props: { onSnapshot?: () => Promise }) { route.navigate({ type: "home", - initialPrompt: currentPrompt, }) dialog.clear() }, diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx index d49dd5c7b6..3c2ba04857 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/history.tsx @@ -25,6 +25,11 @@ export type PromptInfo = { )[] } +export type PromptDraft = { + prompt: PromptInfo + cursor: number +} + const MAX_HISTORY_ENTRIES = 50 export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({ 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 f49bd734b3..2fb38a33c8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -15,7 +15,7 @@ import { homeScope, sessionScope, usePromptRef } from "@tui/context/prompt" import { MessageID, PartID } from "@/session/schema" import { createStore, produce, unwrap } from "solid-js/store" import { useKeybind } from "@tui/context/keybind" -import { usePromptHistory, type PromptInfo } from "./history" +import { usePromptHistory, type PromptDraft, type PromptInfo } from "./history" import { assign } from "./part" import { usePromptStash } from "./stash" import { DialogStash } from "../dialog-stash" @@ -58,7 +58,8 @@ export type PromptProps = { export type PromptRef = { focused: boolean current: PromptInfo - snapshot(): PromptInfo + snapshot(): PromptDraft + restore(draft: PromptDraft): void set(prompt: PromptInfo): void reset(): void blur(): void @@ -421,16 +422,16 @@ export function Prompt(props: PromptProps) { } } - function restorePrompt(prompt: PromptInfo) { - const next = structuredClone(unwrap(prompt)) - input.setText(next.input) - setStore("mode", next.mode ?? "normal") + function restorePrompt(draft: PromptDraft) { + const next = structuredClone(unwrap(draft)) + input.setText(next.prompt.input) + setStore("mode", next.prompt.mode ?? "normal") setStore("prompt", { - input: next.input, - parts: next.parts, + input: next.prompt.input, + parts: next.prompt.parts, }) - restoreExtmarksFromParts(next.parts) - input.gotoBufferEnd() + restoreExtmarksFromParts(next.prompt.parts) + input.cursorOffset = next.cursor } function snapshot() { @@ -444,10 +445,13 @@ export function Prompt(props: PromptProps) { } return { - input: value, - mode: store.mode, - parts: structuredClone(unwrap(store.prompt.parts)), - } satisfies PromptInfo + prompt: { + input: value, + mode: store.mode, + parts: structuredClone(unwrap(store.prompt.parts)), + }, + cursor: input && !input.isDestroyed ? input.cursorOffset : Bun.stringWidth(value), + } satisfies PromptDraft } const ref: PromptRef = { @@ -464,6 +468,12 @@ export function Prompt(props: PromptProps) { snapshot() { return snapshot() }, + restore(draft) { + restorePrompt(draft) + if (active) { + promptState.save(active, draft) + } + }, focus() { input.focus() }, @@ -471,10 +481,10 @@ export function Prompt(props: PromptProps) { input.blur() }, set(prompt) { - restorePrompt(prompt) - if (active) { - promptState.save(active, snapshot()) - } + ref.restore({ + prompt: structuredClone(unwrap(prompt)), + cursor: Bun.stringWidth(prompt.input), + }) }, reset() { clearPrompt() @@ -496,32 +506,24 @@ export function Prompt(props: PromptProps) { const prev = active if (prev) { promptState.save(prev, snapshot()) - promptState.unbind(prev, ref) + promptState.bind(prev, undefined) } active = next + promptState.bind(next, ref) const draft = promptState.load(next) if (draft) { - restorePrompt(draft) - } else if (!prev) { - const prompt = snapshot() - if (prompt.input || prompt.parts.length > 0) { - promptState.save(next, prompt) - } else { - clearPrompt("normal") - } + ref.restore(draft) } else { clearPrompt("normal") } - - promptState.bind(next, ref) }) onCleanup(() => { if (active) { promptState.save(active, snapshot()) - promptState.unbind(active, ref) + promptState.bind(active, undefined) } props.ref?.(undefined) }) @@ -633,10 +635,10 @@ export function Prompt(props: PromptProps) { enabled: !!store.prompt.input || store.prompt.parts.length > 0, onSelect: (dialog) => { const prompt = snapshot() - if (!prompt.input && prompt.parts.length === 0) return + if (!prompt.prompt.input && prompt.prompt.parts.length === 0) return stash.push({ - input: prompt.input, - parts: prompt.parts, + input: prompt.prompt.input, + parts: prompt.prompt.parts, }) ref.reset() dialog.clear() @@ -650,10 +652,7 @@ export function Prompt(props: PromptProps) { onSelect: (dialog) => { const entry = stash.pop() if (entry) { - input.setText(entry.input) - setStore("prompt", { input: entry.input, parts: entry.parts }) - restoreExtmarksFromParts(entry.parts) - input.gotoBufferEnd() + ref.set({ input: entry.input, parts: entry.parts }) } dialog.clear() }, @@ -667,10 +666,7 @@ export function Prompt(props: PromptProps) { dialog.replace(() => ( { - input.setText(entry.input) - setStore("prompt", { input: entry.input, parts: entry.parts }) - restoreExtmarksFromParts(entry.parts) - input.gotoBufferEnd() + ref.set({ input: entry.input, parts: entry.parts }) }} /> )) @@ -683,9 +679,9 @@ export function Prompt(props: PromptProps) { if (props.disabled) return if (autocomplete?.visible) return - if (!prompt.input) return + if (!prompt.prompt.input) return - const trimmed = prompt.input.trim() + const trimmed = prompt.prompt.input.trim() if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") { exit() return @@ -717,7 +713,7 @@ export function Prompt(props: PromptProps) { } const messageID = MessageID.ascending() - let inputText = prompt.input + let inputText = prompt.prompt.input // Expand pasted text inline before submitting const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId) @@ -726,7 +722,7 @@ export function Prompt(props: PromptProps) { for (const extmark of sortedExtmarks) { const partIndex = store.extmarkToPartIndex.get(extmark.id) if (partIndex !== undefined) { - const part = prompt.parts[partIndex] + const part = prompt.prompt.parts[partIndex] if (part?.type === "text" && part.text) { const before = inputText.slice(0, extmark.start) const after = inputText.slice(extmark.end) @@ -736,10 +732,10 @@ export function Prompt(props: PromptProps) { } // Filter out text parts (pasted content) since they're now expanded inline - const nonTextParts = prompt.parts.filter((part) => part.type !== "text") + const nonTextParts = prompt.prompt.parts.filter((part) => part.type !== "text") // Capture mode before it gets reset - const currentMode = prompt.mode ?? "normal" + const currentMode = prompt.prompt.mode ?? "normal" const variant = local.model.variant.current() if (currentMode === "shell") { @@ -803,7 +799,7 @@ export function Prompt(props: PromptProps) { }) .catch(() => {}) } - history.append(prompt) + history.append(prompt.prompt) clearPrompt() if (active) { promptState.drop(active) diff --git a/packages/opencode/src/cli/cmd/tui/context/prompt.tsx b/packages/opencode/src/cli/cmd/tui/context/prompt.tsx index af7065fa37..17bc4c3c5a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/prompt.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/prompt.tsx @@ -1,7 +1,7 @@ import { createSimpleContext } from "./helper" import { unwrap } from "solid-js/store" import type { PromptRef } from "../component/prompt" -import type { PromptInfo } from "../component/prompt/history" +import type { PromptDraft, PromptInfo } from "../component/prompt/history" export function homeScope(workspaceID?: string) { if (!workspaceID) return "home" @@ -12,12 +12,21 @@ export function sessionScope(sessionID: string) { return `session:${sessionID}` } -function clone(prompt: PromptInfo) { - return structuredClone(unwrap(prompt)) +function clone(value: T) { + return structuredClone(unwrap(value)) } -function empty(prompt?: PromptInfo) { - if (!prompt) return true +function draft(input: PromptInfo | PromptDraft) { + if ("prompt" in input) return clone(input) + return { + prompt: clone(input), + cursor: Bun.stringWidth(input.input), + } satisfies PromptDraft +} + +function empty(input?: PromptInfo | PromptDraft) { + if (!input) return true + const prompt = "prompt" in input ? input.prompt : input if (prompt.input) return false return prompt.parts.length === 0 } @@ -25,30 +34,29 @@ function empty(prompt?: PromptInfo) { export const { use: usePromptRef, provider: PromptRefProvider } = createSimpleContext({ name: "PromptRef", init: () => { - const drafts = new Map() - const refs = new Map() + const drafts = new Map() + let live: { scope: string; ref: PromptRef } | undefined function load(scope: string) { - const prompt = drafts.get(scope) - if (!prompt) return - return clone(prompt) + const value = drafts.get(scope) + if (!value) return + return clone(value) } - function save(scope: string, prompt: PromptInfo) { - if (empty(prompt)) { + function save(scope: string, input: PromptInfo | PromptDraft) { + if (empty(input)) { drafts.delete(scope) return } - drafts.set(scope, clone(prompt)) + drafts.set(scope, draft(input)) } return { current(scope: string) { - const ref = refs.get(scope) - if (ref) { - const prompt = ref.snapshot() - if (!empty(prompt)) return prompt + if (live?.scope === scope) { + const value = live.ref.snapshot() + if (!empty(value)) return value return } @@ -56,21 +64,22 @@ export const { use: usePromptRef, provider: PromptRefProvider } = createSimpleCo }, load, save, - apply(scope: string, prompt: PromptInfo) { - save(scope, prompt) - const ref = refs.get(scope) - if (!ref) return - ref.set(prompt) + apply(scope: string, input: PromptInfo | PromptDraft) { + const value = draft(input) + save(scope, value) + if (live?.scope !== scope) return + live.ref.restore(value) }, drop(scope: string) { drafts.delete(scope) }, - bind(scope: string, ref: PromptRef) { - refs.set(scope, ref) - }, - unbind(scope: string, ref: PromptRef) { - if (refs.get(scope) !== ref) return - refs.delete(scope) + bind(scope: string, ref: PromptRef | undefined) { + if (!ref) { + if (live?.scope === scope) live = undefined + return + } + + live = { scope, ref } }, } },