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",
|
"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]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.89"
|
||||||
|
|
@ -209,6 +329,19 @@ dependencies = [
|
||||||
"objc2",
|
"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]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.2"
|
version = "8.0.2"
|
||||||
|
|
@ -237,8 +370,10 @@ dependencies = [
|
||||||
"bterminal-core",
|
"bterminal-core",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"keyring",
|
||||||
"log",
|
"log",
|
||||||
"notify",
|
"notify",
|
||||||
|
"notify-rust",
|
||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
"opentelemetry-otlp",
|
"opentelemetry-otlp",
|
||||||
"opentelemetry_sdk",
|
"opentelemetry_sdk",
|
||||||
|
|
@ -264,6 +399,7 @@ name = "bterminal-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
"landlock",
|
||||||
"log",
|
"log",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -492,6 +628,15 @@ dependencies = [
|
||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -618,6 +763,27 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"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]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
|
@ -869,6 +1035,33 @@ version = "1.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
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]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
@ -919,6 +1112,27 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -1109,6 +1323,19 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
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]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
@ -1512,6 +1739,12 @@ version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
|
|
@ -1987,6 +2220,17 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
name = "kqueue"
|
name = "kqueue"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
|
@ -2019,6 +2263,17 @@ dependencies = [
|
||||||
"selectors",
|
"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]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -2094,6 +2349,16 @@ dependencies = [
|
||||||
"vcpkg",
|
"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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|
@ -2127,6 +2392,18 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
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]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
|
|
@ -2349,6 +2626,20 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
|
|
@ -2673,6 +2964,16 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
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]]
|
[[package]]
|
||||||
name = "osakit"
|
name = "osakit"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
@ -2712,6 +3013,12 @@ dependencies = [
|
||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
|
|
@ -2907,6 +3214,17 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
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]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
@ -2927,7 +3245,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"quick-xml",
|
"quick-xml 0.38.4",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
@ -2945,6 +3263,20 @@ dependencies = [
|
||||||
"miniz_oxide",
|
"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]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.13.1"
|
version = "1.13.1"
|
||||||
|
|
@ -3112,6 +3444,15 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"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]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.4"
|
version = "0.38.4"
|
||||||
|
|
@ -3436,11 +3777,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
|
"chrono",
|
||||||
|
"csv",
|
||||||
"fallible-iterator",
|
"fallible-iterator",
|
||||||
"fallible-streaming-iterator",
|
"fallible-streaming-iterator",
|
||||||
"hashlink",
|
"hashlink",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4518,6 +4865,18 @@ dependencies = [
|
||||||
"toml 0.9.12+spec-1.1.0",
|
"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]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.26.0"
|
version = "3.26.0"
|
||||||
|
|
@ -5050,6 +5409,17 @@ version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
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]]
|
[[package]]
|
||||||
name = "unic-char-property"
|
name = "unic-char-property"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -6208,6 +6578,67 @@ dependencies = [
|
||||||
"synstructure",
|
"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]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.40"
|
version = "0.8.40"
|
||||||
|
|
@ -6305,3 +6736,43 @@ name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
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"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
tauri = { version = "2.10.3", features = [] }
|
tauri = { version = "2.10.3", features = [] }
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled-full"] }
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
notify = { version = "6", features = ["macos_fsevent"] }
|
notify = { version = "6", features = ["macos_fsevent"] }
|
||||||
tauri-plugin-updater = "2.10.0"
|
tauri-plugin-updater = "2.10.0"
|
||||||
|
|
@ -38,6 +38,8 @@ opentelemetry = "0.28"
|
||||||
opentelemetry_sdk = { version = "0.28", features = ["rt-tokio"] }
|
opentelemetry_sdk = { version = "0.28", features = ["rt-tokio"] }
|
||||||
opentelemetry-otlp = { version = "0.28", features = ["http-proto", "reqwest-client"] }
|
opentelemetry-otlp = { version = "0.28", features = ["http-proto", "reqwest-client"] }
|
||||||
tracing-opentelemetry = "0.29"
|
tracing-opentelemetry = "0.29"
|
||||||
|
keyring = { version = "3", features = ["linux-native"] }
|
||||||
|
notify-rust = "4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,7 @@ pub mod remote;
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
pub mod btmsg;
|
pub mod btmsg;
|
||||||
pub mod bttask;
|
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 fs_watcher;
|
||||||
mod groups;
|
mod groups;
|
||||||
mod memora;
|
mod memora;
|
||||||
|
mod notifications;
|
||||||
|
mod plugins;
|
||||||
mod pty;
|
mod pty;
|
||||||
|
mod secrets;
|
||||||
mod remote;
|
mod remote;
|
||||||
|
mod search;
|
||||||
mod sidecar;
|
mod sidecar;
|
||||||
mod session;
|
mod session;
|
||||||
mod telemetry;
|
mod telemetry;
|
||||||
|
|
@ -21,6 +25,7 @@ use session::SessionDb;
|
||||||
use sidecar::{SidecarConfig, SidecarManager};
|
use sidecar::{SidecarConfig, SidecarManager};
|
||||||
use fs_watcher::ProjectFsWatcher;
|
use fs_watcher::ProjectFsWatcher;
|
||||||
use watcher::FileWatcherManager;
|
use watcher::FileWatcherManager;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
|
|
||||||
|
|
@ -33,10 +38,72 @@ pub(crate) struct AppState {
|
||||||
pub ctx_db: Arc<ctx::CtxDb>,
|
pub ctx_db: Arc<ctx::CtxDb>,
|
||||||
pub memora_db: Arc<memora::MemoraDb>,
|
pub memora_db: Arc<memora::MemoraDb>,
|
||||||
pub remote_manager: Arc<RemoteManager>,
|
pub remote_manager: Arc<RemoteManager>,
|
||||||
|
pub search_db: Arc<search::SearchDb>,
|
||||||
pub app_config: Arc<AppConfig>,
|
pub app_config: Arc<AppConfig>,
|
||||||
_telemetry: telemetry::TelemetryGuard,
|
_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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
// Force dark GTK theme for native dialogs (file chooser, etc.)
|
// Force dark GTK theme for native dialogs (file chooser, etc.)
|
||||||
|
|
@ -74,6 +141,7 @@ pub fn run() {
|
||||||
commands::agent::agent_stop,
|
commands::agent::agent_stop,
|
||||||
commands::agent::agent_ready,
|
commands::agent::agent_ready,
|
||||||
commands::agent::agent_restart,
|
commands::agent::agent_restart,
|
||||||
|
commands::agent::agent_set_sandbox,
|
||||||
// File watcher
|
// File watcher
|
||||||
commands::watcher::file_watch,
|
commands::watcher::file_watch,
|
||||||
commands::watcher::file_unwatch,
|
commands::watcher::file_unwatch,
|
||||||
|
|
@ -159,6 +227,16 @@ pub fn run() {
|
||||||
commands::btmsg::btmsg_channel_send,
|
commands::btmsg::btmsg_channel_send,
|
||||||
commands::btmsg::btmsg_create_channel,
|
commands::btmsg::btmsg_create_channel,
|
||||||
commands::btmsg::btmsg_add_channel_member,
|
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)
|
// bttask (task board)
|
||||||
commands::bttask::bttask_list,
|
commands::bttask::bttask_list,
|
||||||
commands::bttask::bttask_comments,
|
commands::bttask::bttask_comments,
|
||||||
|
|
@ -167,6 +245,23 @@ pub fn run() {
|
||||||
commands::bttask::bttask_create,
|
commands::bttask::bttask_create,
|
||||||
commands::bttask::bttask_delete,
|
commands::bttask::bttask_delete,
|
||||||
commands::bttask::bttask_review_queue_count,
|
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
|
// Misc
|
||||||
commands::misc::cli_get_group,
|
commands::misc::cli_get_group,
|
||||||
commands::misc::open_url,
|
commands::misc::open_url,
|
||||||
|
|
@ -200,6 +295,11 @@ pub fn run() {
|
||||||
.parent()
|
.parent()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_path_buf();
|
.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
|
// Forward test mode env vars to sidecar processes
|
||||||
let mut env_overrides = std::collections::HashMap::new();
|
let mut env_overrides = std::collections::HashMap::new();
|
||||||
if config.is_test_mode() {
|
if config.is_test_mode() {
|
||||||
|
|
@ -218,6 +318,7 @@ pub fn run() {
|
||||||
dev_root.join("sidecar"),
|
dev_root.join("sidecar"),
|
||||||
],
|
],
|
||||||
env_overrides,
|
env_overrides,
|
||||||
|
sandbox: bterminal_core::sandbox::SandboxConfig::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pty_manager = Arc::new(PtyManager::new(sink.clone()));
|
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 memora_db = Arc::new(memora::MemoraDb::new_with_path(config.memora_db_path.clone()));
|
||||||
let remote_manager = Arc::new(RemoteManager::new());
|
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
|
// Start local sidecar
|
||||||
match sidecar_manager.start() {
|
match sidecar_manager.start() {
|
||||||
Ok(()) => log::info!("Sidecar startup initiated"),
|
Ok(()) => log::info!("Sidecar startup initiated"),
|
||||||
|
|
@ -249,6 +356,7 @@ pub fn run() {
|
||||||
ctx_db,
|
ctx_db,
|
||||||
memora_db,
|
memora_db,
|
||||||
remote_manager,
|
remote_manager,
|
||||||
|
search_db,
|
||||||
app_config: config,
|
app_config: config,
|
||||||
_telemetry: telemetry_guard,
|
_telemetry: telemetry_guard,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,11 @@ impl SessionDb {
|
||||||
.map_err(|e| format!("Failed to open database: {e}"))?;
|
.map_err(|e| format!("Failed to open database: {e}"))?;
|
||||||
|
|
||||||
// Enable WAL mode for better concurrent read performance
|
// Enable WAL mode for better concurrent read performance
|
||||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
|
// journal_mode returns a result row, so use query_row instead of pragma_update
|
||||||
.map_err(|e| format!("Failed to set pragmas: {e}"))?;
|
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) };
|
let db = Self { conn: Mutex::new(conn) };
|
||||||
db.migrate()?;
|
db.migrate()?;
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,9 @@
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": [
|
"resources": [
|
||||||
"../sidecar/dist/claude-runner.mjs"
|
"../sidecar/dist/claude-runner.mjs",
|
||||||
|
"../../btmsg",
|
||||||
|
"../../bttask"
|
||||||
],
|
],
|
||||||
"category": "DeveloperTool",
|
"category": "DeveloperTool",
|
||||||
"shortDescription": "Multi-session Claude agent dashboard",
|
"shortDescription": "Multi-session Claude agent dashboard",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ import {
|
||||||
getAgentSessions,
|
getAgentSessions,
|
||||||
getAgentSession,
|
getAgentSession,
|
||||||
} from './stores/agents.svelte';
|
} 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 { tel } from './adapters/telemetry-bridge';
|
||||||
import { recordActivity, recordToolDone, recordTokenSnapshot } from './stores/health.svelte';
|
import { recordActivity, recordToolDone, recordTokenSnapshot } from './stores/health.svelte';
|
||||||
import { recordFileWrite, clearSessionWrites, setSessionWorktree } from './stores/conflicts.svelte';
|
import { recordFileWrite, clearSessionWrites, setSessionWorktree } from './stores/conflicts.svelte';
|
||||||
|
|
@ -35,6 +36,10 @@ import {
|
||||||
spawnSubagentPane,
|
spawnSubagentPane,
|
||||||
clearSubagentRoutes,
|
clearSubagentRoutes,
|
||||||
} from './utils/subagent-router';
|
} 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
|
// Re-export public API consumed by other modules
|
||||||
export { registerSessionProject, waitForPendingPersistence } from './utils/session-persistence';
|
export { registerSessionProject, waitForPendingPersistence } from './utils/session-persistence';
|
||||||
|
|
@ -72,11 +77,20 @@ export async function startAgentDispatcher(): Promise<void> {
|
||||||
if (!msg.sessionId) return;
|
if (!msg.sessionId) return;
|
||||||
const sessionId = SessionId(msg.sessionId);
|
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) {
|
switch (msg.type) {
|
||||||
case 'agent_started':
|
case 'agent_started':
|
||||||
updateAgentStatus(sessionId, 'running');
|
updateAgentStatus(sessionId, 'running');
|
||||||
recordSessionStart(sessionId);
|
recordSessionStart(sessionId);
|
||||||
tel.info('agent_started', { 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;
|
break;
|
||||||
|
|
||||||
case 'agent_event':
|
case 'agent_event':
|
||||||
|
|
@ -87,13 +101,39 @@ export async function startAgentDispatcher(): Promise<void> {
|
||||||
updateAgentStatus(sessionId, 'done');
|
updateAgentStatus(sessionId, 'done');
|
||||||
tel.info('agent_stopped', { sessionId });
|
tel.info('agent_stopped', { sessionId });
|
||||||
notify('success', `Agent ${sessionId.slice(0, 8)} completed`);
|
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;
|
break;
|
||||||
|
|
||||||
case 'agent_error':
|
case 'agent_error': {
|
||||||
updateAgentStatus(sessionId, 'error', msg.message);
|
const errorMsg = msg.message ?? 'Unknown';
|
||||||
tel.error('agent_error', { sessionId, error: msg.message });
|
const classified = classifyError(errorMsg);
|
||||||
notify('error', `Agent error: ${msg.message ?? 'Unknown'}`);
|
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;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'agent_log':
|
case 'agent_log':
|
||||||
break;
|
break;
|
||||||
|
|
@ -121,6 +161,7 @@ export async function startAgentDispatcher(): Promise<void> {
|
||||||
restartAttempts++;
|
restartAttempts++;
|
||||||
const delayMs = 1000 * Math.pow(2, restartAttempts - 1); // 1s, 2s, 4s
|
const delayMs = 1000 * Math.pow(2, restartAttempts - 1); // 1s, 2s, 4s
|
||||||
notify('warning', `Sidecar crashed, restarting (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})...`);
|
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));
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||||
try {
|
try {
|
||||||
await restartAgent();
|
await restartAgent();
|
||||||
|
|
@ -234,8 +275,19 @@ function handleAgentEvent(sessionId: SessionIdType, event: Record<string, unknow
|
||||||
isError: cost.isError,
|
isError: cost.isError,
|
||||||
});
|
});
|
||||||
if (cost.isError) {
|
if (cost.isError) {
|
||||||
updateAgentStatus(sessionId, 'error', cost.errors?.join('; '));
|
const costErrorMsg = cost.errors?.join('; ') ?? 'Unknown error';
|
||||||
notify('error', `Agent failed: ${cost.errors?.[0] ?? '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 {
|
} else {
|
||||||
updateAgentStatus(sessionId, 'done');
|
updateAgentStatus(sessionId, 'done');
|
||||||
notify('success', `Agent done — $${cost.totalCostUsd.toFixed(4)}, ${cost.numTurns} turns`);
|
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);
|
else recordActivity(actProjId);
|
||||||
}
|
}
|
||||||
appendAgentMessages(sessionId, mainMessages);
|
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
|
// Append messages to child panes and update their status
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,22 @@
|
||||||
import type { ProviderId, ProviderSettings } from '../../providers/types';
|
import type { ProviderId, ProviderSettings } from '../../providers/types';
|
||||||
import { ANCHOR_BUDGET_SCALES, ANCHOR_BUDGET_SCALE_LABELS, type AnchorBudgetScale } from '../../types/anchors';
|
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 { 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 = [
|
const PROJECT_ICONS = [
|
||||||
'📁', '🚀', '🤖', '🌐', '🔧', '🎮', '📱', '💻',
|
'📁', '🚀', '🤖', '🌐', '🔧', '🎮', '📱', '💻',
|
||||||
|
|
@ -61,6 +77,23 @@
|
||||||
let filesSaveOnBlur = $state(false);
|
let filesSaveOnBlur = $state(false);
|
||||||
let selectedTheme = $state<ThemeId>(getCurrentTheme());
|
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
|
// Dropdown open states
|
||||||
let themeDropdownOpen = $state(false);
|
let themeDropdownOpen = $state(false);
|
||||||
let uiFontDropdownOpen = $state(false);
|
let uiFontDropdownOpen = $state(false);
|
||||||
|
|
@ -152,12 +185,40 @@
|
||||||
} catch {
|
} catch {
|
||||||
providerSettings = {};
|
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) {
|
function applyCssProp(prop: string, value: string) {
|
||||||
document.documentElement.style.setProperty(prop, value);
|
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) {
|
async function saveGlobalSetting(key: string, value: string) {
|
||||||
try {
|
try {
|
||||||
await setSetting(key, value);
|
await setSetting(key, value);
|
||||||
|
|
@ -244,6 +305,66 @@
|
||||||
return providerSettings[providerId]?.enabled ?? true;
|
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) {
|
function handleClickOutside(e: MouseEvent) {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (!target.closest('.custom-dropdown')) {
|
if (!target.closest('.custom-dropdown')) {
|
||||||
|
|
@ -251,6 +372,7 @@
|
||||||
uiFontDropdownOpen = false;
|
uiFontDropdownOpen = false;
|
||||||
termFontDropdownOpen = false;
|
termFontDropdownOpen = false;
|
||||||
providerDropdownOpenFor = null;
|
providerDropdownOpenFor = null;
|
||||||
|
secretsKeyDropdownOpen = false;
|
||||||
}
|
}
|
||||||
if (!target.closest('.icon-field')) {
|
if (!target.closest('.icon-field')) {
|
||||||
iconPickerOpenFor = null;
|
iconPickerOpenFor = null;
|
||||||
|
|
@ -267,6 +389,7 @@
|
||||||
termFontDropdownOpen = false;
|
termFontDropdownOpen = false;
|
||||||
iconPickerOpenFor = null;
|
iconPickerOpenFor = null;
|
||||||
profileDropdownOpenFor = null;
|
profileDropdownOpenFor = null;
|
||||||
|
secretsKeyDropdownOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,6 +424,13 @@
|
||||||
newCwd = '';
|
newCwd = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plugin entries (reactive from store)
|
||||||
|
let pluginEntries = $derived(getPluginEntries());
|
||||||
|
|
||||||
|
async function handleReloadPlugins() {
|
||||||
|
await reloadAllPlugins(activeGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
// New group form
|
// New group form
|
||||||
let newGroupName = $state('');
|
let newGroupName = $state('');
|
||||||
|
|
||||||
|
|
@ -548,6 +678,37 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="settings-section">
|
||||||
<h2>Providers</h2>
|
<h2>Providers</h2>
|
||||||
<div class="provider-list">
|
<div class="provider-list">
|
||||||
|
|
@ -605,6 +766,171 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="settings-section">
|
||||||
<h2>Groups</h2>
|
<h2>Groups</h2>
|
||||||
<div class="group-list">
|
<div class="group-list">
|
||||||
|
|
@ -962,6 +1288,21 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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">
|
<div class="card-field">
|
||||||
<span class="card-field-label">
|
<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>
|
<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;
|
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,
|
||||||
.setting-field .input-with-browse input {
|
.setting-field .input-with-browse input {
|
||||||
padding: 0.375rem 0.625rem;
|
padding: 0.375rem 0.625rem;
|
||||||
|
|
@ -2044,4 +2400,313 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border-top: 1px solid var(--ctp-surface1);
|
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>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,75 @@
|
||||||
// Auto-update checker — uses Tauri updater plugin
|
// Auto-update checker — uses Tauri updater plugin
|
||||||
// Requires signing key to be configured in tauri.conf.json before use
|
// 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;
|
available: boolean;
|
||||||
version?: string;
|
version?: string;
|
||||||
notes?: 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 {
|
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) {
|
if (update) {
|
||||||
return {
|
cachedUpdate = update;
|
||||||
|
lastCheckResult = {
|
||||||
available: true,
|
available: true,
|
||||||
version: update.version,
|
version: update.version,
|
||||||
notes: update.body ?? undefined,
|
notes: update.body ?? undefined,
|
||||||
|
date: update.date ?? undefined,
|
||||||
|
currentVersion,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
cachedUpdate = null;
|
||||||
|
lastCheckResult = {
|
||||||
|
available: false,
|
||||||
|
currentVersion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { available: false };
|
|
||||||
|
return lastCheckResult;
|
||||||
} catch {
|
} catch {
|
||||||
// Updater not configured or network error — silently skip
|
// 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> {
|
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) {
|
if (update) {
|
||||||
|
// downloadAndInstall will restart the app after installation
|
||||||
await update.downloadAndInstall();
|
await update.downloadAndInstall();
|
||||||
|
// If we reach here, the app should relaunch automatically
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue