feat: add native clipboard support for screenshot paste
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 59s
CI / Lint & Test (pull_request) Failing after 6m1s
CI / Build Linux (pull_request) Has been skipped
CI / Build Windows (cross-compile) (pull_request) Has been skipped

- Add Tauri clipboard-manager plugin for native clipboard access
- Fall back to native clipboard when WebView clipboard API returns empty
- Convert RGBA image data to PNG via canvas for saving
- Allow sending messages with attachments only (no text required)
- Log attached files to output with 📎 emoji
- Update send button to enable when attachments exist

Co-Authored-By: Naomi Carrigan <commits@nhcarrigan.com>
This commit is contained in:
2026-01-24 16:10:32 -08:00
parent a183a7ef47
commit 07f9cf8c30
7 changed files with 534 additions and 42 deletions
+1
View File
@@ -24,6 +24,7 @@
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-notification": "^2",
"@tauri-apps/plugin-opener": "^2",
+10
View File
@@ -11,6 +11,9 @@ importers:
'@tauri-apps/api':
specifier: ^2
version: 2.9.1
'@tauri-apps/plugin-clipboard-manager':
specifier: ^2.3.2
version: 2.3.2
'@tauri-apps/plugin-dialog':
specifier: ^2
version: 2.6.0
@@ -735,6 +738,9 @@ packages:
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-clipboard-manager@2.3.2':
resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==}
'@tauri-apps/plugin-dialog@2.6.0':
resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
@@ -2286,6 +2292,10 @@ snapshots:
'@tauri-apps/cli-win32-ia32-msvc': 2.9.6
'@tauri-apps/cli-win32-x64-msvc': 2.9.6
'@tauri-apps/plugin-clipboard-manager@2.3.2':
dependencies:
'@tauri-apps/api': 2.9.1
'@tauri-apps/plugin-dialog@2.6.0':
dependencies:
'@tauri-apps/api': 2.9.1
+350 -5
View File
@@ -47,6 +47,27 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arboard"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
[[package]]
name = "async-broadcast"
version = "0.7.2"
@@ -310,6 +331,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.11.0"
@@ -449,6 +476,15 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
[[package]]
name = "combine"
version = "4.6.7"
@@ -604,6 +640,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -807,6 +849,12 @@ dependencies = [
"litrs",
]
[[package]]
name = "downcast-rs"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "dpi"
version = "0.1.2"
@@ -926,6 +974,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "error-code"
version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]]
name = "event-listener"
version = "5.4.1"
@@ -953,6 +1007,26 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fax"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
dependencies = [
"fax_derive",
]
[[package]]
name = "fax_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
@@ -978,6 +1052,12 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.8"
@@ -994,6 +1074,12 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -1458,12 +1544,32 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -1496,7 +1602,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hikari-desktop"
version = "0.2.0"
version = "0.3.0"
dependencies = [
"chrono",
"parking_lot",
@@ -1505,6 +1611,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-clipboard-manager",
"tauri-plugin-dialog",
"tauri-plugin-http",
"tauri-plugin-notification",
@@ -1665,7 +1772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
dependencies = [
"byteorder",
"png",
"png 0.17.16",
]
[[package]]
@@ -1776,6 +1883,20 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png 0.18.0",
"tiff",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -2135,6 +2256,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moxcms"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "muda"
version = "0.17.1"
@@ -2150,7 +2281,7 @@ dependencies = [
"objc2-core-foundation",
"objc2-foundation",
"once_cell",
"png",
"png 0.17.16",
"serde",
"thiserror 2.0.17",
"windows-sys 0.60.2",
@@ -2210,6 +2341,15 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "notify-rust"
version = "4.11.7"
@@ -2628,6 +2768,17 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap 2.13.0",
]
[[package]]
name = "phf"
version = "0.8.0"
@@ -2817,6 +2968,19 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "png"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
dependencies = [
"bitflags 2.10.0",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "polling"
version = "3.11.0"
@@ -2945,6 +3109,21 @@ dependencies = [
"psl-types",
]
[[package]]
name = "pxfm"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8"
dependencies = [
"num-traits",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.37.5"
@@ -4068,7 +4247,7 @@ dependencies = [
"ico",
"json-patch",
"plist",
"png",
"png 0.17.16",
"proc-macro2",
"quote",
"semver",
@@ -4115,6 +4294,21 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206dc20af4ed210748ba945c2774e60fd0acd52b9a73a028402caf809e9b6ecf"
dependencies = [
"arboard",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.17",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.6.0"
@@ -4452,6 +4646,20 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "tiff"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg",
]
[[package]]
name = "time"
version = "0.3.45"
@@ -4747,12 +4955,23 @@ dependencies = [
"objc2-core-graphics",
"objc2-foundation",
"once_cell",
"png",
"png 0.17.16",
"serde",
"thiserror 2.0.17",
"windows-sys 0.60.2",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom",
"petgraph",
]
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -5034,6 +5253,76 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wayland-backend"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec"
dependencies = [
"bitflags 2.10.0",
"rustix",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baeda9ffbcfc8cd6ddaade385eaf2393bd2115a69523c735f12242353c3df4f3"
dependencies = [
"bitflags 2.10.0",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9597cdf02cf0c34cd5823786dce6b5ae8598f05c2daf5621b6e178d4f7345f3"
dependencies = [
"bitflags 2.10.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3"
dependencies = [
"proc-macro2",
"quick-xml 0.38.4",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd"
dependencies = [
"pkg-config",
]
[[package]]
name = "web-sys"
version = "0.3.85"
@@ -5143,6 +5432,12 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "weezl"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -5673,6 +5968,24 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wl-clipboard-rs"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix",
"thiserror 2.0.17",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
]
[[package]]
name = "writeable"
version = "0.6.2"
@@ -5745,6 +6058,23 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "yoke"
version = "0.8.1"
@@ -5915,6 +6245,21 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
dependencies = [
"zune-core",
]
[[package]]
name = "zvariant"
version = "5.9.1"
+1
View File
@@ -26,6 +26,7 @@ tauri-plugin-store = "2.4.2"
tauri-plugin-notification = "2"
tauri-plugin-os = "2"
tauri-plugin-http = "2"
tauri-plugin-clipboard-manager = "2"
tempfile = "3"
semver = "1"
chrono = { version = "0.4.43", features = ["serde"] }
+3 -1
View File
@@ -13,6 +13,8 @@
"notification:default",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:allow-notify"
"notification:allow-notify",
"clipboard-manager:default",
"clipboard-manager:allow-read-image"
]
}
+1
View File
@@ -33,6 +33,7 @@ pub fn run() {
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_clipboard_manager::init())
.manage(bridge_manager.clone())
.manage(temp_manager.clone())
.setup(move |app| {
+168 -36
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { open } from "@tauri-apps/plugin-dialog";
import { readImage } from "@tauri-apps/plugin-clipboard-manager";
import { get } from "svelte/store";
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
import { characterState } from "$lib/stores/character";
@@ -165,10 +166,13 @@
event.preventDefault();
const message = inputValue.trim();
if (!message || isSubmitting) return;
const hasAttachments = attachments.length > 0;
// Need either a message or attachments to submit
if ((!message && !hasAttachments) || isSubmitting) return;
// Check for slash commands first (these work even when disconnected)
if (isSlashCommand(message)) {
if (message && isSlashCommand(message)) {
// Add slash commands to history too
addToHistory(message);
historyIndex = -1;
@@ -189,8 +193,10 @@
// Regular messages require connection
if (!isConnected) return;
// Add to history before clearing
addToHistory(message);
// Add to history before clearing (only if there's text)
if (message) {
addToHistory(message);
}
historyIndex = -1;
tempInput = "";
userHasTyped = false;
@@ -198,12 +204,33 @@
isSubmitting = true;
inputValue = "";
// Apply mode prefix if needed
// Capture attachments before clearing
const currentAttachments = [...attachments];
// Apply mode prefix if needed (only if there's a message)
const currentMode = getCurrentMode();
const formattedMessage = formatMessageWithMode(message, currentMode);
const formattedMessage = message ? formatMessageWithMode(message, currentMode) : "";
// Build message with attachments
let messageWithAttachments = formattedMessage;
if (currentAttachments.length > 0) {
const attachmentPaths = currentAttachments.map((a) => a.path).join("\n");
const attachmentPrefix = formattedMessage
? `${formattedMessage}\n\n`
: "";
messageWithAttachments = `${attachmentPrefix}[Attached files - please read these files to see their contents:]\n${attachmentPaths}`;
// Log attached files to the output
for (const attachment of currentAttachments) {
claudeStore.addLine("system", `📎 Attached: ${attachment.filename} (${attachment.path})`);
}
}
// Clear attachments after capturing them
claudeStore.clearAttachments();
// Check if we need to restore conversation history
let messageToSend = formattedMessage;
let messageToSend = messageWithAttachments;
if (getShouldRestoreHistory()) {
const savedHistory = getSavedHistory();
@@ -341,7 +368,7 @@ User: ${formattedMessage}`;
}
}
function handleDrop(event: DragEvent) {
async function handleDrop(event: DragEvent) {
event.preventDefault();
event.stopPropagation();
isDragging = false;
@@ -349,6 +376,8 @@ User: ${formattedMessage}`;
const files = event.dataTransfer?.files;
if (!files || files.length === 0) return;
const conversationId = get(claudeStore.activeConversationId);
for (const file of files) {
const filename = file.name;
const extension = filename.split(".").pop()?.toLowerCase() || "";
@@ -361,10 +390,29 @@ User: ${formattedMessage}`;
previewUrl = URL.createObjectURL(file);
}
// Check if the file has a native path (from Tauri's file drop)
// If not, we need to save it to a temp file
let savedPath = (file as File & { path?: string }).path;
if (!savedPath && conversationId) {
try {
const arrayBuffer = await file.arrayBuffer();
const bytes = Array.from(new Uint8Array(arrayBuffer));
savedPath = await invoke<string>("save_temp_file", {
conversationId,
filename,
data: bytes,
});
} catch (error) {
console.error("Failed to save dropped file to temp:", error);
savedPath = file.name;
}
}
const attachment: Attachment = {
id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
filename,
path: (file as File & { path?: string }).path || file.name,
path: savedPath || file.name,
size: file.size,
type: fileType,
mimeType: file.type,
@@ -441,40 +489,124 @@ User: ${formattedMessage}`;
}
}
function handlePaste(event: ClipboardEvent) {
async function handlePaste(event: ClipboardEvent) {
// First, try the web clipboard API for files
const items = event.clipboardData?.items;
if (!items) return;
let handledFile = false;
for (const item of items) {
// Check if the item is a file (image or other)
if (item.kind === "file") {
const file = item.getAsFile();
if (!file) continue;
if (items && items.length > 0) {
for (const item of items) {
if (item.kind === "file") {
const file = item.getAsFile();
if (!file) continue;
// Prevent default for file pastes so we handle it
event.preventDefault();
event.preventDefault();
handledFile = true;
const filename = file.name || `pasted-${Date.now()}.${file.type.split("/")[1] || "png"}`;
const extension = filename.split(".").pop()?.toLowerCase() || "";
const fileType = getFileTypeFromExtension(extension);
const filename = file.name || `pasted-${Date.now()}.${file.type.split("/")[1] || "png"}`;
const extension = filename.split(".").pop()?.toLowerCase() || "";
const fileType = getFileTypeFromExtension(extension);
// Create preview URL for images
let previewUrl: string | undefined;
if (fileType === "image" || file.type.startsWith("image/")) {
previewUrl = URL.createObjectURL(file);
let previewUrl: string | undefined;
if (fileType === "image" || file.type.startsWith("image/")) {
previewUrl = URL.createObjectURL(file);
}
const conversationId = get(claudeStore.activeConversationId);
let savedPath = filename;
if (conversationId) {
try {
const arrayBuffer = await file.arrayBuffer();
const bytes = Array.from(new Uint8Array(arrayBuffer));
savedPath = await invoke<string>("save_temp_file", {
conversationId,
filename,
data: bytes,
});
} catch (error) {
console.error("Failed to save pasted file to temp:", error);
}
}
const attachment: Attachment = {
id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
filename,
path: savedPath,
size: file.size,
type: file.type.startsWith("image/") ? "image" : fileType,
mimeType: file.type,
previewUrl,
};
claudeStore.addAttachment(attachment);
}
}
}
const attachment: Attachment = {
id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
filename,
path: filename,
size: file.size,
type: file.type.startsWith("image/") ? "image" : fileType,
mimeType: file.type,
previewUrl,
};
// If web clipboard didn't have files, try Tauri's native clipboard for images
if (!handledFile) {
try {
const image = await readImage();
const rgba = await image.rgba();
const size = await image.size();
claudeStore.addAttachment(attachment);
if (rgba && rgba.length > 0) {
event.preventDefault();
const conversationId = get(claudeStore.activeConversationId);
const filename = `screenshot-${Date.now()}.png`;
// Convert RGBA to PNG using canvas
const canvas = document.createElement("canvas");
canvas.width = size.width;
canvas.height = size.height;
const ctx = canvas.getContext("2d");
if (ctx) {
const imgData = ctx.createImageData(size.width, size.height);
imgData.data.set(new Uint8ClampedArray(rgba));
ctx.putImageData(imgData, 0, 0);
// Create preview URL from canvas
const previewUrl = canvas.toDataURL("image/png");
// Convert to blob for saving
const blob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, "image/png")
);
let savedPath = filename;
if (blob && conversationId) {
try {
const arrayBuffer = await blob.arrayBuffer();
const bytes = Array.from(new Uint8Array(arrayBuffer));
savedPath = await invoke<string>("save_temp_file", {
conversationId,
filename,
data: bytes,
});
} catch (error) {
console.error("Failed to save clipboard image to temp:", error);
}
}
const attachment: Attachment = {
id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
filename,
path: savedPath,
size: blob?.size || 0,
type: "image",
mimeType: "image/png",
previewUrl,
};
claudeStore.addAttachment(attachment);
}
}
} catch (error) {
// No image in clipboard or clipboard read failed - that's fine, just ignore
console.log("No image in native clipboard:", error);
}
}
}
@@ -619,7 +751,7 @@ User: ${formattedMessage}`;
{:else}
<button
type="submit"
disabled={!isConnected || isSubmitting || !inputValue.trim()}
disabled={!isConnected || isSubmitting || (!inputValue.trim() && attachments.length === 0)}
class="send-button bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)]
disabled:opacity-50 disabled:cursor-not-allowed"
>