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 4b852e66..408c62cd 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -101,16 +101,81 @@ export function Prompt(props: PromptProps) { value: "prompt.editor", onSelect: async (dialog, trigger) => { dialog.clear() - const value = trigger === "prompt" ? "" : input.plainText + + // replace summarized text parts with the actual text + const text = store.prompt.parts + .filter((p) => p.type === "text") + .reduce((acc, p) => { + if (!p.source) return acc + return acc.replace(p.source.text.value, p.text) + }, store.prompt.input) + + const nonTextParts = store.prompt.parts.filter((p) => p.type !== "text") + + const value = trigger === "prompt" ? "" : text const content = await Editor.open({ value, renderer }) - if (content) { - input.setText(content, { history: false }) - setStore("prompt", { - input: content, - parts: [], + if (!content) return + + input.setText(content, { history: false }) + + // Update positions for nonTextParts based on their location in new content + // Filter out parts whose virtual text was deleted + // this handles a case where the user edits the text in the editor + // such that the virtual text moves around or is deleted + const updatedNonTextParts = nonTextParts + .map((part) => { + let virtualText = "" + if (part.type === "file" && part.source?.text) { + virtualText = part.source.text.value + } else if (part.type === "agent" && part.source) { + virtualText = part.source.value + } + + if (!virtualText) return part + + const newStart = content.indexOf(virtualText) + // if the virtual text is deleted, remove the part + if (newStart === -1) return null + + const newEnd = newStart + virtualText.length + + if (part.type === "file" && part.source?.text) { + return { + ...part, + source: { + ...part.source, + text: { + ...part.source.text, + start: newStart, + end: newEnd, + }, + }, + } + } + + if (part.type === "agent" && part.source) { + return { + ...part, + source: { + ...part.source, + start: newStart, + end: newEnd, + }, + } + } + + return part }) - input.cursorOffset = Bun.stringWidth(content) - } + .filter((part) => part !== null) + + setStore("prompt", { + input: content, + // keep only the non-text parts because the text parts were + // already expanded inline + parts: updatedNonTextParts, + }) + restoreExtmarksFromParts(updatedNonTextParts) + input.cursorOffset = Bun.stringWidth(content) }, }, {