summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-01-05 12:52:49 -0600
committerAdam <[email protected]>2026-01-05 13:21:33 -0600
commitec637aa21e65ede960d75b7ba9242ad0ae6bfcac (patch)
tree124869465b53e1cfc505bb17d4c2a8768502bb3d /packages/app/src/components
parent2ff9a757b670d94b22822e7e230bd9fb58de3bed (diff)
downloadopencode-ec637aa21e65ede960d75b7ba9242ad0ae6bfcac.tar.gz
opencode-ec637aa21e65ede960d75b7ba9242ad0ae6bfcac.zip
fix(app): store image attachments
Diffstat (limited to 'packages/app/src/components')
-rw-r--r--packages/app/src/components/prompt-input.tsx50
1 files changed, 22 insertions, 28 deletions
diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx
index d269976f3..fc4a3d1e6 100644
--- a/packages/app/src/components/prompt-input.tsx
+++ b/packages/app/src/components/prompt-input.tsx
@@ -165,6 +165,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
},
)
const working = createMemo(() => status()?.type !== "idle")
+ const imageAttachments = createMemo(
+ () => prompt.current().filter((part) => part.type === "image") as ImageAttachmentPart[],
+ )
const [store, setStore] = createStore<{
popover: "at" | "slash" | null
@@ -172,7 +175,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
savedPrompt: Prompt | null
placeholder: number
dragging: boolean
- imageAttachments: ImageAttachmentPart[]
mode: "normal" | "shell"
applyingHistory: boolean
}>({
@@ -181,7 +183,6 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
savedPrompt: null,
placeholder: Math.floor(Math.random() * PLACEHOLDERS.length),
dragging: false,
- imageAttachments: [],
mode: "normal",
applyingHistory: false,
})
@@ -274,21 +275,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
mime: file.type,
dataUrl,
}
- setStore(
- produce((draft) => {
- draft.imageAttachments.push(attachment)
- }),
- )
+ const cursorPosition = prompt.cursor() ?? getCursorPosition(editorRef)
+ prompt.set([...prompt.current(), attachment], cursorPosition)
}
reader.readAsDataURL(file)
}
const removeImageAttachment = (id: string) => {
- setStore(
- produce((draft) => {
- draft.imageAttachments = draft.imageAttachments.filter((a) => a.id !== id)
- }),
- )
+ const current = prompt.current()
+ const next = current.filter((part) => part.type !== "image" || part.id !== id)
+ prompt.set(next, prompt.cursor())
}
const handlePaste = async (event: ClipboardEvent) => {
@@ -538,8 +534,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
on(
() => prompt.current(),
(currentParts) => {
+ const inputParts = currentParts.filter((part) => part.type !== "image") as Prompt
const domParts = parseFromDOM()
- if (isNormalizedEditor() && isPromptEqual(currentParts, domParts)) return
+ if (isNormalizedEditor() && isPromptEqual(inputParts, domParts)) return
const selection = window.getSelection()
let cursorPosition: number | null = null
@@ -547,7 +544,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
cursorPosition = getCursorPosition(editorRef)
}
- renderEditor(currentParts)
+ renderEditor(inputParts)
if (cursorPosition !== null) {
setCursorPosition(editorRef, cursorPosition)
@@ -638,11 +635,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const handleInput = () => {
const rawParts = parseFromDOM()
+ const images = imageAttachments()
const cursorPosition = getCursorPosition(editorRef)
const rawText = rawParts.map((p) => ("content" in p ? p.content : "")).join("")
const trimmed = rawText.replace(/\u200B/g, "").trim()
const hasNonText = rawParts.some((part) => part.type !== "text")
- const shouldReset = trimmed.length === 0 && !hasNonText
+ const shouldReset = trimmed.length === 0 && !hasNonText && images.length === 0
if (shouldReset) {
setStore("popover", null)
@@ -681,7 +679,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
setStore("savedPrompt", null)
}
- prompt.set(rawParts, cursorPosition)
+ prompt.set([...rawParts, ...images], cursorPosition)
queueScroll()
}
@@ -784,16 +782,14 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
.map((p) => ("content" in p ? p.content : ""))
.join("")
.trim()
- if (!text) return
+ const hasImages = prompt.some((part) => part.type === "image")
+ if (!text && !hasImages) return
const entry = clonePromptParts(prompt)
const currentHistory = mode === "shell" ? shellHistory : history
const setCurrentHistory = mode === "shell" ? setShellHistory : setHistory
const lastEntry = currentHistory.entries[0]
- if (lastEntry) {
- const lastText = lastEntry.map((p) => ("content" in p ? p.content : "")).join("")
- if (lastText === text) return
- }
+ if (lastEntry && isPromptEqual(lastEntry, entry)) return
setCurrentHistory("entries", (entries) => [entry, ...entries].slice(0, MAX_HISTORY))
}
@@ -967,7 +963,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const currentPrompt = prompt.current()
const text = currentPrompt.map((part) => ("content" in part ? part.content : "")).join("")
- const images = store.imageAttachments.slice()
+ const images = imageAttachments().slice()
const mode = store.mode
if (text.trim().length === 0 && images.length === 0) {
@@ -1061,14 +1057,12 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const clearInput = () => {
prompt.reset()
- setStore("imageAttachments", [])
setStore("mode", "normal")
setStore("popover", null)
}
const restoreInput = () => {
prompt.set(currentPrompt, promptLength(currentPrompt))
- setStore("imageAttachments", images)
setStore("mode", mode)
setStore("popover", null)
requestAnimationFrame(() => {
@@ -1471,9 +1465,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
</For>
</div>
</Show>
- <Show when={store.imageAttachments.length > 0}>
+ <Show when={imageAttachments().length > 0}>
<div class="flex flex-wrap gap-2 px-3 pt-3">
- <For each={store.imageAttachments}>
+ <For each={imageAttachments()}>
{(attachment) => (
<div class="relative group">
<Show
@@ -1525,7 +1519,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
"font-mono!": store.mode === "shell",
}}
/>
- <Show when={!prompt.dirty() && store.imageAttachments.length === 0}>
+ <Show when={!prompt.dirty()}>
<div class="absolute top-0 inset-x-0 px-5 py-3 pr-12 text-14-regular text-text-weak pointer-events-none whitespace-nowrap truncate">
{store.mode === "shell"
? "Enter shell command..."
@@ -1658,7 +1652,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
>
<IconButton
type="submit"
- disabled={!prompt.dirty() && store.imageAttachments.length === 0 && !working()}
+ disabled={!prompt.dirty() && !working()}
icon={working() ? "stop" : "arrow-up"}
variant="primary"
class="h-6 w-4.5"