generated from nhcarrigan/template
feat: add native clipboard support for screenshot paste (#67)
## Summary - Adds Tauri clipboard-manager plugin to read images from native clipboard - Falls back to native clipboard when WebView clipboard API returns empty (fixes screenshot paste) - Allows sending messages with just attachments (no text required) - Logs attached files to output with 📎 emoji ## Test plan - [ ] Build and run the app natively on Windows - [ ] Copy a screenshot (Win+Shift+S) and paste in the chat input - [ ] Verify the screenshot appears as an attachment preview - [ ] Send the attachment and verify Claude receives the file path - [ ] Test sending a message with only an attachment (no text) - [ ] Verify the 📎 log line shows the attached filename **Note:** Paste will not work in WSLg dev environment due to clipboard isolation - needs native Windows build to test. ✨ This PR was created with help from Hikari~ 🌸 Co-authored-by: Hikari <hikari@nhcarrigan.com> Reviewed-on: #67 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #67.
This commit is contained in:
@@ -0,0 +1,74 @@
|
|||||||
|
# Hikari Desktop v0.3.0 Release Notes
|
||||||
|
|
||||||
|
## New Features
|
||||||
|
|
||||||
|
### AskUserQuestion Tool Support (#51)
|
||||||
|
|
||||||
|
- Claude can now ask you questions with multiple choice options during conversations
|
||||||
|
- A dedicated modal appears with answer choices and support for custom responses
|
||||||
|
- Answers are seamlessly integrated back into the conversation context
|
||||||
|
|
||||||
|
### Slash Commands
|
||||||
|
|
||||||
|
- `/cd <path>` - Change the working directory while preserving conversation context (#55)
|
||||||
|
- `/search <query>` - Search and highlight matches within the conversation (#32)
|
||||||
|
- `/skill <name> [data]` - Invoke Claude Code skills from `~/.claude/skills/` (#57)
|
||||||
|
- `/new`, `/clear`, `/rename`, `/help` commands for conversation management (#6)
|
||||||
|
|
||||||
|
### Auto-Update Checker (#17)
|
||||||
|
|
||||||
|
- Automatically checks for new releases on startup
|
||||||
|
- Notification appears when a newer version is available
|
||||||
|
- Can be disabled in settings
|
||||||
|
|
||||||
|
### Font Size & Zoom (#19)
|
||||||
|
|
||||||
|
- Adjust font size with keyboard shortcuts: `Ctrl++`, `Ctrl+-`, `Ctrl+0`
|
||||||
|
- Font size slider in settings (10-24px range)
|
||||||
|
- Preference persists between sessions
|
||||||
|
|
||||||
|
### Resizable Character Panel (#10)
|
||||||
|
|
||||||
|
- Drag the divider between the character panel and terminal to resize
|
||||||
|
- Panel width is saved and restored on app restart
|
||||||
|
- Sprite now uses full height for better proportions
|
||||||
|
|
||||||
|
### Input History Navigation (#13)
|
||||||
|
|
||||||
|
- Use up/down arrows to navigate through previous messages and commands
|
||||||
|
- Arrow keys only navigate history when input is empty - otherwise they move the cursor (#58)
|
||||||
|
|
||||||
|
### Keyboard Shortcuts (#21)
|
||||||
|
|
||||||
|
- `Ctrl+N` - New conversation
|
||||||
|
- `Ctrl+W` - Close current tab
|
||||||
|
- `Ctrl+Tab` / `Ctrl+Shift+Tab` - Switch between tabs
|
||||||
|
- `Ctrl+L` - Clear conversation
|
||||||
|
- `Ctrl+,` - Open settings
|
||||||
|
|
||||||
|
### Always On Top Toggle (#28)
|
||||||
|
|
||||||
|
- Pin the window to stay above other applications
|
||||||
|
- Toggle in settings
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
|
||||||
|
### UI/UX
|
||||||
|
|
||||||
|
- Resizable chat input with drag handle (expands upward)
|
||||||
|
- Send button properly aligned with input field
|
||||||
|
- Markdown rendering with syntax-highlighted code blocks (#31, #33)
|
||||||
|
- Light mode text colors improved for better readability
|
||||||
|
- Scroll position persists per conversation tab when switching
|
||||||
|
- Confirmation modal when closing connected tabs
|
||||||
|
- Links in chat now open in default browser (#54)
|
||||||
|
- Spaces allowed when renaming tabs (#52)
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
- Stats (tokens, cost) persist across session changes and only reset on disconnect (#59)
|
||||||
|
- Multiple tabs can now request permissions simultaneously without conflicts
|
||||||
|
|
||||||
|
## Closed Issues
|
||||||
|
|
||||||
|
#6, #10, #13, #17, #19, #21, #28, #31, #32, #33, #51, #52, #54, #55, #57, #58, #59
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"@tauri-apps/plugin-notification": "^2",
|
"@tauri-apps/plugin-notification": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2",
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
|||||||
Generated
+10
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.9.1
|
version: 2.9.1
|
||||||
|
'@tauri-apps/plugin-clipboard-manager':
|
||||||
|
specifier: ^2.3.2
|
||||||
|
version: 2.3.2
|
||||||
'@tauri-apps/plugin-dialog':
|
'@tauri-apps/plugin-dialog':
|
||||||
specifier: ^2
|
specifier: ^2
|
||||||
version: 2.6.0
|
version: 2.6.0
|
||||||
@@ -735,6 +738,9 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-clipboard-manager@2.3.2':
|
||||||
|
resolution: {integrity: sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-dialog@2.6.0':
|
'@tauri-apps/plugin-dialog@2.6.0':
|
||||||
resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
|
resolution: {integrity: sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==}
|
||||||
|
|
||||||
@@ -2286,6 +2292,10 @@ snapshots:
|
|||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.9.6
|
'@tauri-apps/cli-win32-ia32-msvc': 2.9.6
|
||||||
'@tauri-apps/cli-win32-x64-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':
|
'@tauri-apps/plugin-dialog@2.6.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.9.1
|
'@tauri-apps/api': 2.9.1
|
||||||
|
|||||||
Generated
+350
-5
@@ -47,6 +47,27 @@ version = "1.0.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
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]]
|
[[package]]
|
||||||
name = "async-broadcast"
|
name = "async-broadcast"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -310,6 +331,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder-lite"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
@@ -449,6 +476,15 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"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]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -604,6 +640,12 @@ version = "0.8.21"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -807,6 +849,12 @@ dependencies = [
|
|||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downcast-rs"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -926,6 +974,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "event-listener"
|
name = "event-listener"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@@ -953,6 +1007,26 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
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]]
|
[[package]]
|
||||||
name = "fdeflate"
|
name = "fdeflate"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
@@ -978,6 +1052,12 @@ version = "0.1.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
|
checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fixedbitset"
|
||||||
|
version = "0.5.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.8"
|
version = "1.1.8"
|
||||||
@@ -994,6 +1074,12 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1458,12 +1544,32 @@ dependencies = [
|
|||||||
"tracing",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.15.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@@ -1496,7 +1602,7 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hikari-desktop"
|
name = "hikari-desktop"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@@ -1505,6 +1611,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-clipboard-manager",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
@@ -1665,7 +1772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1776,6 +1883,20 @@ dependencies = [
|
|||||||
"icu_properties",
|
"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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "1.9.3"
|
version = "1.9.3"
|
||||||
@@ -2135,6 +2256,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -2150,7 +2281,7 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
@@ -2210,6 +2341,15 @@ version = "0.1.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify-rust"
|
name = "notify-rust"
|
||||||
version = "4.11.7"
|
version = "4.11.7"
|
||||||
@@ -2628,6 +2768,17 @@ version = "2.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
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]]
|
[[package]]
|
||||||
name = "phf"
|
name = "phf"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -2817,6 +2968,19 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"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]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.11.0"
|
version = "3.11.0"
|
||||||
@@ -2945,6 +3109,21 @@ dependencies = [
|
|||||||
"psl-types",
|
"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]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.37.5"
|
version = "0.37.5"
|
||||||
@@ -4068,7 +4247,7 @@ dependencies = [
|
|||||||
"ico",
|
"ico",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
"plist",
|
"plist",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"semver",
|
"semver",
|
||||||
@@ -4115,6 +4294,21 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
@@ -4452,6 +4646,20 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"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]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.45"
|
version = "0.3.45"
|
||||||
@@ -4747,12 +4955,23 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png",
|
"png 0.17.16",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"windows-sys 0.60.2",
|
"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]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -5034,6 +5253,76 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.85"
|
version = "0.3.85"
|
||||||
@@ -5143,6 +5432,12 @@ dependencies = [
|
|||||||
"windows-core 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "winapi"
|
name = "winapi"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -5673,6 +5968,24 @@ version = "0.46.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
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]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -5745,6 +6058,23 @@ dependencies = [
|
|||||||
"pkg-config",
|
"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]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -5915,6 +6245,21 @@ version = "1.0.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea"
|
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]]
|
[[package]]
|
||||||
name = "zvariant"
|
name = "zvariant"
|
||||||
version = "5.9.1"
|
version = "5.9.1"
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ tauri-plugin-store = "2.4.2"
|
|||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
tauri-plugin-os = "2"
|
tauri-plugin-os = "2"
|
||||||
tauri-plugin-http = "2"
|
tauri-plugin-http = "2"
|
||||||
|
tauri-plugin-clipboard-manager = "2"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
semver = "1"
|
semver = "1"
|
||||||
chrono = { version = "0.4.43", features = ["serde"] }
|
chrono = { version = "0.4.43", features = ["serde"] }
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
"notification:default",
|
"notification:default",
|
||||||
"notification:allow-is-permission-granted",
|
"notification:allow-is-permission-granted",
|
||||||
"notification:allow-request-permission",
|
"notification:allow-request-permission",
|
||||||
"notification:allow-notify"
|
"notification:allow-notify",
|
||||||
|
"clipboard-manager:default",
|
||||||
|
"clipboard-manager:allow-read-image"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
use tauri::{AppHandle, State};
|
use tauri::{AppHandle, State};
|
||||||
use tauri_plugin_http::reqwest;
|
use tauri_plugin_http::reqwest;
|
||||||
use tauri_plugin_store::StoreExt;
|
use tauri_plugin_store::StoreExt;
|
||||||
@@ -6,6 +7,7 @@ use crate::achievements::{get_achievement_info, load_achievements, AchievementUn
|
|||||||
use crate::bridge_manager::SharedBridgeManager;
|
use crate::bridge_manager::SharedBridgeManager;
|
||||||
use crate::config::{ClaudeStartOptions, HikariConfig};
|
use crate::config::{ClaudeStartOptions, HikariConfig};
|
||||||
use crate::stats::UsageStats;
|
use crate::stats::UsageStats;
|
||||||
|
use crate::temp_manager::SharedTempFileManager;
|
||||||
|
|
||||||
const CONFIG_STORE_KEY: &str = "config";
|
const CONFIG_STORE_KEY: &str = "config";
|
||||||
|
|
||||||
@@ -298,3 +300,83 @@ pub async fn check_for_updates() -> Result<UpdateInfo, String> {
|
|||||||
release_notes: latest.body.clone(),
|
release_notes: latest.body.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize)]
|
||||||
|
pub struct SavedFileInfo {
|
||||||
|
pub path: String,
|
||||||
|
pub filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn save_temp_file(
|
||||||
|
temp_manager: State<'_, SharedTempFileManager>,
|
||||||
|
conversation_id: String,
|
||||||
|
data: Vec<u8>,
|
||||||
|
filename: Option<String>,
|
||||||
|
) -> Result<SavedFileInfo, String> {
|
||||||
|
let mut manager = temp_manager.lock();
|
||||||
|
let path = manager.save_file(&conversation_id, &data, filename.as_deref())?;
|
||||||
|
|
||||||
|
let filename = path
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
|
Ok(SavedFileInfo {
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
filename,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn register_temp_file(
|
||||||
|
temp_manager: State<'_, SharedTempFileManager>,
|
||||||
|
conversation_id: String,
|
||||||
|
file_path: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut manager = temp_manager.lock();
|
||||||
|
manager.register_file(&conversation_id, PathBuf::from(file_path));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_temp_files(
|
||||||
|
temp_manager: State<'_, SharedTempFileManager>,
|
||||||
|
conversation_id: String,
|
||||||
|
) -> Result<Vec<String>, String> {
|
||||||
|
let manager = temp_manager.lock();
|
||||||
|
let files = manager.get_files_for_conversation(&conversation_id);
|
||||||
|
Ok(files.iter().map(|p| p.to_string_lossy().to_string()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cleanup_temp_files(
|
||||||
|
temp_manager: State<'_, SharedTempFileManager>,
|
||||||
|
conversation_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut manager = temp_manager.lock();
|
||||||
|
manager.cleanup_conversation(&conversation_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cleanup_all_temp_files(
|
||||||
|
temp_manager: State<'_, SharedTempFileManager>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut manager = temp_manager.lock();
|
||||||
|
manager.cleanup_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn cleanup_orphaned_temp_files(
|
||||||
|
temp_manager: State<'_, SharedTempFileManager>,
|
||||||
|
) -> Result<usize, String> {
|
||||||
|
let mut manager = temp_manager.lock();
|
||||||
|
manager.cleanup_orphaned_files()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_file_size(file_path: String) -> Result<u64, String> {
|
||||||
|
let metadata = std::fs::metadata(&file_path)
|
||||||
|
.map_err(|e| format!("Failed to get file metadata: {}", e))?;
|
||||||
|
Ok(metadata.len())
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod commands;
|
|||||||
mod config;
|
mod config;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
mod stats;
|
mod stats;
|
||||||
|
mod temp_manager;
|
||||||
mod types;
|
mod types;
|
||||||
mod vbs_notification;
|
mod vbs_notification;
|
||||||
mod windows_toast;
|
mod windows_toast;
|
||||||
@@ -14,6 +15,7 @@ use bridge_manager::create_shared_bridge_manager;
|
|||||||
use commands::load_saved_achievements;
|
use commands::load_saved_achievements;
|
||||||
use commands::*;
|
use commands::*;
|
||||||
use notifications::*;
|
use notifications::*;
|
||||||
|
use temp_manager::create_shared_temp_manager;
|
||||||
use vbs_notification::*;
|
use vbs_notification::*;
|
||||||
use windows_toast::*;
|
use windows_toast::*;
|
||||||
use wsl_notifications::*;
|
use wsl_notifications::*;
|
||||||
@@ -21,6 +23,7 @@ use wsl_notifications::*;
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let bridge_manager = create_shared_bridge_manager();
|
let bridge_manager = create_shared_bridge_manager();
|
||||||
|
let temp_manager = create_shared_temp_manager().expect("Failed to create temp file manager");
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
@@ -30,10 +33,20 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_notification::init())
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
.manage(bridge_manager.clone())
|
.manage(bridge_manager.clone())
|
||||||
|
.manage(temp_manager.clone())
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
// Initialize the app handle in the bridge manager
|
// Initialize the app handle in the bridge manager
|
||||||
bridge_manager.lock().set_app_handle(app.handle().clone());
|
bridge_manager.lock().set_app_handle(app.handle().clone());
|
||||||
|
|
||||||
|
// Clean up any orphaned temp files from previous sessions
|
||||||
|
if let Ok(count) = temp_manager.lock().cleanup_orphaned_files() {
|
||||||
|
if count > 0 {
|
||||||
|
println!("Cleaned up {} orphaned temp files", count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
@@ -58,6 +71,13 @@ pub fn run() {
|
|||||||
validate_directory,
|
validate_directory,
|
||||||
list_skills,
|
list_skills,
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
|
save_temp_file,
|
||||||
|
register_temp_file,
|
||||||
|
get_temp_files,
|
||||||
|
cleanup_temp_files,
|
||||||
|
cleanup_all_temp_files,
|
||||||
|
cleanup_orphaned_temp_files,
|
||||||
|
get_file_size,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const TEMP_DIR_NAME: &str = "hikari-uploads";
|
||||||
|
|
||||||
|
pub struct TempFileManager {
|
||||||
|
base_dir: PathBuf,
|
||||||
|
files: HashMap<String, Vec<PathBuf>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempFileManager {
|
||||||
|
pub fn new() -> Result<Self, String> {
|
||||||
|
let base_dir = std::env::temp_dir().join(TEMP_DIR_NAME);
|
||||||
|
|
||||||
|
if !base_dir.exists() {
|
||||||
|
fs::create_dir_all(&base_dir)
|
||||||
|
.map_err(|e| format!("Failed to create temp directory: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(TempFileManager {
|
||||||
|
base_dir,
|
||||||
|
files: HashMap::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn get_base_dir(&self) -> &Path {
|
||||||
|
&self.base_dir
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_file(
|
||||||
|
&mut self,
|
||||||
|
conversation_id: &str,
|
||||||
|
data: &[u8],
|
||||||
|
original_filename: Option<&str>,
|
||||||
|
) -> Result<PathBuf, String> {
|
||||||
|
let unique_id = Uuid::new_v4();
|
||||||
|
let extension = original_filename
|
||||||
|
.and_then(|name| Path::new(name).extension())
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.unwrap_or("bin");
|
||||||
|
|
||||||
|
let filename = format!("{}_{}.{}", conversation_id, unique_id, extension);
|
||||||
|
let file_path = self.base_dir.join(&filename);
|
||||||
|
|
||||||
|
fs::write(&file_path, data)
|
||||||
|
.map_err(|e| format!("Failed to write temp file: {}", e))?;
|
||||||
|
|
||||||
|
self.files
|
||||||
|
.entry(conversation_id.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(file_path.clone());
|
||||||
|
|
||||||
|
Ok(file_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_file(&mut self, conversation_id: &str, file_path: PathBuf) {
|
||||||
|
self.files
|
||||||
|
.entry(conversation_id.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_files_for_conversation(&self, conversation_id: &str) -> Vec<PathBuf> {
|
||||||
|
self.files
|
||||||
|
.get(conversation_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_conversation(&mut self, conversation_id: &str) -> Result<(), String> {
|
||||||
|
if let Some(files) = self.files.remove(conversation_id) {
|
||||||
|
for file_path in files {
|
||||||
|
if file_path.exists() {
|
||||||
|
if let Err(e) = fs::remove_file(&file_path) {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: Failed to remove temp file {:?}: {}",
|
||||||
|
file_path, e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_all(&mut self) -> Result<(), String> {
|
||||||
|
let conversation_ids: Vec<String> = self.files.keys().cloned().collect();
|
||||||
|
|
||||||
|
for conversation_id in conversation_ids {
|
||||||
|
self.cleanup_conversation(&conversation_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanup_orphaned_files(&mut self) -> Result<usize, String> {
|
||||||
|
let mut cleaned_count = 0;
|
||||||
|
|
||||||
|
if !self.base_dir.exists() {
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tracked_files: std::collections::HashSet<PathBuf> =
|
||||||
|
self.files.values().flatten().cloned().collect();
|
||||||
|
|
||||||
|
let entries = fs::read_dir(&self.base_dir)
|
||||||
|
.map_err(|e| format!("Failed to read temp directory: {}", e))?;
|
||||||
|
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file() && !tracked_files.contains(&path) {
|
||||||
|
if let Err(e) = fs::remove_file(&path) {
|
||||||
|
eprintln!("Warning: Failed to remove orphaned file {:?}: {}", path, e);
|
||||||
|
} else {
|
||||||
|
cleaned_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cleaned_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TempFileManager {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new().expect("Failed to create TempFileManager")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type SharedTempFileManager = Arc<Mutex<TempFileManager>>;
|
||||||
|
|
||||||
|
pub fn create_shared_temp_manager() -> Result<SharedTempFileManager, String> {
|
||||||
|
Ok(Arc::new(Mutex::new(TempFileManager::new()?)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Attachment } from "$lib/types/messages";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
attachments: Attachment[];
|
||||||
|
onRemove: (id: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { attachments, onRemove }: Props = $props();
|
||||||
|
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return `${bytes} B`;
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileIcon(type: Attachment["type"]): string {
|
||||||
|
switch (type) {
|
||||||
|
case "image":
|
||||||
|
return "🖼️";
|
||||||
|
case "document":
|
||||||
|
return "📄";
|
||||||
|
default:
|
||||||
|
return "📎";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if attachments.length > 0}
|
||||||
|
<div class="attachment-preview-container">
|
||||||
|
<div class="attachment-header">
|
||||||
|
<span class="attachment-count"
|
||||||
|
>{attachments.length} attachment{attachments.length !== 1 ? "s" : ""}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="attachment-list">
|
||||||
|
{#each attachments as attachment (attachment.id)}
|
||||||
|
<div class="attachment-item" class:is-image={attachment.type === "image"}>
|
||||||
|
{#if attachment.type === "image" && attachment.previewUrl}
|
||||||
|
<div class="image-preview">
|
||||||
|
<img src={attachment.previewUrl} alt={attachment.filename} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="file-icon">
|
||||||
|
{getFileIcon(attachment.type)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="attachment-info">
|
||||||
|
<span class="attachment-filename" title={attachment.filename}>
|
||||||
|
{attachment.filename}
|
||||||
|
</span>
|
||||||
|
<span class="attachment-size">
|
||||||
|
{formatFileSize(attachment.size)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="remove-button"
|
||||||
|
onclick={() => onRemove(attachment.id)}
|
||||||
|
title="Remove attachment"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.attachment-preview-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-count {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
max-width: 200px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item.is-image {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 4px;
|
||||||
|
max-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 110px;
|
||||||
|
max-height: 80px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 80px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-image .attachment-info {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-filename {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-size {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
opacity 0.2s,
|
||||||
|
background 0.2s,
|
||||||
|
color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-item:hover .remove-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button:hover {
|
||||||
|
background: var(--error-color, #ef4444);
|
||||||
|
border-color: var(--error-color, #ef4444);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
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 { get } from "svelte/store";
|
||||||
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
import { claudeStore, isClaudeProcessing } from "$lib/stores/claude";
|
||||||
import { characterState } from "$lib/stores/character";
|
import { characterState } from "$lib/stores/character";
|
||||||
@@ -22,6 +24,8 @@
|
|||||||
isSlashCommand,
|
isSlashCommand,
|
||||||
type SlashCommand,
|
type SlashCommand,
|
||||||
} from "$lib/commands/slashCommands";
|
} from "$lib/commands/slashCommands";
|
||||||
|
import AttachmentPreview from "$lib/components/AttachmentPreview.svelte";
|
||||||
|
import type { Attachment } from "$lib/types/messages";
|
||||||
|
|
||||||
const INPUT_HISTORY_KEY = "hikari-input-history";
|
const INPUT_HISTORY_KEY = "hikari-input-history";
|
||||||
const MAX_HISTORY_SIZE = 100;
|
const MAX_HISTORY_SIZE = 100;
|
||||||
@@ -33,6 +37,8 @@
|
|||||||
let showCommandMenu = $state(false);
|
let showCommandMenu = $state(false);
|
||||||
let matchingCommands = $state<SlashCommand[]>([]);
|
let matchingCommands = $state<SlashCommand[]>([]);
|
||||||
let selectedCommandIndex = $state(0);
|
let selectedCommandIndex = $state(0);
|
||||||
|
let attachments = $state<Attachment[]>([]);
|
||||||
|
let isDragging = $state(false);
|
||||||
|
|
||||||
// Input history state
|
// Input history state
|
||||||
let inputHistory = $state<string[]>([]);
|
let inputHistory = $state<string[]>([]);
|
||||||
@@ -112,6 +118,10 @@
|
|||||||
isProcessing = processing;
|
isProcessing = processing;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
claudeStore.attachments.subscribe((storedAttachments) => {
|
||||||
|
attachments = storedAttachments;
|
||||||
|
});
|
||||||
|
|
||||||
function handleInputChange() {
|
function handleInputChange() {
|
||||||
// If input is empty, allow history navigation again
|
// If input is empty, allow history navigation again
|
||||||
// Otherwise, mark that user has manually typed
|
// Otherwise, mark that user has manually typed
|
||||||
@@ -156,10 +166,13 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const message = inputValue.trim();
|
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)
|
// Check for slash commands first (these work even when disconnected)
|
||||||
if (isSlashCommand(message)) {
|
if (message && isSlashCommand(message)) {
|
||||||
// Add slash commands to history too
|
// Add slash commands to history too
|
||||||
addToHistory(message);
|
addToHistory(message);
|
||||||
historyIndex = -1;
|
historyIndex = -1;
|
||||||
@@ -180,8 +193,10 @@
|
|||||||
// Regular messages require connection
|
// Regular messages require connection
|
||||||
if (!isConnected) return;
|
if (!isConnected) return;
|
||||||
|
|
||||||
// Add to history before clearing
|
// Add to history before clearing (only if there's text)
|
||||||
addToHistory(message);
|
if (message) {
|
||||||
|
addToHistory(message);
|
||||||
|
}
|
||||||
historyIndex = -1;
|
historyIndex = -1;
|
||||||
tempInput = "";
|
tempInput = "";
|
||||||
userHasTyped = false;
|
userHasTyped = false;
|
||||||
@@ -189,12 +204,31 @@
|
|||||||
isSubmitting = true;
|
isSubmitting = true;
|
||||||
inputValue = "";
|
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 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
|
// Check if we need to restore conversation history
|
||||||
let messageToSend = formattedMessage;
|
let messageToSend = messageWithAttachments;
|
||||||
if (getShouldRestoreHistory()) {
|
if (getShouldRestoreHistory()) {
|
||||||
const savedHistory = getSavedHistory();
|
const savedHistory = getSavedHistory();
|
||||||
|
|
||||||
@@ -289,6 +323,300 @@ User: ${formattedMessage}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleRemoveAttachment(id: string) {
|
||||||
|
claudeStore.removeAttachment(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileTypeFromExtension(extension: string): "image" | "document" | "other" {
|
||||||
|
const imageExtensions = ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"];
|
||||||
|
const documentExtensions = ["pdf", "txt", "md", "doc", "docx", "csv", "json", "xml"];
|
||||||
|
|
||||||
|
if (imageExtensions.includes(extension)) {
|
||||||
|
return "image";
|
||||||
|
} else if (documentExtensions.includes(extension)) {
|
||||||
|
return "document";
|
||||||
|
}
|
||||||
|
return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnter(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.dataTransfer?.types.includes("Files")) {
|
||||||
|
isDragging = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (event.dataTransfer) {
|
||||||
|
event.dataTransfer.dropEffect = "copy";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
// Only set isDragging to false if we're leaving the form element entirely
|
||||||
|
const relatedTarget = event.relatedTarget as Node | null;
|
||||||
|
const currentTarget = event.currentTarget as HTMLElement;
|
||||||
|
if (!relatedTarget || !currentTarget.contains(relatedTarget)) {
|
||||||
|
isDragging = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDrop(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
isDragging = false;
|
||||||
|
|
||||||
|
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() || "";
|
||||||
|
const fileType = getFileTypeFromExtension(extension);
|
||||||
|
|
||||||
|
// Create attachment from dropped file
|
||||||
|
// Note: For dropped files, we create a preview URL for images
|
||||||
|
let previewUrl: string | undefined;
|
||||||
|
if (fileType === "image") {
|
||||||
|
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: savedPath || file.name,
|
||||||
|
size: file.size,
|
||||||
|
type: fileType,
|
||||||
|
mimeType: file.type,
|
||||||
|
previewUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
claudeStore.addAttachment(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFilePicker() {
|
||||||
|
try {
|
||||||
|
const selected = await open({
|
||||||
|
multiple: true,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: "All Files",
|
||||||
|
extensions: ["*"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Images",
|
||||||
|
extensions: ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Documents",
|
||||||
|
extensions: ["pdf", "txt", "md", "doc", "docx", "csv", "json", "xml"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Code",
|
||||||
|
extensions: [
|
||||||
|
"js",
|
||||||
|
"ts",
|
||||||
|
"jsx",
|
||||||
|
"tsx",
|
||||||
|
"py",
|
||||||
|
"rs",
|
||||||
|
"go",
|
||||||
|
"java",
|
||||||
|
"c",
|
||||||
|
"cpp",
|
||||||
|
"h",
|
||||||
|
"hpp",
|
||||||
|
"css",
|
||||||
|
"html",
|
||||||
|
"svelte",
|
||||||
|
"vue",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!selected) return;
|
||||||
|
|
||||||
|
// Handle both single and multiple file selection
|
||||||
|
const files = Array.isArray(selected) ? selected : [selected];
|
||||||
|
|
||||||
|
for (const filePath of files) {
|
||||||
|
const filename = filePath.split(/[/\\]/).pop() || "unknown";
|
||||||
|
const extension = filename.split(".").pop()?.toLowerCase() || "";
|
||||||
|
const fileType = getFileTypeFromExtension(extension);
|
||||||
|
|
||||||
|
// Get file size from Tauri
|
||||||
|
let fileSize = 0;
|
||||||
|
try {
|
||||||
|
fileSize = await invoke<number>("get_file_size", { filePath });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Could not get file size:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment: Attachment = {
|
||||||
|
id: `attachment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
|
filename,
|
||||||
|
path: filePath,
|
||||||
|
size: fileSize,
|
||||||
|
type: fileType,
|
||||||
|
};
|
||||||
|
|
||||||
|
claudeStore.addAttachment(attachment);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open file picker:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePaste(event: ClipboardEvent) {
|
||||||
|
// First, try the web clipboard API for files
|
||||||
|
const items = event.clipboardData?.items;
|
||||||
|
let handledFile = false;
|
||||||
|
|
||||||
|
if (items && items.length > 0) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.kind === "file") {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (!file) continue;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleKeyDown(event: KeyboardEvent) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
// Handle command menu navigation
|
// Handle command menu navigation
|
||||||
if (showCommandMenu && matchingCommands.length > 0) {
|
if (showCommandMenu && matchingCommands.length > 0) {
|
||||||
@@ -352,7 +680,17 @@ User: ${formattedMessage}`;
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form onsubmit={handleSubmit} class="input-bar">
|
<form
|
||||||
|
onsubmit={handleSubmit}
|
||||||
|
ondragenter={handleDragEnter}
|
||||||
|
ondragover={handleDragOver}
|
||||||
|
ondragleave={handleDragLeave}
|
||||||
|
ondrop={handleDrop}
|
||||||
|
class="input-bar"
|
||||||
|
class:is-dragging={isDragging}
|
||||||
|
>
|
||||||
|
<AttachmentPreview {attachments} onRemove={handleRemoveAttachment} />
|
||||||
|
|
||||||
<div class="input-controls flex gap-2 mb-2">
|
<div class="input-controls flex gap-2 mb-2">
|
||||||
<MessageModeSelector />
|
<MessageModeSelector />
|
||||||
</div>
|
</div>
|
||||||
@@ -370,6 +708,7 @@ User: ${formattedMessage}`;
|
|||||||
bind:value={inputValue}
|
bind:value={inputValue}
|
||||||
onkeydown={handleKeyDown}
|
onkeydown={handleKeyDown}
|
||||||
oninput={handleInputChange}
|
oninput={handleInputChange}
|
||||||
|
onpaste={handlePaste}
|
||||||
placeholder={isConnected
|
placeholder={isConnected
|
||||||
? "Ask Hikari anything... (type / for commands)"
|
? "Ask Hikari anything... (type / for commands)"
|
||||||
: "Connect to Claude first..."}
|
: "Connect to Claude first..."}
|
||||||
@@ -384,6 +723,23 @@ User: ${formattedMessage}`;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-wrapper">
|
<div class="button-wrapper">
|
||||||
|
<button type="button" onclick={handleFilePicker} class="attach-button" title="Attach files">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
{#if isProcessing}
|
{#if isProcessing}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -396,7 +752,9 @@ User: ${formattedMessage}`;
|
|||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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)]
|
class="send-button bg-[var(--accent-primary)] hover:bg-[var(--accent-secondary)]
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
@@ -416,6 +774,35 @@ User: ${formattedMessage}`;
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
position: relative;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s,
|
||||||
|
background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-bar.is-dragging {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 2px dashed var(--accent-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-bar.is-dragging::before {
|
||||||
|
content: "Drop files here";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-bar.is-dragging > * {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-controls {
|
.input-controls {
|
||||||
@@ -469,6 +856,32 @@ User: ${formattedMessage}`;
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attach-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-button:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
color: var(--accent-primary);
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attach-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
.send-button {
|
.send-button {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const claudeStore = {
|
|||||||
isProcessing: conversationsStore.isProcessing,
|
isProcessing: conversationsStore.isProcessing,
|
||||||
grantedTools: conversationsStore.grantedTools,
|
grantedTools: conversationsStore.grantedTools,
|
||||||
pendingRetryMessage: conversationsStore.pendingRetryMessage,
|
pendingRetryMessage: conversationsStore.pendingRetryMessage,
|
||||||
|
attachments: conversationsStore.attachments,
|
||||||
|
|
||||||
// New conversation-aware subscriptions
|
// New conversation-aware subscriptions
|
||||||
conversations: conversationsStore.conversations,
|
conversations: conversationsStore.conversations,
|
||||||
@@ -67,6 +68,12 @@ export const claudeStore = {
|
|||||||
saveScrollPosition: conversationsStore.saveScrollPosition,
|
saveScrollPosition: conversationsStore.saveScrollPosition,
|
||||||
getScrollPosition: conversationsStore.getScrollPosition,
|
getScrollPosition: conversationsStore.getScrollPosition,
|
||||||
|
|
||||||
|
// Attachment management
|
||||||
|
addAttachment: conversationsStore.addAttachment,
|
||||||
|
removeAttachment: conversationsStore.removeAttachment,
|
||||||
|
clearAttachments: conversationsStore.clearAttachments,
|
||||||
|
getAttachments: conversationsStore.getAttachments,
|
||||||
|
|
||||||
getGrantedTools: (): string[] => {
|
getGrantedTools: (): string[] => {
|
||||||
let tools: string[] = [];
|
let tools: string[] = [];
|
||||||
conversationsStore.grantedTools.subscribe((t) => (tools = Array.from(t)))();
|
conversationsStore.grantedTools.subscribe((t) => (tools = Array.from(t)))();
|
||||||
@@ -86,6 +93,7 @@ export const claudeStore = {
|
|||||||
conversationsStore.setWorkingDirectory("");
|
conversationsStore.setWorkingDirectory("");
|
||||||
conversationsStore.setProcessing(false);
|
conversationsStore.setProcessing(false);
|
||||||
conversationsStore.revokeAllTools();
|
conversationsStore.revokeAllTools();
|
||||||
|
conversationsStore.clearAttachments();
|
||||||
// Also clear history restoration
|
// Also clear history restoration
|
||||||
clearHistoryRestore();
|
clearHistoryRestore();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
ConnectionStatus,
|
ConnectionStatus,
|
||||||
PermissionRequest,
|
PermissionRequest,
|
||||||
UserQuestionEvent,
|
UserQuestionEvent,
|
||||||
|
Attachment,
|
||||||
} from "$lib/types/messages";
|
} from "$lib/types/messages";
|
||||||
import type { CharacterState } from "$lib/types/states";
|
import type { CharacterState } from "$lib/types/states";
|
||||||
import { cleanupConversationTracking } from "$lib/tauri";
|
import { cleanupConversationTracking } from "$lib/tauri";
|
||||||
@@ -24,6 +25,7 @@ export interface Conversation {
|
|||||||
scrollPosition: number;
|
scrollPosition: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
lastActivityAt: Date;
|
lastActivityAt: Date;
|
||||||
|
attachments: Attachment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function createConversationsStore() {
|
function createConversationsStore() {
|
||||||
@@ -59,6 +61,7 @@ function createConversationsStore() {
|
|||||||
scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll)
|
scrollPosition: -1, // -1 means "scroll to bottom" (auto-scroll)
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
lastActivityAt: new Date(),
|
lastActivityAt: new Date(),
|
||||||
|
attachments: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +112,7 @@ function createConversationsStore() {
|
|||||||
);
|
);
|
||||||
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
|
const pendingQuestion = derived(activeConversation, ($conv) => $conv?.pendingQuestion || null);
|
||||||
const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1);
|
const scrollPosition = derived(activeConversation, ($conv) => $conv?.scrollPosition ?? -1);
|
||||||
|
const attachments = derived(activeConversation, ($conv) => $conv?.attachments || []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Expose derived stores for compatibility
|
// Expose derived stores for compatibility
|
||||||
@@ -122,6 +126,7 @@ function createConversationsStore() {
|
|||||||
grantedTools: { subscribe: grantedTools.subscribe },
|
grantedTools: { subscribe: grantedTools.subscribe },
|
||||||
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
|
pendingRetryMessage: { subscribe: pendingRetryMessage.subscribe },
|
||||||
scrollPosition: { subscribe: scrollPosition.subscribe },
|
scrollPosition: { subscribe: scrollPosition.subscribe },
|
||||||
|
attachments: { subscribe: attachments.subscribe },
|
||||||
|
|
||||||
// New conversation-specific stores
|
// New conversation-specific stores
|
||||||
conversations: { subscribe: conversations.subscribe },
|
conversations: { subscribe: conversations.subscribe },
|
||||||
@@ -272,7 +277,7 @@ function createConversationsStore() {
|
|||||||
return newConv.id;
|
return newConv.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteConversation: (id: string) => {
|
deleteConversation: async (id: string) => {
|
||||||
ensureInitialized();
|
ensureInitialized();
|
||||||
const convs = get(conversations);
|
const convs = get(conversations);
|
||||||
const activeId = get(activeConversationId);
|
const activeId = get(activeConversationId);
|
||||||
@@ -282,8 +287,8 @@ function createConversationsStore() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up tracking for this conversation
|
// Clean up tracking for this conversation (including temp files)
|
||||||
cleanupConversationTracking(id);
|
await cleanupConversationTracking(id);
|
||||||
|
|
||||||
conversations.update((c) => {
|
conversations.update((c) => {
|
||||||
c.delete(id);
|
c.delete(id);
|
||||||
@@ -571,6 +576,58 @@ function createConversationsStore() {
|
|||||||
return conv?.grantedTools.has(toolName) || false;
|
return conv?.grantedTools.has(toolName) || false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Attachment management
|
||||||
|
addAttachment: (attachment: Attachment) => {
|
||||||
|
const activeId = get(activeConversationId);
|
||||||
|
if (!activeId) return;
|
||||||
|
|
||||||
|
conversations.update((convs) => {
|
||||||
|
const conv = convs.get(activeId);
|
||||||
|
if (conv) {
|
||||||
|
conv.attachments.push(attachment);
|
||||||
|
conv.lastActivityAt = new Date();
|
||||||
|
}
|
||||||
|
return convs;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeAttachment: (id: string) => {
|
||||||
|
const activeId = get(activeConversationId);
|
||||||
|
if (!activeId) return;
|
||||||
|
|
||||||
|
conversations.update((convs) => {
|
||||||
|
const conv = convs.get(activeId);
|
||||||
|
if (conv) {
|
||||||
|
conv.attachments = conv.attachments.filter((a) => a.id !== id);
|
||||||
|
conv.lastActivityAt = new Date();
|
||||||
|
}
|
||||||
|
return convs;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAttachments: () => {
|
||||||
|
const activeId = get(activeConversationId);
|
||||||
|
if (!activeId) return;
|
||||||
|
|
||||||
|
conversations.update((convs) => {
|
||||||
|
const conv = convs.get(activeId);
|
||||||
|
if (conv) {
|
||||||
|
conv.attachments = [];
|
||||||
|
conv.lastActivityAt = new Date();
|
||||||
|
}
|
||||||
|
return convs;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getAttachments: (): Attachment[] => {
|
||||||
|
const activeId = get(activeConversationId);
|
||||||
|
if (!activeId) return [];
|
||||||
|
|
||||||
|
const convs = get(conversations);
|
||||||
|
const conv = convs.get(activeId);
|
||||||
|
return conv?.attachments || [];
|
||||||
|
},
|
||||||
|
|
||||||
// Add initialization helper
|
// Add initialization helper
|
||||||
initialize: () => {
|
initialize: () => {
|
||||||
ensureInitialized();
|
ensureInitialized();
|
||||||
|
|||||||
+8
-1
@@ -107,8 +107,15 @@ interface WorkingDirectoryPayload {
|
|||||||
conversation_id?: string;
|
conversation_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cleanupConversationTracking(conversationId: string) {
|
export async function cleanupConversationTracking(conversationId: string) {
|
||||||
connectedConversations.delete(conversationId);
|
connectedConversations.delete(conversationId);
|
||||||
|
|
||||||
|
// Clean up any temp files associated with this conversation
|
||||||
|
try {
|
||||||
|
await invoke("cleanup_temp_files", { conversationId });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to cleanup temp files for conversation:", error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initializeTauriListeners() {
|
export async function initializeTauriListeners() {
|
||||||
|
|||||||
@@ -142,6 +142,16 @@ export interface UserQuestionEvent {
|
|||||||
|
|
||||||
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
export type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
||||||
|
|
||||||
|
export interface Attachment {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
type: "image" | "document" | "other";
|
||||||
|
mimeType?: string;
|
||||||
|
previewUrl?: string; // For images, a data URL or object URL for preview
|
||||||
|
}
|
||||||
|
|
||||||
export interface UpdateInfo {
|
export interface UpdateInfo {
|
||||||
current_version: string;
|
current_version: string;
|
||||||
latest_version: string;
|
latest_version: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user