summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/components/prompt-input/attachments.ts
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-06 09:37:49 -0600
committerGitHub <[email protected]>2026-02-06 09:37:49 -0600
commita4bc883595df9ea0f752079519081bc602408553 (patch)
tree583f21642f431899abe1dfb1f6bd9b2c01dc0206 /packages/app/src/components/prompt-input/attachments.ts
parentc07077f96c0019b2e18e0e8e1e0383deda08b3e6 (diff)
downloadopencode-a4bc883595df9ea0f752079519081bc602408553.tar.gz
opencode-a4bc883595df9ea0f752079519081bc602408553.zip
chore: refactoring and tests (#12468)
Diffstat (limited to 'packages/app/src/components/prompt-input/attachments.ts')
-rw-r--r--packages/app/src/components/prompt-input/attachments.ts132
1 files changed, 132 insertions, 0 deletions
diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts
new file mode 100644
index 000000000..4ea2cfb90
--- /dev/null
+++ b/packages/app/src/components/prompt-input/attachments.ts
@@ -0,0 +1,132 @@
+import { onCleanup, onMount } from "solid-js"
+import { showToast } from "@opencode-ai/ui/toast"
+import { usePrompt, type ContentPart, type ImageAttachmentPart } from "@/context/prompt"
+import { useLanguage } from "@/context/language"
+import { getCursorPosition } from "./editor-dom"
+
+export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"]
+export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"]
+
+type PromptAttachmentsInput = {
+ editor: () => HTMLDivElement | undefined
+ isFocused: () => boolean
+ isDialogActive: () => boolean
+ setDragging: (value: boolean) => void
+ addPart: (part: ContentPart) => void
+}
+
+export function createPromptAttachments(input: PromptAttachmentsInput) {
+ const prompt = usePrompt()
+ const language = useLanguage()
+
+ const addImageAttachment = async (file: File) => {
+ if (!ACCEPTED_FILE_TYPES.includes(file.type)) return
+
+ const reader = new FileReader()
+ reader.onload = () => {
+ const editor = input.editor()
+ if (!editor) return
+ const dataUrl = reader.result as string
+ const attachment: ImageAttachmentPart = {
+ type: "image",
+ id: crypto.randomUUID(),
+ filename: file.name,
+ mime: file.type,
+ dataUrl,
+ }
+ const cursorPosition = prompt.cursor() ?? getCursorPosition(editor)
+ prompt.set([...prompt.current(), attachment], cursorPosition)
+ }
+ reader.readAsDataURL(file)
+ }
+
+ const removeImageAttachment = (id: string) => {
+ 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) => {
+ if (!input.isFocused()) return
+ const clipboardData = event.clipboardData
+ if (!clipboardData) return
+
+ event.preventDefault()
+ event.stopPropagation()
+
+ const items = Array.from(clipboardData.items)
+ const fileItems = items.filter((item) => item.kind === "file")
+ const imageItems = fileItems.filter((item) => ACCEPTED_FILE_TYPES.includes(item.type))
+
+ if (imageItems.length > 0) {
+ for (const item of imageItems) {
+ const file = item.getAsFile()
+ if (file) await addImageAttachment(file)
+ }
+ return
+ }
+
+ if (fileItems.length > 0) {
+ showToast({
+ title: language.t("prompt.toast.pasteUnsupported.title"),
+ description: language.t("prompt.toast.pasteUnsupported.description"),
+ })
+ return
+ }
+
+ const plainText = clipboardData.getData("text/plain") ?? ""
+ if (!plainText) return
+ input.addPart({ type: "text", content: plainText, start: 0, end: 0 })
+ }
+
+ const handleGlobalDragOver = (event: DragEvent) => {
+ if (input.isDialogActive()) return
+
+ event.preventDefault()
+ const hasFiles = event.dataTransfer?.types.includes("Files")
+ if (hasFiles) {
+ input.setDragging(true)
+ }
+ }
+
+ const handleGlobalDragLeave = (event: DragEvent) => {
+ if (input.isDialogActive()) return
+ if (!event.relatedTarget) {
+ input.setDragging(false)
+ }
+ }
+
+ const handleGlobalDrop = async (event: DragEvent) => {
+ if (input.isDialogActive()) return
+
+ event.preventDefault()
+ input.setDragging(false)
+
+ const dropped = event.dataTransfer?.files
+ if (!dropped) return
+
+ for (const file of Array.from(dropped)) {
+ if (ACCEPTED_FILE_TYPES.includes(file.type)) {
+ await addImageAttachment(file)
+ }
+ }
+ }
+
+ onMount(() => {
+ document.addEventListener("dragover", handleGlobalDragOver)
+ document.addEventListener("dragleave", handleGlobalDragLeave)
+ document.addEventListener("drop", handleGlobalDrop)
+ })
+
+ onCleanup(() => {
+ document.removeEventListener("dragover", handleGlobalDragOver)
+ document.removeEventListener("dragleave", handleGlobalDragLeave)
+ document.removeEventListener("drop", handleGlobalDrop)
+ })
+
+ return {
+ addImageAttachment,
+ removeImageAttachment,
+ handlePaste,
+ }
+}