feat(opencode): Add PDF attachment Drag and Drop (#16926)

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
pull/21204/head^2
gitpush-gitpaid 2026-04-07 00:39:59 -04:00 committed by GitHub
parent 3ea6413407
commit 3c96bf8468
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 26 additions and 12 deletions

View File

@ -2,6 +2,7 @@ import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, decodePasteB
import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js" import { createEffect, createMemo, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js"
import "opentui-spinner/solid" import "opentui-spinner/solid"
import path from "path" import path from "path"
import { fileURLToPath } from "url"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util/filesystem"
import { useLocal } from "@tui/context/local" import { useLocal } from "@tui/context/local"
import { useTheme } from "@tui/context/theme" import { useTheme } from "@tui/context/theme"
@ -248,7 +249,7 @@ export function Prompt(props: PromptProps) {
onSelect: async () => { onSelect: async () => {
const content = await Clipboard.read() const content = await Clipboard.read()
if (content?.mime.startsWith("image/")) { if (content?.mime.startsWith("image/")) {
await pasteImage({ await pasteAttachment({
filename: "clipboard", filename: "clipboard",
mime: content.mime, mime: content.mime,
content: content.data, content: content.data,
@ -771,11 +772,16 @@ export function Prompt(props: PromptProps) {
) )
} }
async function pasteImage(file: { filename?: string; content: string; mime: string }) { async function pasteAttachment(file: { filename?: string; filepath?: string; content: string; mime: string }) {
const currentOffset = input.visualCursor.offset const currentOffset = input.visualCursor.offset
const extmarkStart = currentOffset const extmarkStart = currentOffset
const count = store.prompt.parts.filter((x) => x.type === "file" && x.mime.startsWith("image/")).length const pdf = file.mime === "application/pdf"
const virtualText = `[Image ${count + 1}]` const count = store.prompt.parts.filter((x) => {
if (x.type !== "file") return false
if (pdf) return x.mime === "application/pdf"
return x.mime.startsWith("image/")
}).length
const virtualText = pdf ? `[PDF ${count + 1}]` : `[Image ${count + 1}]`
const extmarkEnd = extmarkStart + virtualText.length const extmarkEnd = extmarkStart + virtualText.length
const textToInsert = virtualText + " " const textToInsert = virtualText + " "
@ -796,7 +802,7 @@ export function Prompt(props: PromptProps) {
url: `data:${file.mime};base64,${file.content}`, url: `data:${file.mime};base64,${file.content}`,
source: { source: {
type: "file", type: "file",
path: file.filename ?? "", path: file.filepath ?? file.filename ?? "",
text: { text: {
start: extmarkStart, start: extmarkStart,
end: extmarkEnd, end: extmarkEnd,
@ -926,7 +932,7 @@ export function Prompt(props: PromptProps) {
const content = await Clipboard.read() const content = await Clipboard.read()
if (content?.mime.startsWith("image/")) { if (content?.mime.startsWith("image/")) {
e.preventDefault() e.preventDefault()
await pasteImage({ await pasteAttachment({
filename: "clipboard", filename: "clipboard",
mime: content.mime, mime: content.mime,
content: content.data, content: content.data,
@ -1012,9 +1018,16 @@ export function Prompt(props: PromptProps) {
return return
} }
// trim ' from the beginning and end of the pasted content. just const filepath = iife(() => {
// ' and nothing else const raw = pastedContent.replace(/^['"]+|['"]+$/g, "")
const filepath = pastedContent.replace(/^'+|'+$/g, "").replace(/\\ /g, " ") if (raw.startsWith("file://")) {
try {
return fileURLToPath(raw)
} catch {}
}
if (process.platform === "win32") return raw
return raw.replace(/\\(.)/g, "$1")
})
const isUrl = /^(https?):\/\//.test(filepath) const isUrl = /^(https?):\/\//.test(filepath)
if (!isUrl) { if (!isUrl) {
try { try {
@ -1029,14 +1042,15 @@ export function Prompt(props: PromptProps) {
return return
} }
} }
if (mime.startsWith("image/")) { if (mime.startsWith("image/") || mime === "application/pdf") {
event.preventDefault() event.preventDefault()
const content = await Filesystem.readArrayBuffer(filepath) const content = await Filesystem.readArrayBuffer(filepath)
.then((buffer) => Buffer.from(buffer).toString("base64")) .then((buffer) => Buffer.from(buffer).toString("base64"))
.catch(() => {}) .catch(() => {})
if (content) { if (content) {
await pasteImage({ await pasteAttachment({
filename, filename,
filepath,
mime, mime,
content, content,
}) })

View File

@ -55,7 +55,7 @@ const TIPS = [
"Use {highlight}/undo{/highlight} to revert the last message and file changes", "Use {highlight}/undo{/highlight} to revert the last message and file changes",
"Use {highlight}/redo{/highlight} to restore previously undone messages and file changes", "Use {highlight}/redo{/highlight} to restore previously undone messages and file changes",
"Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai", "Run {highlight}/share{/highlight} to create a public link to your conversation at opencode.ai",
"Drag and drop images into the terminal to add them as context", "Drag and drop images or PDFs into the terminal to add them as context",
"Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard into the prompt", "Press {highlight}Ctrl+V{/highlight} to paste images from your clipboard into the prompt",
"Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor", "Press {highlight}Ctrl+X E{/highlight} or {highlight}/editor{/highlight} to compose messages in your external editor",
"Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase", "Run {highlight}/init{/highlight} to auto-generate project rules based on your codebase",