fix(desktop): add native clipboard image paste and fix text paste (#12682)
This commit is contained in:
3
bun.lock
3
bun.lock
@@ -190,6 +190,7 @@
|
|||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
"@solidjs/meta": "catalog:",
|
"@solidjs/meta": "catalog:",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": "~2",
|
||||||
"@tauri-apps/plugin-deep-link": "~2",
|
"@tauri-apps/plugin-deep-link": "~2",
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
"@tauri-apps/plugin-http": "~2",
|
"@tauri-apps/plugin-http": "~2",
|
||||||
@@ -1784,6 +1785,8 @@
|
|||||||
|
|
||||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="],
|
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.9.6", "", { "os": "win32", "cpu": "x64" }, "sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.6", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA=="],
|
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.6", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-UUOSt0U5juK20uhO2MoHZX/IPblkrhUh+VPtIeu3RwtzI0R9Em3Auzfg/PwcZ9Pv8mLne3cQ4p9CFXD6WxqCZA=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
|
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { Persist, persisted } from "@/utils/persist"
|
|||||||
import { SessionContextUsage } from "@/components/session-context-usage"
|
import { SessionContextUsage } from "@/components/session-context-usage"
|
||||||
import { usePermission } from "@/context/permission"
|
import { usePermission } from "@/context/permission"
|
||||||
import { useLanguage } from "@/context/language"
|
import { useLanguage } from "@/context/language"
|
||||||
|
import { usePlatform } from "@/context/platform"
|
||||||
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
import { createTextFragment, getCursorPosition, setCursorPosition, setRangeEdge } from "./prompt-input/editor-dom"
|
||||||
import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
|
import { createPromptAttachments, ACCEPTED_FILE_TYPES } from "./prompt-input/attachments"
|
||||||
import { navigatePromptHistory, prependHistoryEntry, promptLength } from "./prompt-input/history"
|
import { navigatePromptHistory, prependHistoryEntry, promptLength } from "./prompt-input/history"
|
||||||
@@ -97,6 +98,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
const command = useCommand()
|
const command = useCommand()
|
||||||
const permission = usePermission()
|
const permission = usePermission()
|
||||||
const language = useLanguage()
|
const language = useLanguage()
|
||||||
|
const platform = usePlatform()
|
||||||
let editorRef!: HTMLDivElement
|
let editorRef!: HTMLDivElement
|
||||||
let fileInputRef!: HTMLInputElement
|
let fileInputRef!: HTMLInputElement
|
||||||
let scrollRef!: HTMLDivElement
|
let scrollRef!: HTMLDivElement
|
||||||
@@ -766,6 +768,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
setCursorPosition(editorRef, promptLength(prompt.current()))
|
setCursorPosition(editorRef, promptLength(prompt.current()))
|
||||||
},
|
},
|
||||||
addPart,
|
addPart,
|
||||||
|
readClipboardImage: platform.readClipboardImage,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { abort, handleSubmit } = createPromptSubmit({
|
const { abort, handleSubmit } = createPromptSubmit({
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type PromptAttachmentsInput = {
|
|||||||
setDraggingType: (type: "image" | "@mention" | null) => void
|
setDraggingType: (type: "image" | "@mention" | null) => void
|
||||||
focusEditor: () => void
|
focusEditor: () => void
|
||||||
addPart: (part: ContentPart) => void
|
addPart: (part: ContentPart) => void
|
||||||
|
readClipboardImage?: () => Promise<File | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPromptAttachments(input: PromptAttachmentsInput) {
|
export function createPromptAttachments(input: PromptAttachmentsInput) {
|
||||||
@@ -76,6 +77,16 @@ export function createPromptAttachments(input: PromptAttachmentsInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const plainText = clipboardData.getData("text/plain") ?? ""
|
const plainText = clipboardData.getData("text/plain") ?? ""
|
||||||
|
|
||||||
|
// Desktop: Browser clipboard has no images and no text, try platform's native clipboard for images
|
||||||
|
if (input.readClipboardImage && !plainText) {
|
||||||
|
const file = await input.readClipboardImage()
|
||||||
|
if (file) {
|
||||||
|
await addImageAttachment(file)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!plainText) return
|
if (!plainText) return
|
||||||
input.addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
input.addPart({ type: "text", content: plainText, start: 0, end: 0 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ export type Platform = {
|
|||||||
|
|
||||||
/** Check if an editor app exists (desktop only) */
|
/** Check if an editor app exists (desktop only) */
|
||||||
checkAppExists?(appName: string): Promise<boolean>
|
checkAppExists?(appName: string): Promise<boolean>
|
||||||
|
|
||||||
|
/** Read image from clipboard (desktop only) */
|
||||||
|
readClipboardImage?(): Promise<File | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
export const { use: usePlatform, provider: PlatformProvider } = createSimpleContext({
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@solid-primitives/i18n": "2.2.1",
|
"@solid-primitives/i18n": "2.2.1",
|
||||||
"@solid-primitives/storage": "catalog:",
|
"@solid-primitives/storage": "catalog:",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": "~2",
|
||||||
"@tauri-apps/plugin-deep-link": "~2",
|
"@tauri-apps/plugin-deep-link": "~2",
|
||||||
"@tauri-apps/plugin-dialog": "~2",
|
"@tauri-apps/plugin-dialog": "~2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
{
|
{
|
||||||
"identifier": "http:default",
|
"identifier": "http:default",
|
||||||
"allow": [{ "url": "http://*" }, { "url": "https://*" }, { "url": "http://*:*/*" }]
|
"allow": [{ "url": "http://*" }, { "url": "https://*" }, { "url": "http://*:*/*" }]
|
||||||
}
|
},
|
||||||
|
"clipboard-manager:allow-read-image"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { fetch as tauriFetch } from "@tauri-apps/plugin-http"
|
|||||||
import { Store } from "@tauri-apps/plugin-store"
|
import { Store } from "@tauri-apps/plugin-store"
|
||||||
import { Splash } from "@opencode-ai/ui/logo"
|
import { Splash } from "@opencode-ai/ui/logo"
|
||||||
import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup } from "solid-js"
|
import { createSignal, Show, Accessor, JSX, createResource, onMount, onCleanup } from "solid-js"
|
||||||
|
import { readImage } from "@tauri-apps/plugin-clipboard-manager"
|
||||||
|
|
||||||
import { UPDATER_ENABLED } from "./updater"
|
import { UPDATER_ENABLED } from "./updater"
|
||||||
import { initI18n, t } from "./i18n"
|
import { initI18n, t } from "./i18n"
|
||||||
@@ -344,6 +345,29 @@ const createPlatform = (password: Accessor<string | null>): Platform => ({
|
|||||||
checkAppExists: async (appName: string) => {
|
checkAppExists: async (appName: string) => {
|
||||||
return commands.checkAppExists(appName)
|
return commands.checkAppExists(appName)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async readClipboardImage() {
|
||||||
|
const image = await readImage().catch(() => null)
|
||||||
|
if (!image) return null
|
||||||
|
const bytes = await image.rgba().catch(() => null)
|
||||||
|
if (!bytes || bytes.length === 0) return null
|
||||||
|
const size = await image.size().catch(() => null)
|
||||||
|
if (!size) return null
|
||||||
|
const canvas = document.createElement("canvas")
|
||||||
|
canvas.width = size.width
|
||||||
|
canvas.height = size.height
|
||||||
|
const ctx = canvas.getContext("2d")
|
||||||
|
if (!ctx) return null
|
||||||
|
const imageData = ctx.createImageData(size.width, size.height)
|
||||||
|
imageData.data.set(bytes)
|
||||||
|
ctx.putImageData(imageData, 0, 0)
|
||||||
|
return new Promise<File | null>((resolve) => {
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (!blob) return resolve(null)
|
||||||
|
resolve(new File([blob], `pasted-image-${Date.now()}.png`, { type: "image/png" }))
|
||||||
|
}, "image/png")
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let menuTrigger = null as null | ((id: string) => void)
|
let menuTrigger = null as null | ((id: string) => void)
|
||||||
|
|||||||
Reference in New Issue
Block a user