mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-09-04 10:11:46 +03:00
feat(tui): support pasting clipboard images (#5563)
* feat(tui): support pasting clipboard images * fix(tui): keep image placeholders atomic * fix(tui): reconcile duplicate image placeholders * fix(tui): retain highlighted image placeholders * fix(tui): preserve image placeholder layout * fix(tui): keep image display state local * fix(tui): reject images in commands
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
createHostClipboard,
|
||||
type HostClipboardService,
|
||||
} from "@opentui/core"
|
||||
|
||||
const IMAGE_MIME_TYPES = [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/webp",
|
||||
"image/gif",
|
||||
] as const
|
||||
const MAX_IMAGE_BYTES = 6 * 1024 * 1024
|
||||
|
||||
export interface ClipboardImage {
|
||||
dataUrl: string
|
||||
mimeType: typeof IMAGE_MIME_TYPES[number]
|
||||
}
|
||||
|
||||
export interface ClipboardImageReader {
|
||||
read(): Promise<ClipboardImage>
|
||||
dispose(): Promise<void>
|
||||
}
|
||||
|
||||
type ClipboardFactory = () => HostClipboardService
|
||||
|
||||
function readFailure(status: string): Error {
|
||||
if (status === "empty") return new Error("No image in clipboard")
|
||||
if (status === "limit-exceeded") return new Error("Clipboard image is larger than 6 MB")
|
||||
if (status === "timed-out") return new Error("Clipboard image read timed out")
|
||||
return new Error("Clipboard image paste is unavailable")
|
||||
}
|
||||
|
||||
/** Lazily owns OpenTUI's native host clipboard so ordinary TUI startup does no clipboard work. */
|
||||
export function createClipboardImageReader(
|
||||
createClipboard: ClipboardFactory = () => createHostClipboard({ maxReadBytes: MAX_IMAGE_BYTES }),
|
||||
): ClipboardImageReader {
|
||||
let clipboard: HostClipboardService | null = null
|
||||
let disposed = false
|
||||
|
||||
return {
|
||||
async read(): Promise<ClipboardImage> {
|
||||
if (disposed) throw new Error("Clipboard image paste is unavailable")
|
||||
clipboard ||= createClipboard()
|
||||
const result = await clipboard.read({ preferredTypes: IMAGE_MIME_TYPES })
|
||||
if (result.status !== "read") throw readFailure(result.status)
|
||||
const normalizedMime = result.representation.mimeType.toLowerCase()
|
||||
const mimeType = IMAGE_MIME_TYPES.find((candidate) => candidate === normalizedMime)
|
||||
if (!mimeType) throw new Error("Clipboard does not contain a supported image")
|
||||
const bytes = result.representation.bytes
|
||||
if (!bytes.length) throw new Error("Clipboard image is empty")
|
||||
if (bytes.length > MAX_IMAGE_BYTES) throw new Error("Clipboard image is larger than 6 MB")
|
||||
return {
|
||||
mimeType,
|
||||
dataUrl: `data:${mimeType};base64,${Buffer.from(bytes).toString("base64")}`,
|
||||
}
|
||||
},
|
||||
async dispose(): Promise<void> {
|
||||
if (disposed) return
|
||||
disposed = true
|
||||
await clipboard?.dispose()
|
||||
clipboard = null
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user