fix(app): store image attachments

This commit is contained in:
Adam
2026-01-05 12:52:49 -06:00
parent 2ff9a757b6
commit ec637aa21e
3 changed files with 193 additions and 61 deletions

View File

@@ -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"