feat: integrate all production readiness modules
Register new commands in lib.rs, add command modules, update Cargo deps (notify-rust, keyring, bundled-full), fix PRAGMA WAL for bundled-full, add notifications/heartbeats/FTS5 indexing to agent-dispatcher, update SettingsTab with secrets/plugins/sandbox/updates sections.
This commit is contained in:
parent
3cb65fd5e5
commit
c193db49a8
9 changed files with 1377 additions and 20 deletions
473
v2/Cargo.lock
generated
473
v2/Cargo.lock
generated
|
|
@ -118,6 +118,126 @@ dependencies = [
|
|||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
|
|
@ -209,6 +329,19 @@ dependencies = [
|
|||
"objc2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.2"
|
||||
|
|
@ -237,8 +370,10 @@ dependencies = [
|
|||
"bterminal-core",
|
||||
"dirs 5.0.1",
|
||||
"futures-util",
|
||||
"keyring",
|
||||
"log",
|
||||
"notify",
|
||||
"notify-rust",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"opentelemetry_sdk",
|
||||
|
|
@ -264,6 +399,7 @@ name = "bterminal-core"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dirs 5.0.1",
|
||||
"landlock",
|
||||
"log",
|
||||
"portable-pty",
|
||||
"serde",
|
||||
|
|
@ -492,6 +628,15 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
|
|
@ -618,6 +763,27 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
||||
dependencies = [
|
||||
"csv-core",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csv-core"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.9"
|
||||
|
|
@ -869,6 +1035,33 @@ version = "1.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "1.0.0"
|
||||
|
|
@ -919,6 +1112,27 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1109,6 +1323,19 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
|
|
@ -1512,6 +1739,12 @@ version = "0.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
|
|
@ -1987,6 +2220,17 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "3.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
|
||||
dependencies = [
|
||||
"linux-keyutils",
|
||||
"log",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
|
|
@ -2019,6 +2263,17 @@ dependencies = [
|
|||
"selectors",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "landlock"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49fefd6652c57d68aaa32544a4c0e642929725bdc1fd929367cdeb673ab81088"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"libc",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
|
|
@ -2094,6 +2349,16 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-keyutils"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
|
|
@ -2127,6 +2392,18 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.14.1"
|
||||
|
|
@ -2349,6 +2626,20 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2"
|
||||
dependencies = [
|
||||
"futures-lite",
|
||||
"log",
|
||||
"mac-notification-sys",
|
||||
"serde",
|
||||
"tauri-winrt-notification",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
|
|
@ -2673,6 +2964,16 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osakit"
|
||||
version = "0.3.1"
|
||||
|
|
@ -2712,6 +3013,12 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
|
|
@ -2907,6 +3214,17 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
|
|
@ -2927,7 +3245,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
|||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.13.0",
|
||||
"quick-xml",
|
||||
"quick-xml 0.38.4",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
|
@ -2945,6 +3263,20 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.1"
|
||||
|
|
@ -3112,6 +3444,15 @@ dependencies = [
|
|||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
|
|
@ -3436,11 +3777,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"chrono",
|
||||
"csv",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"time",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4518,6 +4865,18 @@ dependencies = [
|
|||
"toml 0.9.12+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winrt-notification"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
||||
dependencies = [
|
||||
"quick-xml 0.37.5",
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.26.0"
|
||||
|
|
@ -5050,6 +5409,17 @@ version = "1.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca"
|
||||
dependencies = [
|
||||
"memoffset 0.9.1",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
|
|
@ -6208,6 +6578,67 @@ dependencies = [
|
|||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"libc",
|
||||
"ordered-stream",
|
||||
"rustix",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"uuid",
|
||||
"windows-sys 0.61.2",
|
||||
"winnow 0.7.15",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"winnow 0.7.15",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.40"
|
||||
|
|
@ -6305,3 +6736,43 @@ name = "zmij"
|
|||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"winnow 0.7.15",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
|
||||
dependencies = [
|
||||
"proc-macro-crate 3.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.117",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ serde_json = "1.0"
|
|||
serde = { version = "1.0", features = ["derive"] }
|
||||
log = "0.4"
|
||||
tauri = { version = "2.10.3", features = [] }
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
rusqlite = { version = "0.31", features = ["bundled-full"] }
|
||||
dirs = "5"
|
||||
notify = { version = "6", features = ["macos_fsevent"] }
|
||||
tauri-plugin-updater = "2.10.0"
|
||||
|
|
@ -38,6 +38,8 @@ opentelemetry = "0.28"
|
|||
opentelemetry_sdk = { version = "0.28", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = { version = "0.28", features = ["http-proto", "reqwest-client"] }
|
||||
tracing-opentelemetry = "0.29"
|
||||
keyring = { version = "3", features = ["linux-native"] }
|
||||
notify-rust = "4"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -11,3 +11,7 @@ pub mod remote;
|
|||
pub mod misc;
|
||||
pub mod btmsg;
|
||||
pub mod bttask;
|
||||
pub mod notifications;
|
||||
pub mod search;
|
||||
pub mod plugins;
|
||||
pub mod secrets;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@ mod event_sink;
|
|||
mod fs_watcher;
|
||||
mod groups;
|
||||
mod memora;
|
||||
mod notifications;
|
||||
mod plugins;
|
||||
mod pty;
|
||||
mod secrets;
|
||||
mod remote;
|
||||
mod search;
|
||||
mod sidecar;
|
||||
mod session;
|
||||
mod telemetry;
|
||||
|
|
@ -21,6 +25,7 @@ use session::SessionDb;
|
|||
use sidecar::{SidecarConfig, SidecarManager};
|
||||
use fs_watcher::ProjectFsWatcher;
|
||||
use watcher::FileWatcherManager;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tauri::Manager;
|
||||
|
||||
|
|
@ -33,10 +38,72 @@ pub(crate) struct AppState {
|
|||
pub ctx_db: Arc<ctx::CtxDb>,
|
||||
pub memora_db: Arc<memora::MemoraDb>,
|
||||
pub remote_manager: Arc<RemoteManager>,
|
||||
pub search_db: Arc<search::SearchDb>,
|
||||
pub app_config: Arc<AppConfig>,
|
||||
_telemetry: telemetry::TelemetryGuard,
|
||||
}
|
||||
|
||||
/// Install btmsg/bttask CLI tools to ~/.local/bin/ so agent subprocesses can find them.
|
||||
/// Sources: bundled resources (production) or repo root (development).
|
||||
/// Only overwrites if the source is newer or the destination doesn't exist.
|
||||
fn install_cli_tools(resource_dir: &Path, dev_root: &Path) {
|
||||
let bin_dir = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".local")
|
||||
.join("bin");
|
||||
if let Err(e) = std::fs::create_dir_all(&bin_dir) {
|
||||
log::warn!("Failed to create ~/.local/bin: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
for tool_name in &["btmsg", "bttask"] {
|
||||
// Try resource dir first (production bundle), then dev repo root
|
||||
let source = [
|
||||
resource_dir.join(tool_name),
|
||||
dev_root.join(tool_name),
|
||||
]
|
||||
.into_iter()
|
||||
.find(|p| p.is_file());
|
||||
|
||||
let source = match source {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
log::warn!("CLI tool '{tool_name}' not found in resources or dev root");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let dest = bin_dir.join(tool_name);
|
||||
let should_install = if dest.exists() {
|
||||
// Compare modification times — install if source is newer
|
||||
match (source.metadata(), dest.metadata()) {
|
||||
(Ok(sm), Ok(dm)) => {
|
||||
sm.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
> dm.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_install {
|
||||
match std::fs::copy(&source, &dest) {
|
||||
Ok(_) => {
|
||||
// Ensure executable permission on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(0o755));
|
||||
}
|
||||
log::info!("Installed {tool_name} to {}", dest.display());
|
||||
}
|
||||
Err(e) => log::warn!("Failed to install {tool_name}: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Force dark GTK theme for native dialogs (file chooser, etc.)
|
||||
|
|
@ -74,6 +141,7 @@ pub fn run() {
|
|||
commands::agent::agent_stop,
|
||||
commands::agent::agent_ready,
|
||||
commands::agent::agent_restart,
|
||||
commands::agent::agent_set_sandbox,
|
||||
// File watcher
|
||||
commands::watcher::file_watch,
|
||||
commands::watcher::file_unwatch,
|
||||
|
|
@ -159,6 +227,16 @@ pub fn run() {
|
|||
commands::btmsg::btmsg_channel_send,
|
||||
commands::btmsg::btmsg_create_channel,
|
||||
commands::btmsg::btmsg_add_channel_member,
|
||||
commands::btmsg::btmsg_register_agents,
|
||||
// btmsg health monitoring
|
||||
commands::btmsg::btmsg_record_heartbeat,
|
||||
commands::btmsg::btmsg_get_stale_agents,
|
||||
commands::btmsg::btmsg_get_dead_letters,
|
||||
commands::btmsg::btmsg_clear_dead_letters,
|
||||
// Audit log
|
||||
commands::btmsg::audit_log_event,
|
||||
commands::btmsg::audit_log_list,
|
||||
commands::btmsg::audit_log_for_agent,
|
||||
// bttask (task board)
|
||||
commands::bttask::bttask_list,
|
||||
commands::bttask::bttask_comments,
|
||||
|
|
@ -167,6 +245,23 @@ pub fn run() {
|
|||
commands::bttask::bttask_create,
|
||||
commands::bttask::bttask_delete,
|
||||
commands::bttask::bttask_review_queue_count,
|
||||
// Search (FTS5)
|
||||
commands::search::search_init,
|
||||
commands::search::search_query,
|
||||
commands::search::search_rebuild,
|
||||
commands::search::search_index_message,
|
||||
// Notifications
|
||||
commands::notifications::notify_desktop,
|
||||
// Secrets (system keyring)
|
||||
commands::secrets::secrets_store,
|
||||
commands::secrets::secrets_get,
|
||||
commands::secrets::secrets_delete,
|
||||
commands::secrets::secrets_list,
|
||||
commands::secrets::secrets_has_keyring,
|
||||
commands::secrets::secrets_known_keys,
|
||||
// Plugins
|
||||
commands::plugins::plugins_discover,
|
||||
commands::plugins::plugin_read_file,
|
||||
// Misc
|
||||
commands::misc::cli_get_group,
|
||||
commands::misc::open_url,
|
||||
|
|
@ -200,6 +295,11 @@ pub fn run() {
|
|||
.parent()
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
// Install btmsg/bttask CLI tools to ~/.local/bin/
|
||||
if !config.is_test_mode() {
|
||||
install_cli_tools(&resource_dir, &dev_root);
|
||||
}
|
||||
|
||||
// Forward test mode env vars to sidecar processes
|
||||
let mut env_overrides = std::collections::HashMap::new();
|
||||
if config.is_test_mode() {
|
||||
|
|
@ -218,6 +318,7 @@ pub fn run() {
|
|||
dev_root.join("sidecar"),
|
||||
],
|
||||
env_overrides,
|
||||
sandbox: bterminal_core::sandbox::SandboxConfig::default(),
|
||||
};
|
||||
|
||||
let pty_manager = Arc::new(PtyManager::new(sink.clone()));
|
||||
|
|
@ -234,6 +335,12 @@ pub fn run() {
|
|||
let memora_db = Arc::new(memora::MemoraDb::new_with_path(config.memora_db_path.clone()));
|
||||
let remote_manager = Arc::new(RemoteManager::new());
|
||||
|
||||
// Initialize FTS5 search database
|
||||
let search_db_path = config.data_dir.join("bterminal").join("search.db");
|
||||
let search_db = Arc::new(
|
||||
search::SearchDb::open(&search_db_path).expect("Failed to open search database"),
|
||||
);
|
||||
|
||||
// Start local sidecar
|
||||
match sidecar_manager.start() {
|
||||
Ok(()) => log::info!("Sidecar startup initiated"),
|
||||
|
|
@ -249,6 +356,7 @@ pub fn run() {
|
|||
ctx_db,
|
||||
memora_db,
|
||||
remote_manager,
|
||||
search_db,
|
||||
app_config: config,
|
||||
_telemetry: telemetry_guard,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -34,8 +34,11 @@ impl SessionDb {
|
|||
.map_err(|e| format!("Failed to open database: {e}"))?;
|
||||
|
||||
// Enable WAL mode for better concurrent read performance
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
|
||||
.map_err(|e| format!("Failed to set pragmas: {e}"))?;
|
||||
// journal_mode returns a result row, so use query_row instead of pragma_update
|
||||
conn.query_row("PRAGMA journal_mode=WAL", [], |_| Ok(()))
|
||||
.map_err(|e| format!("Failed to set journal_mode: {e}"))?;
|
||||
conn.pragma_update(None, "foreign_keys", "ON")
|
||||
.map_err(|e| format!("Failed to set foreign_keys: {e}"))?;
|
||||
|
||||
let db = Self { conn: Mutex::new(conn) };
|
||||
db.migrate()?;
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@
|
|||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"../sidecar/dist/claude-runner.mjs"
|
||||
"../sidecar/dist/claude-runner.mjs",
|
||||
"../../btmsg",
|
||||
"../../bttask"
|
||||
],
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "Multi-session Claude agent dashboard",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import {
|
|||
getAgentSessions,
|
||||
getAgentSession,
|
||||
} from './stores/agents.svelte';
|
||||
import { notify } from './stores/notifications.svelte';
|
||||
import { notify, addNotification } from './stores/notifications.svelte';
|
||||
import { classifyError } from './utils/error-classifier';
|
||||
import { tel } from './adapters/telemetry-bridge';
|
||||
import { recordActivity, recordToolDone, recordTokenSnapshot } from './stores/health.svelte';
|
||||
import { recordFileWrite, clearSessionWrites, setSessionWorktree } from './stores/conflicts.svelte';
|
||||
|
|
@ -35,6 +36,10 @@ import {
|
|||
spawnSubagentPane,
|
||||
clearSubagentRoutes,
|
||||
} from './utils/subagent-router';
|
||||
import { indexMessage } from './adapters/search-bridge';
|
||||
import { recordHeartbeat } from './adapters/btmsg-bridge';
|
||||
import { logAuditEvent } from './adapters/audit-bridge';
|
||||
import type { AgentId } from './types/ids';
|
||||
|
||||
// Re-export public API consumed by other modules
|
||||
export { registerSessionProject, waitForPendingPersistence } from './utils/session-persistence';
|
||||
|
|
@ -72,11 +77,20 @@ export async function startAgentDispatcher(): Promise<void> {
|
|||
if (!msg.sessionId) return;
|
||||
const sessionId = SessionId(msg.sessionId);
|
||||
|
||||
// Record heartbeat on any agent activity (best-effort, fire-and-forget)
|
||||
const hbProjectId = getSessionProjectId(sessionId);
|
||||
if (hbProjectId) {
|
||||
recordHeartbeat(hbProjectId as unknown as AgentId).catch(() => {});
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case 'agent_started':
|
||||
updateAgentStatus(sessionId, 'running');
|
||||
recordSessionStart(sessionId);
|
||||
tel.info('agent_started', { sessionId });
|
||||
if (hbProjectId) {
|
||||
logAuditEvent(hbProjectId as unknown as AgentId, 'status_change', `Agent started (session ${sessionId.slice(0, 8)})`).catch(() => {});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'agent_event':
|
||||
|
|
@ -87,13 +101,39 @@ export async function startAgentDispatcher(): Promise<void> {
|
|||
updateAgentStatus(sessionId, 'done');
|
||||
tel.info('agent_stopped', { sessionId });
|
||||
notify('success', `Agent ${sessionId.slice(0, 8)} completed`);
|
||||
addNotification('Agent complete', `Session ${sessionId.slice(0, 8)} finished`, 'agent_complete', getSessionProjectId(sessionId) ?? undefined);
|
||||
if (hbProjectId) {
|
||||
logAuditEvent(hbProjectId as unknown as AgentId, 'status_change', `Agent completed (session ${sessionId.slice(0, 8)})`).catch(() => {});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'agent_error':
|
||||
updateAgentStatus(sessionId, 'error', msg.message);
|
||||
tel.error('agent_error', { sessionId, error: msg.message });
|
||||
notify('error', `Agent error: ${msg.message ?? 'Unknown'}`);
|
||||
case 'agent_error': {
|
||||
const errorMsg = msg.message ?? 'Unknown';
|
||||
const classified = classifyError(errorMsg);
|
||||
updateAgentStatus(sessionId, 'error', errorMsg);
|
||||
tel.error('agent_error', { sessionId, error: errorMsg, errorType: classified.type });
|
||||
|
||||
// Show type-specific toast
|
||||
if (classified.type === 'rate_limit') {
|
||||
notify('warning', `Rate limited. ${classified.retryDelaySec > 0 ? `Retrying in ~${classified.retryDelaySec}s...` : ''}`);
|
||||
} else if (classified.type === 'auth') {
|
||||
notify('error', 'API key invalid or expired. Check Settings.');
|
||||
} else if (classified.type === 'quota') {
|
||||
notify('error', 'API quota exceeded. Check your billing.');
|
||||
} else if (classified.type === 'overloaded') {
|
||||
notify('warning', 'API overloaded. Will retry shortly...');
|
||||
} else if (classified.type === 'network') {
|
||||
notify('error', 'Network error. Check your connection.');
|
||||
} else {
|
||||
notify('error', `Agent error: ${errorMsg}`);
|
||||
}
|
||||
|
||||
addNotification('Agent error', classified.message, 'agent_error', getSessionProjectId(sessionId) ?? undefined);
|
||||
if (hbProjectId) {
|
||||
logAuditEvent(hbProjectId as unknown as AgentId, 'status_change', `Agent error (${classified.type}): ${errorMsg} (session ${sessionId.slice(0, 8)})`).catch(() => {});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'agent_log':
|
||||
break;
|
||||
|
|
@ -121,6 +161,7 @@ export async function startAgentDispatcher(): Promise<void> {
|
|||
restartAttempts++;
|
||||
const delayMs = 1000 * Math.pow(2, restartAttempts - 1); // 1s, 2s, 4s
|
||||
notify('warning', `Sidecar crashed, restarting (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})...`);
|
||||
addNotification('Sidecar crashed', `Restarting (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})`, 'system');
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
try {
|
||||
await restartAgent();
|
||||
|
|
@ -234,8 +275,19 @@ function handleAgentEvent(sessionId: SessionIdType, event: Record<string, unknow
|
|||
isError: cost.isError,
|
||||
});
|
||||
if (cost.isError) {
|
||||
updateAgentStatus(sessionId, 'error', cost.errors?.join('; '));
|
||||
notify('error', `Agent failed: ${cost.errors?.[0] ?? 'Unknown error'}`);
|
||||
const costErrorMsg = cost.errors?.join('; ') ?? 'Unknown error';
|
||||
const costClassified = classifyError(costErrorMsg);
|
||||
updateAgentStatus(sessionId, 'error', costErrorMsg);
|
||||
|
||||
if (costClassified.type === 'rate_limit') {
|
||||
notify('warning', `Rate limited. ${costClassified.retryDelaySec > 0 ? `Retrying in ~${costClassified.retryDelaySec}s...` : ''}`);
|
||||
} else if (costClassified.type === 'auth') {
|
||||
notify('error', 'API key invalid or expired. Check Settings.');
|
||||
} else if (costClassified.type === 'quota') {
|
||||
notify('error', 'API quota exceeded. Check your billing.');
|
||||
} else {
|
||||
notify('error', `Agent failed: ${cost.errors?.[0] ?? 'Unknown error'}`);
|
||||
}
|
||||
} else {
|
||||
updateAgentStatus(sessionId, 'done');
|
||||
notify('success', `Agent done — $${cost.totalCostUsd.toFixed(4)}, ${cost.numTurns} turns`);
|
||||
|
|
@ -264,6 +316,13 @@ function handleAgentEvent(sessionId: SessionIdType, event: Record<string, unknow
|
|||
else recordActivity(actProjId);
|
||||
}
|
||||
appendAgentMessages(sessionId, mainMessages);
|
||||
|
||||
// Index searchable text content into FTS5 search database
|
||||
for (const msg of mainMessages) {
|
||||
if (msg.type === 'text' && typeof msg.content === 'string' && msg.content.trim()) {
|
||||
indexMessage(sessionId, 'assistant', msg.content).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append messages to child panes and update their status
|
||||
|
|
|
|||
|
|
@ -25,6 +25,22 @@
|
|||
import type { ProviderId, ProviderSettings } from '../../providers/types';
|
||||
import { ANCHOR_BUDGET_SCALES, ANCHOR_BUDGET_SCALE_LABELS, type AnchorBudgetScale } from '../../types/anchors';
|
||||
import { WAKE_STRATEGIES, WAKE_STRATEGY_LABELS, WAKE_STRATEGY_DESCRIPTIONS, type WakeStrategy } from '../../types/wake';
|
||||
import {
|
||||
storeSecret, getSecret, deleteSecret, listSecrets,
|
||||
hasKeyring, knownSecretKeys, SECRET_KEY_LABELS,
|
||||
} from '../../adapters/secrets-bridge';
|
||||
import {
|
||||
checkForUpdates,
|
||||
getCurrentVersion,
|
||||
getLastCheckTimestamp,
|
||||
type UpdateInfo,
|
||||
} from '../../utils/updater';
|
||||
import {
|
||||
getPluginEntries,
|
||||
setPluginEnabled,
|
||||
reloadAllPlugins,
|
||||
type PluginEntry,
|
||||
} from '../../stores/plugins.svelte';
|
||||
|
||||
const PROJECT_ICONS = [
|
||||
'📁', '🚀', '🤖', '🌐', '🔧', '🎮', '📱', '💻',
|
||||
|
|
@ -61,6 +77,23 @@
|
|||
let filesSaveOnBlur = $state(false);
|
||||
let selectedTheme = $state<ThemeId>(getCurrentTheme());
|
||||
|
||||
// Updater state
|
||||
let appVersion = $state('');
|
||||
let updateCheckResult = $state<UpdateInfo | null>(null);
|
||||
let updateChecking = $state(false);
|
||||
let updateLastCheck = $state<string>('');
|
||||
|
||||
// Secrets state
|
||||
let keyringAvailable = $state(false);
|
||||
let storedKeys = $state<string[]>([]);
|
||||
let knownKeys = $state<string[]>([]);
|
||||
let revealedKey = $state<string | null>(null);
|
||||
let revealedValue = $state('');
|
||||
let newSecretKey = $state('');
|
||||
let newSecretValue = $state('');
|
||||
let secretsKeyDropdownOpen = $state(false);
|
||||
let secretsSaving = $state(false);
|
||||
|
||||
// Dropdown open states
|
||||
let themeDropdownOpen = $state(false);
|
||||
let uiFontDropdownOpen = $state(false);
|
||||
|
|
@ -152,12 +185,40 @@
|
|||
} catch {
|
||||
providerSettings = {};
|
||||
}
|
||||
|
||||
// Load secrets state
|
||||
try {
|
||||
keyringAvailable = await hasKeyring();
|
||||
if (keyringAvailable) {
|
||||
storedKeys = await listSecrets();
|
||||
knownKeys = await knownSecretKeys();
|
||||
}
|
||||
} catch {
|
||||
keyringAvailable = false;
|
||||
}
|
||||
|
||||
// Load app version for updater section
|
||||
appVersion = await getCurrentVersion();
|
||||
const ts = getLastCheckTimestamp();
|
||||
if (ts) updateLastCheck = new Date(ts).toLocaleString();
|
||||
});
|
||||
|
||||
function applyCssProp(prop: string, value: string) {
|
||||
document.documentElement.style.setProperty(prop, value);
|
||||
}
|
||||
|
||||
async function handleCheckForUpdates() {
|
||||
updateChecking = true;
|
||||
try {
|
||||
updateCheckResult = await checkForUpdates();
|
||||
updateLastCheck = new Date().toLocaleString();
|
||||
} catch {
|
||||
updateCheckResult = { available: false };
|
||||
} finally {
|
||||
updateChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveGlobalSetting(key: string, value: string) {
|
||||
try {
|
||||
await setSetting(key, value);
|
||||
|
|
@ -244,6 +305,66 @@
|
|||
return providerSettings[providerId]?.enabled ?? true;
|
||||
}
|
||||
|
||||
// --- Secrets handlers ---
|
||||
|
||||
async function handleRevealSecret(key: string) {
|
||||
if (revealedKey === key) {
|
||||
revealedKey = null;
|
||||
revealedValue = '';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const val = await getSecret(key);
|
||||
revealedKey = key;
|
||||
revealedValue = val ?? '';
|
||||
} catch (e) {
|
||||
console.error(`Failed to reveal secret '${key}':`, e);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveSecret() {
|
||||
if (!newSecretKey || !newSecretValue) return;
|
||||
secretsSaving = true;
|
||||
try {
|
||||
await storeSecret(newSecretKey, newSecretValue);
|
||||
storedKeys = await listSecrets();
|
||||
newSecretKey = '';
|
||||
newSecretValue = '';
|
||||
// If we just saved the currently revealed key, clear reveal
|
||||
revealedKey = null;
|
||||
revealedValue = '';
|
||||
} catch (e) {
|
||||
console.error('Failed to store secret:', e);
|
||||
} finally {
|
||||
secretsSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteSecret(key: string) {
|
||||
try {
|
||||
await deleteSecret(key);
|
||||
storedKeys = await listSecrets();
|
||||
if (revealedKey === key) {
|
||||
revealedKey = null;
|
||||
revealedValue = '';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to delete secret '${key}':`, e);
|
||||
}
|
||||
}
|
||||
|
||||
function getSecretKeyLabel(key: string): string {
|
||||
return SECRET_KEY_LABELS[key] ?? key;
|
||||
}
|
||||
|
||||
let availableKeysForAdd = $derived(
|
||||
knownKeys.filter(k => !storedKeys.includes(k)),
|
||||
);
|
||||
|
||||
let newSecretKeyLabel = $derived(
|
||||
newSecretKey ? getSecretKeyLabel(newSecretKey) : 'Select key...',
|
||||
);
|
||||
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.custom-dropdown')) {
|
||||
|
|
@ -251,6 +372,7 @@
|
|||
uiFontDropdownOpen = false;
|
||||
termFontDropdownOpen = false;
|
||||
providerDropdownOpenFor = null;
|
||||
secretsKeyDropdownOpen = false;
|
||||
}
|
||||
if (!target.closest('.icon-field')) {
|
||||
iconPickerOpenFor = null;
|
||||
|
|
@ -267,6 +389,7 @@
|
|||
termFontDropdownOpen = false;
|
||||
iconPickerOpenFor = null;
|
||||
profileDropdownOpenFor = null;
|
||||
secretsKeyDropdownOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -301,6 +424,13 @@
|
|||
newCwd = '';
|
||||
}
|
||||
|
||||
// Plugin entries (reactive from store)
|
||||
let pluginEntries = $derived(getPluginEntries());
|
||||
|
||||
async function handleReloadPlugins() {
|
||||
await reloadAllPlugins(activeGroupId);
|
||||
}
|
||||
|
||||
// New group form
|
||||
let newGroupName = $state('');
|
||||
|
||||
|
|
@ -548,6 +678,37 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Updates</h2>
|
||||
<div class="settings-list">
|
||||
<div class="setting-field">
|
||||
<span class="setting-label">Current version</span>
|
||||
<span class="setting-value">{appVersion || '...'}</span>
|
||||
</div>
|
||||
{#if updateLastCheck}
|
||||
<div class="setting-field">
|
||||
<span class="setting-label">Last checked</span>
|
||||
<span class="setting-value setting-muted">{updateLastCheck}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if updateCheckResult?.available}
|
||||
<div class="setting-field">
|
||||
<span class="setting-label">Available</span>
|
||||
<span class="setting-value update-available">v{updateCheckResult.version}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="setting-field">
|
||||
<button
|
||||
class="btn-primary"
|
||||
onclick={handleCheckForUpdates}
|
||||
disabled={updateChecking}
|
||||
>
|
||||
{updateChecking ? 'Checking...' : 'Check for Updates'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Providers</h2>
|
||||
<div class="provider-list">
|
||||
|
|
@ -605,6 +766,171 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Secrets</h2>
|
||||
<div class="secrets-status">
|
||||
<span class="keyring-indicator" class:available={keyringAvailable} class:unavailable={!keyringAvailable}></span>
|
||||
<span class="keyring-label">
|
||||
{keyringAvailable ? 'System keyring available' : 'System keyring unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if !keyringAvailable}
|
||||
<div class="secrets-warning">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
<span>System keyring not available. Secrets cannot be stored securely.</span>
|
||||
</div>
|
||||
{:else}
|
||||
{#if storedKeys.length > 0}
|
||||
<div class="secrets-list">
|
||||
{#each storedKeys as key}
|
||||
<div class="secret-row">
|
||||
<div class="secret-info">
|
||||
<span class="secret-key-name">{getSecretKeyLabel(key)}</span>
|
||||
<span class="secret-key-id">{key}</span>
|
||||
</div>
|
||||
<div class="secret-value-area">
|
||||
{#if revealedKey === key}
|
||||
<input
|
||||
type="text"
|
||||
class="secret-value-input"
|
||||
value={revealedValue}
|
||||
readonly
|
||||
/>
|
||||
{:else}
|
||||
<span class="secret-masked">{'\u25CF'.repeat(8)}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="secret-actions">
|
||||
<button
|
||||
class="secret-btn"
|
||||
title={revealedKey === key ? 'Hide' : 'Reveal'}
|
||||
onclick={() => handleRevealSecret(key)}
|
||||
>
|
||||
{#if revealedKey === key}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
|
||||
{:else}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="secret-btn secret-btn-danger"
|
||||
title="Delete"
|
||||
onclick={() => handleDeleteSecret(key)}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="secret-add-form">
|
||||
<div class="secret-add-row">
|
||||
<div class="custom-dropdown secret-key-dropdown">
|
||||
<button
|
||||
class="dropdown-trigger"
|
||||
onclick={() => { secretsKeyDropdownOpen = !secretsKeyDropdownOpen; }}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={secretsKeyDropdownOpen}
|
||||
>
|
||||
<span class="dropdown-label">{newSecretKeyLabel}</span>
|
||||
<span class="dropdown-arrow">{secretsKeyDropdownOpen ? '\u25B4' : '\u25BE'}</span>
|
||||
</button>
|
||||
{#if secretsKeyDropdownOpen}
|
||||
<div class="dropdown-menu" role="listbox">
|
||||
{#each availableKeysForAdd as key}
|
||||
<button
|
||||
class="dropdown-option"
|
||||
class:active={newSecretKey === key}
|
||||
role="option"
|
||||
aria-selected={newSecretKey === key}
|
||||
onclick={() => { newSecretKey = key; secretsKeyDropdownOpen = false; }}
|
||||
>
|
||||
<span class="dropdown-option-label">{getSecretKeyLabel(key)}</span>
|
||||
<span class="secret-key-hint">{key}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{#if availableKeysForAdd.length === 0}
|
||||
<span class="dropdown-empty">All keys configured</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<input
|
||||
type="password"
|
||||
class="secret-value-new"
|
||||
bind:value={newSecretValue}
|
||||
placeholder="Secret value"
|
||||
disabled={!newSecretKey}
|
||||
/>
|
||||
<button
|
||||
class="btn-primary"
|
||||
onclick={handleSaveSecret}
|
||||
disabled={!newSecretKey || !newSecretValue || secretsSaving}
|
||||
>
|
||||
{secretsSaving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Plugins</h2>
|
||||
{#if pluginEntries.length === 0}
|
||||
<p class="empty-notice">No plugins found in ~/.config/bterminal/plugins/</p>
|
||||
{:else}
|
||||
<div class="plugin-list">
|
||||
{#each pluginEntries as entry (entry.meta.id)}
|
||||
<div class="plugin-row">
|
||||
<div class="plugin-info">
|
||||
<span class="plugin-name">{entry.meta.name}</span>
|
||||
<span class="plugin-version">v{entry.meta.version}</span>
|
||||
{#if entry.status === 'loaded'}
|
||||
<span class="plugin-badge loaded" title="Loaded">loaded</span>
|
||||
{:else if entry.status === 'error'}
|
||||
<span class="plugin-badge error" title={entry.error ?? 'Error'}>error</span>
|
||||
{:else if entry.status === 'disabled'}
|
||||
<span class="plugin-badge disabled">disabled</span>
|
||||
{:else}
|
||||
<span class="plugin-badge discovered">discovered</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if entry.meta.description}
|
||||
<p class="plugin-desc">{entry.meta.description}</p>
|
||||
{/if}
|
||||
{#if entry.meta.permissions.length > 0}
|
||||
<div class="plugin-perms">
|
||||
{#each entry.meta.permissions as perm}
|
||||
<span class="perm-badge">{perm}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if entry.error}
|
||||
<p class="plugin-error">{entry.error}</p>
|
||||
{/if}
|
||||
<label class="card-toggle" title={entry.status === 'disabled' ? 'Disabled' : 'Enabled'}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={entry.status !== 'disabled'}
|
||||
onchange={async (e) => {
|
||||
const enabled = (e.target as HTMLInputElement).checked;
|
||||
await setPluginEnabled(entry.meta.id, enabled);
|
||||
}}
|
||||
/>
|
||||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||
</label>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<button class="btn-primary reload-plugins-btn" onclick={handleReloadPlugins}>
|
||||
Reload Plugins
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Groups</h2>
|
||||
<div class="group-list">
|
||||
|
|
@ -962,6 +1288,21 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card-field card-field-row">
|
||||
<span class="card-field-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
Sandbox (Landlock)
|
||||
</span>
|
||||
<label class="card-toggle" title={project.sandboxEnabled ? 'Filesystem sandbox enabled' : 'Filesystem sandbox disabled'}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={project.sandboxEnabled ?? false}
|
||||
onchange={e => updateProject(activeGroupId, project.id, { sandboxEnabled: (e.target as HTMLInputElement).checked })}
|
||||
/>
|
||||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="card-field">
|
||||
<span class="card-field-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
|
|
@ -1068,6 +1409,21 @@
|
|||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.setting-value {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ctp-text);
|
||||
}
|
||||
|
||||
.setting-muted {
|
||||
color: var(--ctp-overlay0);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.update-available {
|
||||
color: var(--ctp-green);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.setting-field > input,
|
||||
.setting-field .input-with-browse input {
|
||||
padding: 0.375rem 0.625rem;
|
||||
|
|
@ -2044,4 +2400,313 @@
|
|||
overflow-y: auto;
|
||||
border-top: 1px solid var(--ctp-surface1);
|
||||
}
|
||||
|
||||
/* Secrets section */
|
||||
.secrets-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.keyring-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.keyring-indicator.available {
|
||||
background: var(--ctp-green);
|
||||
box-shadow: 0 0 4px color-mix(in srgb, var(--ctp-green) 50%, transparent);
|
||||
}
|
||||
|
||||
.keyring-indicator.unavailable {
|
||||
background: var(--ctp-red);
|
||||
box-shadow: 0 0 4px color-mix(in srgb, var(--ctp-red) 50%, transparent);
|
||||
}
|
||||
|
||||
.keyring-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--ctp-subtext0);
|
||||
}
|
||||
|
||||
.secrets-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
background: color-mix(in srgb, var(--ctp-red) 8%, var(--ctp-surface0));
|
||||
border: 1px solid color-mix(in srgb, var(--ctp-red) 30%, var(--ctp-surface1));
|
||||
border-radius: 0.375rem;
|
||||
color: var(--ctp-red);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.secrets-warning svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.secrets-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.secret-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.375rem;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
.secret-row:hover {
|
||||
border-color: var(--ctp-surface2);
|
||||
}
|
||||
|
||||
.secret-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.0625rem;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.secret-key-name {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.secret-key-id {
|
||||
font-size: 0.625rem;
|
||||
color: var(--ctp-overlay0);
|
||||
font-family: var(--term-font-family, monospace);
|
||||
}
|
||||
|
||||
.secret-value-area {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.secret-masked {
|
||||
color: var(--ctp-overlay0);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.secret-value-input {
|
||||
width: 100%;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--ctp-base);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--ctp-text);
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--term-font-family, monospace);
|
||||
}
|
||||
|
||||
.secret-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.secret-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
background: transparent;
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--ctp-overlay1);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.secret-btn:hover {
|
||||
color: var(--ctp-text);
|
||||
background: var(--ctp-surface0);
|
||||
border-color: var(--ctp-surface2);
|
||||
}
|
||||
|
||||
.secret-btn-danger:hover {
|
||||
color: var(--ctp-red);
|
||||
background: color-mix(in srgb, var(--ctp-red) 8%, transparent);
|
||||
border-color: color-mix(in srgb, var(--ctp-red) 30%, var(--ctp-surface1));
|
||||
}
|
||||
|
||||
.secret-add-form {
|
||||
padding: 0.5rem 0.625rem;
|
||||
background: var(--ctp-mantle);
|
||||
border: 1px dashed var(--ctp-surface1);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.secret-add-row {
|
||||
display: flex;
|
||||
gap: 0.375rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.secret-key-dropdown {
|
||||
min-width: 10rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.secret-key-hint {
|
||||
font-size: 0.625rem;
|
||||
color: var(--ctp-overlay0);
|
||||
font-family: var(--term-font-family, monospace);
|
||||
margin-left: auto;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.dropdown-empty {
|
||||
display: block;
|
||||
padding: 0.375rem 0.625rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--ctp-overlay0);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.secret-value-new {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0.375rem 0.625rem;
|
||||
background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--ctp-text);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.secret-value-new:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.secret-value-new:focus {
|
||||
border-color: var(--ctp-blue);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* --- Plugins section --- */
|
||||
|
||||
.plugin-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.plugin-row {
|
||||
position: relative;
|
||||
background: var(--ctp-surface0);
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.plugin-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.plugin-name {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: var(--ctp-text);
|
||||
}
|
||||
|
||||
.plugin-version {
|
||||
font-size: 0.68rem;
|
||||
color: var(--ctp-overlay0);
|
||||
}
|
||||
|
||||
.plugin-badge {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.05rem 0.3rem;
|
||||
border-radius: 0.1875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.plugin-badge.loaded {
|
||||
background: color-mix(in srgb, var(--ctp-green) 20%, transparent);
|
||||
color: var(--ctp-green);
|
||||
}
|
||||
|
||||
.plugin-badge.error {
|
||||
background: color-mix(in srgb, var(--ctp-red) 20%, transparent);
|
||||
color: var(--ctp-red);
|
||||
}
|
||||
|
||||
.plugin-badge.disabled {
|
||||
background: color-mix(in srgb, var(--ctp-overlay0) 20%, transparent);
|
||||
color: var(--ctp-overlay0);
|
||||
}
|
||||
|
||||
.plugin-badge.discovered {
|
||||
background: color-mix(in srgb, var(--ctp-blue) 20%, transparent);
|
||||
color: var(--ctp-blue);
|
||||
}
|
||||
|
||||
.plugin-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--ctp-subtext0);
|
||||
margin: 0.125rem 0 0.25rem;
|
||||
}
|
||||
|
||||
.plugin-perms {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.125rem;
|
||||
}
|
||||
|
||||
.perm-badge {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.05rem 0.25rem;
|
||||
border-radius: 0.125rem;
|
||||
background: color-mix(in srgb, var(--ctp-mauve) 15%, transparent);
|
||||
color: var(--ctp-mauve);
|
||||
font-family: var(--font-mono, monospace);
|
||||
}
|
||||
|
||||
.plugin-error {
|
||||
font-size: 0.7rem;
|
||||
color: var(--ctp-red);
|
||||
margin: 0.25rem 0 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.plugin-row .card-toggle {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.empty-notice {
|
||||
font-size: 0.78rem;
|
||||
color: var(--ctp-overlay0);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.reload-plugins-btn {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,32 +1,75 @@
|
|||
// Auto-update checker — uses Tauri updater plugin
|
||||
// Requires signing key to be configured in tauri.conf.json before use
|
||||
|
||||
import { check } from '@tauri-apps/plugin-updater';
|
||||
import { check, type Update } from '@tauri-apps/plugin-updater';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
|
||||
export async function checkForUpdates(): Promise<{
|
||||
export interface UpdateInfo {
|
||||
available: boolean;
|
||||
version?: string;
|
||||
notes?: string;
|
||||
}> {
|
||||
date?: string;
|
||||
currentVersion?: string;
|
||||
}
|
||||
|
||||
// Cache the last check result for UI access
|
||||
let lastCheckResult: UpdateInfo | null = null;
|
||||
let lastCheckTimestamp: number | null = null;
|
||||
let cachedUpdate: Update | null = null;
|
||||
|
||||
export function getLastCheckResult(): UpdateInfo | null {
|
||||
return lastCheckResult;
|
||||
}
|
||||
|
||||
export function getLastCheckTimestamp(): number | null {
|
||||
return lastCheckTimestamp;
|
||||
}
|
||||
|
||||
export async function getCurrentVersion(): Promise<string> {
|
||||
try {
|
||||
const update = await check();
|
||||
return await getVersion();
|
||||
} catch {
|
||||
return '0.0.0';
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForUpdates(): Promise<UpdateInfo> {
|
||||
try {
|
||||
const [update, currentVersion] = await Promise.all([check(), getCurrentVersion()]);
|
||||
lastCheckTimestamp = Date.now();
|
||||
|
||||
if (update) {
|
||||
return {
|
||||
cachedUpdate = update;
|
||||
lastCheckResult = {
|
||||
available: true,
|
||||
version: update.version,
|
||||
notes: update.body ?? undefined,
|
||||
date: update.date ?? undefined,
|
||||
currentVersion,
|
||||
};
|
||||
} else {
|
||||
cachedUpdate = null;
|
||||
lastCheckResult = {
|
||||
available: false,
|
||||
currentVersion,
|
||||
};
|
||||
}
|
||||
return { available: false };
|
||||
|
||||
return lastCheckResult;
|
||||
} catch {
|
||||
// Updater not configured or network error — silently skip
|
||||
return { available: false };
|
||||
lastCheckResult = { available: false };
|
||||
lastCheckTimestamp = Date.now();
|
||||
return lastCheckResult;
|
||||
}
|
||||
}
|
||||
|
||||
export async function installUpdate(): Promise<void> {
|
||||
const update = await check();
|
||||
// Use cached update from last check if available
|
||||
const update = cachedUpdate ?? (await check());
|
||||
if (update) {
|
||||
// downloadAndInstall will restart the app after installation
|
||||
await update.downloadAndInstall();
|
||||
// If we reach here, the app should relaunch automatically
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue