mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
tui: add image paste support from clipboard
Users can now paste images directly from their clipboard into the prompt using the paste command. The image will be automatically converted to base64 and embedded as a file attachment. This works on macOS by reading PNG data from the clipboard and converting it to the appropriate format for the AI to process.
This commit is contained in:
parent
bcd637e8e5
commit
49e52e0ca5
2 changed files with 76 additions and 3 deletions
|
|
@ -25,6 +25,8 @@ import { useCommandDialog } from "../dialog-command"
|
|||
import { useRenderer } from "@opentui/solid"
|
||||
import { Editor } from "@tui/util/editor"
|
||||
import { useExit } from "../../context/exit"
|
||||
import { Clipboard } from "../../util/clipboard"
|
||||
import type { FilePart } from "@opencode-ai/sdk"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
|
|
@ -139,6 +141,23 @@ export function Prompt(props: PromptProps) {
|
|||
dialog.clear()
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Paste",
|
||||
value: "prompt.paste",
|
||||
disabled: true,
|
||||
keybind: "input_paste",
|
||||
category: "Prompt",
|
||||
onSelect: async () => {
|
||||
const content = await Clipboard.read()
|
||||
if (content?.mime.startsWith("image/")) {
|
||||
await pasteImage({
|
||||
filename: "clipboard",
|
||||
mime: content.mime,
|
||||
content: content.data,
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
|
@ -384,6 +403,50 @@ export function Prompt(props: PromptProps) {
|
|||
}
|
||||
const exit = useExit()
|
||||
|
||||
async function pasteImage(file: {filename?: string, content: string, mime: string}) {
|
||||
const currentOffset = input.visualCursor.offset
|
||||
const extmarkStart = currentOffset
|
||||
const count = store.prompt.parts.filter((x) => x.type === "file").length
|
||||
const virtualText = `[Image ${count + 1}]`
|
||||
const extmarkEnd = extmarkStart + virtualText.length
|
||||
const textToInsert = virtualText + " "
|
||||
|
||||
input.insertText(textToInsert)
|
||||
|
||||
const extmarkId = input.extmarks.create({
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
virtual: true,
|
||||
styleId: pasteStyleId,
|
||||
typeId: promptPartTypeId,
|
||||
})
|
||||
|
||||
const part: Omit<FilePart, "id" | "messageID" | "sessionID"> = {
|
||||
type: "file" as const,
|
||||
mime: file.mime,
|
||||
filename: file.filename,
|
||||
url: `data:${file.mime};base64,${file.content}`,
|
||||
source: {
|
||||
type: "file",
|
||||
path: file.filename ?? "",
|
||||
text: {
|
||||
start: extmarkStart,
|
||||
end: extmarkEnd,
|
||||
value: virtualText,
|
||||
},
|
||||
},
|
||||
}
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
const partIndex = draft.prompt.parts.length
|
||||
draft.prompt.parts.push(part)
|
||||
draft.extmarkToPartIndex.set(extmarkId, partIndex)
|
||||
}),
|
||||
)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { $ } from "bun"
|
|||
import { platform } from "os"
|
||||
import clipboardy from "clipboardy"
|
||||
import { lazy } from "../../../../util/lazy.js"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
|
||||
export namespace Clipboard {
|
||||
export interface Content {
|
||||
|
|
@ -13,9 +15,17 @@ export namespace Clipboard {
|
|||
const os = platform()
|
||||
|
||||
if (os === "darwin") {
|
||||
const imageBuffer = await $`osascript -e 'try' -e 'the clipboard as «class PNGf»' -e 'end try'`.nothrow().text()
|
||||
if (imageBuffer) {
|
||||
return { data: Buffer.from(imageBuffer).toString("base64url"), mime: "image/png" }
|
||||
const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
|
||||
try {
|
||||
await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'`
|
||||
.nothrow()
|
||||
.quiet()
|
||||
const file = Bun.file(tmpfile)
|
||||
const buffer = await file.arrayBuffer()
|
||||
return { data: Buffer.from(buffer).toString("base64"), mime: "image/png" }
|
||||
} catch {
|
||||
} finally {
|
||||
await $`rm -f "${tmpfile}"`.nothrow().quiet()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue