From b63c289a02161424c70009163978038373598994 Mon Sep 17 00:00:00 2001 From: Tran Quang Dang Date: Wed, 10 Jun 2026 21:21:52 +0700 Subject: [PATCH 1/5] feat: integrate beads_rust as issue tracking backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New crate: crates/jcode-beads-bridge — BeadsProject facade + BeadsTaskManager with open/init, CRUD, dependency graph, ready/blocked queries - jcode-base: todo.rs, goal.rs, beads.rs rewritten to delegate to beads_rust - New agent tools: beads_list, beads_create, beads_ready, beads_claim, beads_close, beads_dep — registered in base_tools() - Removed jcode-task-types dep from jcode-base (still used by app-core for CatchupBrief/PersistedCatchupState) - Full workspace compiles clean with zero errors --- Cargo.lock | 1627 +++++++++++++++++++--- Cargo.toml | 2 + crates/jcode-app-core/src/tool/beads.rs | 387 +++++ crates/jcode-app-core/src/tool/mod.rs | 8 + crates/jcode-base/Cargo.toml | 3 +- crates/jcode-base/src/beads.rs | 7 + crates/jcode-base/src/goal.rs | 824 ++++------- crates/jcode-base/src/lib.rs | 2 + crates/jcode-base/src/todo.rs | 49 +- crates/jcode-beads-bridge/Cargo.toml | 14 + crates/jcode-beads-bridge/src/lib.rs | 23 + crates/jcode-beads-bridge/src/mapping.rs | 245 ++++ crates/jcode-beads-bridge/src/project.rs | 115 ++ crates/jcode-beads-bridge/src/tasks.rs | 203 +++ 14 files changed, 2754 insertions(+), 755 deletions(-) create mode 100644 crates/jcode-app-core/src/tool/beads.rs create mode 100644 crates/jcode-base/src/beads.rs create mode 100644 crates/jcode-beads-bridge/Cargo.toml create mode 100644 crates/jcode-beads-bridge/src/lib.rs create mode 100644 crates/jcode-beads-bridge/src/mapping.rs create mode 100644 crates/jcode-beads-bridge/src/project.rs create mode 100644 crates/jcode-beads-bridge/src/tasks.rs diff --git a/Cargo.lock b/Cargo.lock index a3de2d8acf..c67ec72617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,15 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.1" @@ -33,6 +42,16 @@ dependencies = [ "pom", ] +[[package]] +name = "advisory-lock" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6caee7d48f930f9ad3fc9546f8cbf843365da0c5b0ca4eee1d1ac3dd12d8f93" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "aead" version = "0.5.2" @@ -43,6 +62,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "age" version = "0.11.3" @@ -172,9 +216,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -193,9 +237,9 @@ checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -206,7 +250,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -217,14 +261,14 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "anymap2" @@ -247,6 +291,15 @@ dependencies = [ "object", ] +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arboard" version = "3.6.1" @@ -277,6 +330,18 @@ dependencies = [ "rustversion", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -321,6 +386,59 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "asupersync" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c72750f89a297f34f0daaf0eb824dbd9effcaa85a562f631109d502dbf742a3" +dependencies = [ + "aes-gcm", + "base64 0.22.1", + "bincode-next", + "chacha20poly1305", + "crc32fast", + "crossbeam-deque", + "crossbeam-queue", + "franken-decision", + "franken-evidence", + "franken-kernel", + "futures-lite", + "getrandom 0.4.1", + "hashbrown 0.17.1", + "hex", + "hkdf", + "hmac 0.13.0", + "io-uring", + "js-sys", + "libc", + "memchr", + "nix 0.31.3", + "nkeys", + "parking_lot 0.12.5", + "pin-project", + "polling", + "prost 0.14.4", + "rmp-serde", + "semver", + "serde", + "serde_json", + "sha1 0.11.0", + "sha2 0.11.0", + "signal-hook 0.4.4", + "slab", + "smallvec", + "socket2 0.6.1", + "subtle", + "sysinfo", + "tempfile", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.61.2", + "zeroize", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -395,7 +513,7 @@ dependencies = [ "futures-lite", "parking", "polling", - "rustix 1.1.3", + "rustix 1.1.4", "slab", "windows-sys 0.61.2", ] @@ -426,7 +544,7 @@ dependencies = [ "cfg-if", "event-listener", "futures-lite", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -452,7 +570,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -540,7 +658,7 @@ dependencies = [ "fastrand", "hex", "http 1.4.0", - "sha1", + "sha1 0.10.6", "time", "tokio", "tracing", @@ -1017,6 +1135,21 @@ dependencies = [ "url", ] +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base16ct" version = "0.1.1" @@ -1066,6 +1199,55 @@ dependencies = [ "serde", ] +[[package]] +name = "beads_rust" +version = "0.2.15" +dependencies = [ + "anyhow", + "chrono", + "clap", + "clap_complete", + "crossterm", + "dunce", + "fsqlite", + "fsqlite-ast", + "fsqlite-btree", + "fsqlite-core", + "fsqlite-error", + "fsqlite-func", + "fsqlite-mvcc", + "fsqlite-observability", + "fsqlite-pager", + "fsqlite-parser", + "fsqlite-planner", + "fsqlite-types", + "fsqlite-vdbe", + "fsqlite-vfs", + "fsqlite-wal", + "indicatif", + "mimalloc", + "once_cell", + "regex", + "rich_rust", + "schemars 1.2.1", + "self_update", + "semver", + "serde", + "serde_json", + "serde_norway", + "sha2 0.11.0", + "shell-words", + "signal-hook 0.4.4", + "similar 3.1.1", + "tempfile", + "thiserror 2.0.18", + "tracing", + "tracing-subscriber", + "tru", + "unicode-width 0.2.2", + "vergen-gix", +] + [[package]] name = "bech32" version = "0.9.1" @@ -1090,6 +1272,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bincode-next" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe9e7e6d14aeb39557f226bff158a30e367fac279d0e69b8b42fb41999f9a86" +dependencies = [ + "bincode_derive-next", + "serde", + "unty-next", +] + +[[package]] +name = "bincode_derive-next" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af389a025376dbbe728c43dfa5c08f866116c0b62627787c21ca1c69e2b8dec" +dependencies = [ + "virtue-next", +] + [[package]] name = "bindet" version = "0.3.2" @@ -1153,6 +1355,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "blake3" version = "1.8.5" @@ -1338,7 +1549,7 @@ dependencies = [ "cached_proc_macro_types", "hashbrown 0.15.5", "once_cell", - "thiserror 2.0.17", + "thiserror 2.0.18", "web-time 1.1.0", ] @@ -1416,7 +1627,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -1517,9 +1728,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1552,9 +1763,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -1562,9 +1773,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -1579,13 +1790,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" dependencies = [ "clap", + "clap_lex", + "is_executable", + "shlex", ] [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1595,9 +1809,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "clipboard-win" @@ -1670,7 +1884,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1768,6 +1982,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -1795,6 +2021,17 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + [[package]] name = "cookie-factory" version = "0.3.3" @@ -1804,6 +2041,24 @@ dependencies = [ "futures", ] +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1902,6 +2157,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1929,7 +2193,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", "tracing-subscriber", "urlencoding", @@ -2008,7 +2272,7 @@ dependencies = [ "futures-core", "mio 1.1.1", "parking_lot 0.12.5", - "rustix 1.1.3", + "rustix 1.1.4", "signal-hook 0.3.18", "signal-hook-mio", "winapi", @@ -2058,6 +2322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -2080,6 +2345,15 @@ dependencies = [ "phf", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -2104,6 +2378,7 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", + "digest 0.10.7", "fiat-crypto", "rustc_version", "subtle", @@ -2498,6 +2773,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -2519,6 +2805,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -2649,7 +2946,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2763,10 +3060,35 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", + "der 0.6.1", "elliptic-curve", "rfc6979", - "signature", + "signature 1.6.4", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "signature 2.2.0", + "subtle", + "zeroize", ] [[package]] @@ -2783,12 +3105,12 @@ checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ "base16ct", "crypto-bigint 0.4.9", - "der", + "der 0.6.1", "digest 0.10.7", "ff", "generic-array", "group", - "pkcs8", + "pkcs8 0.9.0", "rand_core 0.6.4", "sec1", "subtle", @@ -2825,10 +3147,16 @@ dependencies = [ "serde", "serde_json", "sled", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2884,7 +3212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3127,7 +3455,7 @@ dependencies = [ "serde_json", "smallvec", "smartstring", - "thiserror 2.0.17", + "thiserror 2.0.18", "toml 0.8.2", "tracing", "tracing-appender", @@ -3241,6 +3569,7 @@ checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -3394,53 +3723,418 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] -name = "foreign-types-shared" -version = "0.3.1" +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "franken-decision" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eab25c0123956de2a2386908e31508b3049eeb5145d1818f151157e3535760" +dependencies = [ + "franken-evidence", + "franken-kernel", + "serde", +] + +[[package]] +name = "franken-evidence" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff54a8d80d29a1960c5f78f9c7fb15b1e0bef8ac5e386b46edd847d96038119" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "franken-kernel" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2304bb46a2a4f813de1f561340970d0f9b3a1020f5746f4dc1a2235838cfc5" +dependencies = [ + "serde", +] + +[[package]] +name = "from_variant" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ff35a391aef949120a0340d690269b3d9f63460a6106e99bd07b961f345ea9" +dependencies = [ + "swc_macros_common", + "syn 2.0.117", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "fsqlite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edbef569fe8a23bee5ca6d7b6b6812c3c352c95feaaccc14b4c1de61e4aa8b96" +dependencies = [ + "fsqlite-core", + "fsqlite-error", + "fsqlite-ext-fts5", + "fsqlite-ext-json", + "fsqlite-ext-rtree", + "fsqlite-parser", + "fsqlite-types", + "fsqlite-vfs", +] + +[[package]] +name = "fsqlite-ast" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7307ba035196025a6a30512868baf3d161b84c227a7132c6ebb4ed5ecfbd4b63" +dependencies = [ + "fsqlite-types", +] + +[[package]] +name = "fsqlite-btree" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f2aa4cf2a0f282ad459c6929ed36cc7d7e1da07d80e70fd69d541f9d44e17" +dependencies = [ + "foldhash 0.2.0", + "fsqlite-error", + "fsqlite-func", + "fsqlite-pager", + "fsqlite-types", + "hashbrown 0.14.5", + "serde", + "smallvec", + "tracing", + "xxhash-rust", +] + +[[package]] +name = "fsqlite-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "729e9c745c3c077c34eaa7ee0e8dd945a160f5a8c2a3e832bcdec1e0c93d8851" +dependencies = [ + "asupersync", + "blake3", + "foldhash 0.2.0", + "fsqlite-ast", + "fsqlite-btree", + "fsqlite-error", + "fsqlite-ext-fts5", + "fsqlite-ext-icu", + "fsqlite-ext-json", + "fsqlite-ext-misc", + "fsqlite-ext-rtree", + "fsqlite-func", + "fsqlite-mvcc", + "fsqlite-observability", + "fsqlite-pager", + "fsqlite-parser", + "fsqlite-planner", + "fsqlite-types", + "fsqlite-vdbe", + "fsqlite-vfs", + "fsqlite-wal", + "hashbrown 0.14.5", + "itoa", + "lazy_static", + "lru 0.16.3", + "serde", + "serde_json", + "smallvec", + "tracing", + "xxhash-rust", +] + +[[package]] +name = "fsqlite-error" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f228de6f42de5be1bdd6228705d79740c686b74d3f387075b83650e451653f15" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "fsqlite-ext-fts5" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1a5864ef7be749b91bb6380ee7e996eba365b58bbcfb3d33f04b21a5e7c4ad" +dependencies = [ + "fsqlite-error", + "fsqlite-func", + "fsqlite-types", + "smallvec", + "tracing", +] + +[[package]] +name = "fsqlite-ext-icu" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cec5636cfdddc1bf56ea2549ccb520e8cc27e3ebd6b4ad1f853cc1ee57a97b71" +dependencies = [ + "fsqlite-error", + "fsqlite-func", + "fsqlite-types", + "tracing", +] + +[[package]] +name = "fsqlite-ext-json" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc56e06411ef73a873f9b53c22b08a4e3ba904ac5b5a0b1d4652fa59b5a568ce" +dependencies = [ + "fsqlite-error", + "fsqlite-func", + "fsqlite-types", + "json5 1.3.1", + "serde_json", +] + +[[package]] +name = "fsqlite-ext-misc" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5aa48e77ad8cddf15d190e06838b8b7dfeac92db52643d775840a1dbcc8c3e" +dependencies = [ + "fsqlite-error", + "fsqlite-func", + "fsqlite-types", + "getrandom 0.2.16", + "rand 0.8.5", + "tracing", +] + +[[package]] +name = "fsqlite-ext-rtree" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b184933cb1c27b5031e1b468b5b9e0d2032820d55d31a78ae0c224077db3c87e" +dependencies = [ + "fsqlite-error", + "fsqlite-func", + "fsqlite-types", +] + +[[package]] +name = "fsqlite-func" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57f64b6e87f8ea966fb27d7f3814275596b3968660d631dac0912b1ccfa38df" +dependencies = [ + "chrono", + "fsqlite-error", + "fsqlite-types", + "tracing", +] + +[[package]] +name = "fsqlite-mvcc" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0017521cab8c7afa8b0db7f2ee15c6ab904b89262275e13596ea2bd0665b5fd" +dependencies = [ + "asupersync", + "blake3", + "crc32c", + "crossbeam-epoch", + "fsqlite-btree", + "fsqlite-error", + "fsqlite-observability", + "fsqlite-pager", + "fsqlite-types", + "fsqlite-vfs", + "fsqlite-wal", + "nix 0.29.0", + "parking_lot 0.12.5", + "serde", + "smallvec", + "tracing", + "xxhash-rust", +] + +[[package]] +name = "fsqlite-observability" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7145f65cdc441bf5c44744fc2c97a2efdf6c411396e4e17744170b7673d8ce" +dependencies = [ + "fsqlite-types", + "parking_lot 0.12.5", + "serde", + "tracing", +] + +[[package]] +name = "fsqlite-pager" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28feb4dcf7532187706fbab6e18ba13b0d3c94844940f52d900ca7b8566e4" +dependencies = [ + "argon2", + "bumpalo", + "chacha20poly1305", + "dashmap", + "foldhash 0.2.0", + "fsqlite-error", + "fsqlite-observability", + "fsqlite-types", + "fsqlite-vfs", + "fsqlite-wal", + "hashbrown 0.14.5", + "parking_lot 0.12.5", + "smallvec", + "tracing", + "xxhash-rust", +] + +[[package]] +name = "fsqlite-parser" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cccce297511923502e480978c2ccadbcd8fd71d6f3fa0f782f567e5f078276" +dependencies = [ + "fsqlite-ast", + "fsqlite-error", + "fsqlite-types", + "hashbrown 0.14.5", + "memchr", + "tracing", +] + +[[package]] +name = "fsqlite-planner" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +checksum = "f196ba3a6381373437adbdb648ce1176ac4acef0499fbc2201007532ac03f9a1" +dependencies = [ + "blake3", + "fsqlite-ast", + "fsqlite-error", + "fsqlite-parser", + "fsqlite-types", + "lru 0.16.3", + "serde", + "serde_json", + "tracing", + "xxhash-rust", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "fsqlite-types" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "e901ca6a6545b49888c99f1db57a304a9efb0d1ac1142c37dc8b2c948db536b6" dependencies = [ - "percent-encoding", + "asupersync", + "bitflags 2.13.0", + "blake3", + "fsqlite-error", + "memchr", + "parking_lot 0.12.5", + "serde", + "simdutf8", + "smallvec", + "tracing", + "xxhash-rust", ] [[package]] -name = "from_variant" -version = "3.0.0" +name = "fsqlite-vdbe" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ff35a391aef949120a0340d690269b3d9f63460a6106e99bd07b961f345ea9" +checksum = "b5b221d2fd41528be0609f6391277473ff694d80f87c857d91b2b1a8e18453b7" dependencies = [ - "swc_macros_common", - "syn 2.0.117", + "asupersync", + "crossbeam-deque", + "fsqlite-ast", + "fsqlite-btree", + "fsqlite-error", + "fsqlite-func", + "fsqlite-mvcc", + "fsqlite-pager", + "fsqlite-parser", + "fsqlite-types", + "fsqlite-wal", + "hashbrown 0.14.5", + "smallvec", + "tempfile", + "tracing", ] [[package]] -name = "fs2" -version = "0.4.3" +name = "fsqlite-vfs" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +checksum = "fa05027aeeb4fa7965d80045150e33ff6e078a9885eb516ae77e8e27666ca04e" dependencies = [ + "advisory-lock", + "asupersync", + "fsqlite-error", + "fsqlite-observability", + "fsqlite-types", "libc", - "winapi", + "nix 0.29.0", + "pollster 0.4.0", + "smallvec", + "tracing", ] [[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "fsevent-sys" -version = "4.1.0" +name = "fsqlite-wal" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +checksum = "94ff93545ba5ddefa7b5891ffada0e2f423460670087b8fb45835b5284a98661" dependencies = [ - "libc", + "asupersync", + "blake3", + "crc32c", + "fsqlite-error", + "fsqlite-types", + "fsqlite-vfs", + "parking_lot 0.12.5", + "serde", + "tracing", + "xxhash-rust", ] [[package]] @@ -3576,7 +4270,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "windows-link", ] @@ -3623,11 +4317,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "rand_core 0.10.1", "wasip2", "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -3640,6 +4346,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "git2" version = "0.20.4" @@ -3701,7 +4413,7 @@ dependencies = [ "parking_lot 0.12.5", "signal-hook 0.3.18", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3714,7 +4426,7 @@ dependencies = [ "gix-date", "gix-utils", "itoa", - "thiserror 2.0.17", + "thiserror 2.0.18", "winnow 0.7.15", ] @@ -3731,7 +4443,7 @@ dependencies = [ "gix-trace", "kstring", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-bom", ] @@ -3741,7 +4453,7 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e150161b8a75b5860521cb876b506879a3376d3adc857ec7a9d35e7c6a5e531" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3750,7 +4462,7 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c356b3825677cb6ff579551bb8311a81821e184453cbd105e2fc5311b288eeb" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3776,7 +4488,7 @@ dependencies = [ "gix-chunk", "gix-hash", "memmap2 0.9.9", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3794,7 +4506,7 @@ dependencies = [ "gix-sec", "memchr", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-bom", "winnow 0.7.15", ] @@ -3809,7 +4521,7 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3822,7 +4534,7 @@ dependencies = [ "itoa", "jiff", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3846,7 +4558,7 @@ dependencies = [ "gix-traverse", "gix-worktree", "imara-diff", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3866,7 +4578,7 @@ dependencies = [ "gix-trace", "gix-utils", "gix-worktree", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3882,7 +4594,7 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3898,7 +4610,7 @@ dependencies = [ "libc", "once_cell", "prodash", - "thiserror 2.0.17", + "thiserror 2.0.18", "walkdir", "zlib-rs", ] @@ -3921,7 +4633,7 @@ dependencies = [ "gix-trace", "gix-utils", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3935,7 +4647,7 @@ dependencies = [ "gix-features", "gix-path", "gix-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3959,7 +4671,7 @@ dependencies = [ "faster-hex", "gix-features", "sha1-checked", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4009,9 +4721,9 @@ dependencies = [ "itoa", "libc", "memmap2 0.9.9", - "rustix 1.1.3", + "rustix 1.1.4", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4022,7 +4734,7 @@ checksum = "115268ae5e3b3b7bc7fc77260eecee05acca458e45318ca45d35467fa81a3ac5" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4042,7 +4754,7 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", "winnow 0.7.15", ] @@ -4064,7 +4776,7 @@ dependencies = [ "gix-quote", "parking_lot 0.12.5", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4082,7 +4794,7 @@ dependencies = [ "gix-path", "memmap2 0.9.9", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4094,7 +4806,7 @@ dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4106,7 +4818,7 @@ dependencies = [ "bstr", "gix-trace", "gix-validate", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4121,7 +4833,7 @@ dependencies = [ "gix-config-value", "gix-glob", "gix-path", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4139,7 +4851,7 @@ dependencies = [ "gix-transport", "gix-utils", "maybe-async", - "thiserror 2.0.17", + "thiserror 2.0.18", "winnow 0.7.15", ] @@ -4151,7 +4863,7 @@ checksum = "e912ec04b7b1566a85ad486db0cab6b9955e3e32bcd3c3a734542ab3af084c5b" dependencies = [ "bstr", "gix-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4171,7 +4883,7 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2 0.9.9", - "thiserror 2.0.17", + "thiserror 2.0.18", "winnow 0.7.15", ] @@ -4187,7 +4899,7 @@ dependencies = [ "gix-revision", "gix-validate", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4205,7 +4917,7 @@ dependencies = [ "gix-object", "gix-revwalk", "gix-trace", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4220,7 +4932,7 @@ dependencies = [ "gix-hashtable", "gix-object", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4244,7 +4956,7 @@ dependencies = [ "bstr", "gix-hash", "gix-lock", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4267,7 +4979,7 @@ dependencies = [ "gix-pathspec", "gix-worktree", "portable-atomic", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4282,7 +4994,7 @@ dependencies = [ "gix-pathspec", "gix-refspec", "gix-url", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4319,7 +5031,7 @@ dependencies = [ "gix-quote", "gix-sec", "gix-url", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4336,7 +5048,7 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4349,7 +5061,7 @@ dependencies = [ "gix-features", "gix-path", "percent-encoding", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4370,7 +5082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b1e63a5b516e970a594f870ed4571a8fdcb8a344e7bd407a20db8bd61dbfde4" dependencies = [ "bstr", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -4426,7 +5138,7 @@ dependencies = [ "objc2 0.6.3", "objc2-app-kit", "once_cell", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.59.0", "x11rb", "xkeysym", @@ -4608,6 +5320,7 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", + "serde", ] [[package]] @@ -4632,6 +5345,17 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + [[package]] name = "hashline" version = "0.3.1" @@ -5204,7 +5928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85518b9086bf01117761b90e7691c0ef3236fa8adfb1fb44dd248fe5f87215d5" dependencies = [ "quantette", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -5335,6 +6059,19 @@ dependencies = [ "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width 0.2.2", + "unit-prefix", + "web-time 1.1.0", +] + [[package]] name = "indoc" version = "2.0.7" @@ -5425,6 +6162,17 @@ dependencies = [ "unic-langid", ] +[[package]] +name = "io-uring" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d09b98f7eace8982db770e4408e7470b028ce513ac28fecdc6bf4c30fe92b62" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "libc", +] + [[package]] name = "io_tee" version = "0.1.1" @@ -5487,6 +6235,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "is_executable" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" +dependencies = [ + "windows-sys 0.60.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -5636,7 +6393,7 @@ dependencies = [ "serde_json", "serde_yaml", "sha2 0.10.9", - "similar", + "similar 2.7.0", "strum 0.26.3", "tar", "tempfile", @@ -5762,7 +6519,7 @@ dependencies = [ "serde_json", "serde_yaml", "sha2 0.10.9", - "similar", + "similar 2.7.0", "tar", "tempfile", "thiserror 1.0.69", @@ -5809,6 +6566,7 @@ dependencies = [ "anyhow", "async-trait", "base64 0.22.1", + "beads_rust", "bytes", "chrono", "cross_agent_session_resumer", @@ -5827,6 +6585,7 @@ dependencies = [ "jcode-azure-auth", "jcode-background-types", "jcode-batch-types", + "jcode-beads-bridge", "jcode-best-of-n", "jcode-build-meta", "jcode-compaction-core", @@ -5856,7 +6615,6 @@ dependencies = [ "jcode-session-types", "jcode-side-panel-types", "jcode-storage", - "jcode-task-types", "jcode-telemetry-core", "jcode-terminal-image", "jcode-terminal-launch", @@ -5874,7 +6632,7 @@ dependencies = [ "serde_json", "serde_yaml", "sha2 0.10.9", - "similar", + "similar 2.7.0", "tempfile", "tikv-jemalloc-ctl", "tikv-jemalloc-sys", @@ -5898,6 +6656,19 @@ dependencies = [ "serde", ] +[[package]] +name = "jcode-beads-bridge" +version = "0.1.0" +dependencies = [ + "anyhow", + "beads_rust", + "chrono", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", +] + [[package]] name = "jcode-best-of-n" version = "0.1.0" @@ -5906,7 +6677,7 @@ dependencies = [ "dashmap", "serde", "serde_json", - "similar", + "similar 2.7.0", "thiserror 1.0.69", "tokio", "tracing", @@ -5970,8 +6741,8 @@ dependencies = [ "image", "jcode-tui-messages", "libc", - "pollster", - "pulldown-cmark", + "pollster 0.3.0", + "pulldown-cmark 0.12.2", "serde", "serde_json", "wgpu", @@ -5992,7 +6763,7 @@ dependencies = [ "regex", "serde", "serde_json", - "similar", + "similar 2.7.0", "tar", "tempfile", "tokio", @@ -6160,7 +6931,7 @@ dependencies = [ "clap", "jcode-mobile-core", "libc", - "pollster", + "pollster 0.3.0", "serde", "serde_json", "tempfile", @@ -6178,7 +6949,7 @@ dependencies = [ "imap", "lettre", "mail-parser", - "pulldown-cmark", + "pulldown-cmark 0.12.2", "urlencoding", ] @@ -6214,7 +6985,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -6237,7 +7008,7 @@ dependencies = [ "swc_ecma_parser", "swc_ecma_transforms_base", "swc_ecma_transforms_typescript", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -6426,7 +7197,7 @@ dependencies = [ name = "jcode-render-core" version = "0.1.0" dependencies = [ - "pulldown-cmark", + "pulldown-cmark 0.12.2", "serde", "unicode-width 0.2.2", ] @@ -6634,7 +7405,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "similar", + "similar 2.7.0", "tempfile", "tokio", "unicode-width 0.2.2", @@ -6672,7 +7443,7 @@ dependencies = [ "jcode-render-core", "jcode-tui-mermaid", "jcode-tui-workspace", - "pulldown-cmark", + "pulldown-cmark 0.12.2", "ratatui", "serde", "serde_json", @@ -6876,7 +7647,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.17", + "thiserror 2.0.18", "walkdir", "windows-link", ] @@ -6973,7 +7744,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -7110,9 +7881,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.180" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libgit2-sys" @@ -7152,6 +7923,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +[[package]] +name = "libmimalloc-sys" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" +dependencies = [ + "cc", +] + [[package]] name = "libredox" version = "0.1.12" @@ -7213,9 +7993,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "liquid" @@ -7360,7 +8140,7 @@ version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" dependencies = [ - "nix", + "nix 0.29.0", "winapi", ] @@ -7446,9 +8226,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memmap2" @@ -7511,7 +8291,7 @@ dependencies = [ "serde_yaml", "sha2 0.10.9", "signal-hook 0.3.18", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "unicode-script", @@ -7535,7 +8315,7 @@ dependencies = [ "resvg", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "ttf-parser 0.25.1", "usvg", ] @@ -7555,6 +8335,15 @@ dependencies = [ "paste", ] +[[package]] +name = "mimalloc" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" +dependencies = [ + "libmimalloc-sys", +] + [[package]] name = "mime" version = "0.3.17" @@ -7744,6 +8533,34 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nkeys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf" +dependencies = [ + "data-encoding", + "ed25519", + "ed25519-dalek", + "getrandom 0.2.16", + "log", + "rand 0.8.5", + "signatory", +] + [[package]] name = "nom" version = "7.1.3" @@ -7811,13 +8628,22 @@ dependencies = [ "bitflags 2.13.0", ] +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8237,7 +9063,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -8387,6 +9213,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -8431,6 +9268,15 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -8598,8 +9444,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", ] [[package]] @@ -8644,7 +9500,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.3", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -8654,6 +9510,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "poly1305" version = "0.8.0" @@ -8665,6 +9527,18 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "pom" version = "1.1.0" @@ -8827,7 +9701,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.11.9", +] + +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive 0.14.4", ] [[package]] @@ -8843,6 +9727,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "psm" version = "0.1.30" @@ -8866,6 +9763,19 @@ dependencies = [ "unicase", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +dependencies = [ + "bitflags 2.13.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + [[package]] name = "pulldown-cmark-escape" version = "0.11.0" @@ -8913,6 +9823,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.39.2" @@ -8936,7 +9855,7 @@ dependencies = [ "rustc-hash 2.1.2", "rustls 0.23.37", "socket2 0.6.1", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time 1.1.0", @@ -8958,7 +9877,7 @@ dependencies = [ "rustls 0.23.37", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time 1.1.0", @@ -9140,7 +10059,7 @@ dependencies = [ "kasuari", "lru 0.16.3", "strum 0.27.2", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", "unicode-width 0.2.2", @@ -9330,7 +10249,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -9491,8 +10410,10 @@ checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -9507,6 +10428,8 @@ dependencies = [ "rustls 0.23.37", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper 1.0.2", "tokio", "tokio-rustls 0.26.4", @@ -9564,6 +10487,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b19f5711867dc33a82cdbfd437c03b4089308f63a7ec3ee6ab34a9d74ff519" dependencies = [ + "backtrace", "bitflags 2.13.0", "crossterm", "fancy-regex 0.17.0", @@ -9572,7 +10496,10 @@ dependencies = [ "num-rational", "once_cell", "os_pipe", + "pulldown-cmark 0.13.4", "regex", + "serde", + "serde_json", "smallvec", "stdio-override", "time", @@ -9609,7 +10536,7 @@ dependencies = [ "schemars 1.2.1", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tokio-util", "tracing", @@ -9628,6 +10555,25 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -9691,7 +10637,7 @@ dependencies = [ "serde_json", "sha2 0.10.9", "toml 0.8.2", - "ureq", + "ureq 2.12.1", "walkdir", "which 8.0.3", ] @@ -9754,6 +10700,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -9804,15 +10756,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags 2.13.0", "errno", "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.52.0", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] @@ -9941,7 +10893,7 @@ dependencies = [ "security-framework 3.6.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -10184,9 +11136,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ "base16ct", - "der", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", "subtle", "zeroize", ] @@ -10251,6 +11203,17 @@ dependencies = [ "libc", ] +[[package]] +name = "self-replace" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ec815b5eab420ab893f63393878d89c90fdd94c0bcc44c07abb8ad95552fb7" +dependencies = [ + "fastrand", + "tempfile", + "windows-sys 0.52.0", +] + [[package]] name = "self_cell" version = "0.10.3" @@ -10266,6 +11229,32 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +[[package]] +name = "self_update" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e79722b5a505d4ddc77527455a97244e9e8c4c07533ff44cf4421cce7bb6d17" +dependencies = [ + "either", + "flate2", + "http 1.4.0", + "indicatif", + "log", + "quick-xml 0.38.4", + "regex", + "reqwest 0.13.4", + "self-replace", + "semver", + "serde", + "serde_json", + "tar", + "tempfile", + "ureq 3.3.0", + "urlencoding", + "zip", + "zipsign-api", +] + [[package]] name = "semver" version = "1.0.27" @@ -10337,6 +11326,19 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_norway" +version = "0.9.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e408f29489b5fd500fab51ff1484fc859bb655f32c671f307dcd733b72e8168c" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml-norway", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -10393,6 +11395,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha1-checked" version = "0.10.0" @@ -10400,7 +11413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89f599ac0c323ebb1c6082821a54962b839832b03984598375bff3975b804423" dependencies = [ "digest 0.10.7", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -10493,6 +11506,18 @@ dependencies = [ "libc", ] +[[package]] +name = "signatory" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" +dependencies = [ + "pkcs8 0.10.2", + "rand_core 0.6.4", + "signature 2.2.0", + "zeroize", +] + [[package]] name = "signature" version = "1.6.4" @@ -10503,6 +11528,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -10531,6 +11566,15 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "similar" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6505efef05804732ed8a3f2d4f279429eb485bd69d5b0cc6b19cc02005cda16" +dependencies = [ + "bstr", +] + [[package]] name = "simplecss" version = "0.2.2" @@ -10665,6 +11709,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "socks" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" +dependencies = [ + "byteorder", + "libc", + "winapi", +] + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -10681,7 +11736,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", ] [[package]] @@ -11059,7 +12124,7 @@ dependencies = [ "once_cell", "rustc-hash 2.1.2", "serde", - "sha1", + "sha1 0.10.6", "string_enum", "swc_atoms", "swc_common", @@ -11233,7 +12298,7 @@ dependencies = [ "regex-syntax", "serde", "serde_derive", - "thiserror 2.0.17", + "thiserror 2.0.18", "walkdir", ] @@ -11246,6 +12311,20 @@ dependencies = [ "libc", ] +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -11307,15 +12386,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", - "rustix 1.1.3", - "windows-sys 0.52.0", + "rustix 1.1.4", + "windows-sys 0.61.2", ] [[package]] @@ -11366,7 +12445,7 @@ dependencies = [ "libc", "log", "memmem", - "nix", + "nix 0.29.0", "num-derive", "num-traits", "ordered-float 4.6.0", @@ -11401,11 +12480,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -11421,9 +12500,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -11596,7 +12675,7 @@ dependencies = [ "serde", "serde_json", "spm_precompiled", - "thiserror 2.0.17", + "thiserror 2.0.18", "unicode-normalization-alignments", "unicode-segmentation", "unicode_categories", @@ -11842,7 +12921,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" dependencies = [ "crossbeam-channel", "symlink", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tracing-subscriber", ] @@ -11879,6 +12958,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -11889,12 +12978,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -12013,7 +13105,7 @@ dependencies = [ "log", "memmap2 0.9.9", "num-integer", - "prost", + "prost 0.11.9", "smallvec", "tract-hir", "tract-nnef", @@ -12235,6 +13327,24 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "tru" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e3a3ec4e4ded9246bfbf007bcea74355c322eb66f987cf338eeaeadffde0817" +dependencies = [ + "anyhow", + "chrono", + "clap", + "clap_complete", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", + "tracing-subscriber", + "vergen-gix", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -12277,7 +13387,7 @@ dependencies = [ "rand 0.8.5", "rustls 0.23.37", "rustls-pki-types", - "sha1", + "sha1 0.10.6", "thiserror 1.0.69", "utf-8", ] @@ -12371,7 +13481,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -12530,6 +13640,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + [[package]] name = "universal-hash" version = "0.5.1" @@ -12546,12 +13662,24 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unsafe-libyaml-norway" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39abd59bf32521c7f2301b52d05a6a2c975b6003521cbd0c6dc1582f0a22104" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty-next" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa66022bbd1ab992fad72bdedcfd07a0023b6f5ecc83d50121e39e3a3caed41" + [[package]] name = "ureq" version = "2.12.1" @@ -12568,6 +13696,40 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "cookie_store", + "encoding_rs", + "flate2", + "log", + "percent-encoding", + "rustls 0.23.37", + "rustls-pki-types", + "serde", + "serde_json", + "socks", + "ureq-proto", + "utf8-zero", + "webpki-roots 1.0.6", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http 1.4.0", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -12619,6 +13781,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -12705,6 +13873,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue-next" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5208a9710a6ca1c2da0d64289a5dcca13deb05afdedf651f4e11bcd9fc53eb" + [[package]] name = "vsimd" version = "0.8.0" @@ -12892,7 +14066,7 @@ checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.3", + "rustix 1.1.4", "scoped-tls", "smallvec", "wayland-sys", @@ -12905,7 +14079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ "bitflags 2.13.0", - "rustix 1.1.3", + "rustix 1.1.4", "wayland-backend", "wayland-scanner", ] @@ -12927,7 +14101,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" dependencies = [ - "rustix 1.1.3", + "rustix 1.1.4", "wayland-client", "xcursor", ] @@ -13002,7 +14176,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.39.2", "quote", ] @@ -13268,7 +14442,7 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" dependencies = [ "either", "env_home", - "rustix 1.1.3", + "rustix 1.1.4", "winsafe", ] @@ -13330,7 +14504,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -13349,6 +14523,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.58.0" @@ -13368,19 +14552,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.58.0", + "windows-interface 0.58.0", "windows-result 0.2.0", "windows-strings 0.1.0", "windows-targets 0.52.6", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-implement" version = "0.58.0" @@ -13392,6 +14599,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-interface" version = "0.58.0" @@ -13420,6 +14638,15 @@ dependencies = [ "windows-strings 0.5.1", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -13948,8 +15175,8 @@ dependencies = [ "libc", "log", "os_pipe", - "rustix 1.1.3", - "thiserror 2.0.17", + "rustix 1.1.4", + "thiserror 2.0.18", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -13994,7 +15221,7 @@ dependencies = [ "libc", "libloading 0.8.9", "once_cell", - "rustix 1.1.3", + "rustix 1.1.4", "x11rb-protocol", ] @@ -14023,7 +15250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.3", + "rustix 1.1.4", ] [[package]] @@ -14142,12 +15369,12 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.29.0", "ordered-stream", "rand 0.8.5", "serde", "serde_repr", - "sha1", + "sha1 0.10.6", "static_assertions", "tracing", "uds_windows", @@ -14283,6 +15510,32 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "time", + "zopfli", +] + +[[package]] +name = "zipsign-api" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba6063ff82cdbd9a765add16d369abe81e520f836054e997c2db217ceca40c0" +dependencies = [ + "base64 0.22.1", + "ed25519-dalek", + "thiserror 2.0.18", +] + [[package]] name = "zlib-rs" version = "0.5.5" @@ -14295,6 +15548,18 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 7e7cb21f5f..76f9f4194d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ members = [ "crates/jcode-plugin-core", "crates/jcode-plugin-runtime", "crates/jcode-render-core", + "crates/jcode-beads-bridge", "evals/jbench", "evals/jcode-edit-bench", @@ -103,6 +104,7 @@ ffs-engine = { git = "https://github.com/quangdang46/fast_file_search", rev = "2 ffs-symbol = { git = "https://github.com/quangdang46/fast_file_search", rev = "28aef7c" } strum = { version = "0.26", features = ["derive"] } regex = "1" +beads_rust = { path = "../beads_rust" } [lib] name = "jcode" diff --git a/crates/jcode-app-core/src/tool/beads.rs b/crates/jcode-app-core/src/tool/beads.rs new file mode 100644 index 0000000000..64b49c416f --- /dev/null +++ b/crates/jcode-app-core/src/tool/beads.rs @@ -0,0 +1,387 @@ +//! Beads-rs issue tracker tools for jcode agents. +//! +//! Tools: `beads_list`, `beads_create`, `beads_ready`, `beads_claim`, +//! `beads_close`, `beads_dep`. + +use super::{Tool, ToolContext, ToolOutput}; +use anyhow::{Context, Result}; +use async_trait::async_trait; +use serde::Deserialize; +use serde_json::{Value, json}; +use std::path::Path; + +// ─── BeadsListTool ───────────────────────────────────────────────────────── + +pub struct BeadsListTool; + +impl BeadsListTool { + pub fn new() -> Self { + Self + } +} + +#[derive(Deserialize)] +struct BeadsListInput { + #[serde(default)] + status: Option, + #[serde(default)] + limit: Option, + #[serde(default)] + label: Option, + #[serde(default)] + assignee: Option, + #[serde(default)] + sort: Option, +} + +#[async_trait] +impl Tool for BeadsListTool { + fn name(&self) -> &str { "beads_list" } + fn description(&self) -> &str { + "List beads issues with optional filters (status, label, assignee, limit)." + } + + fn parameters_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["open", "in_progress", "blocked", "closed", "all"], + "description": "Filter by status." + }, + "limit": { "type": "integer", "description": "Max results (default 50)." }, + "label": { "type": "string", "description": "Filter by label." }, + "assignee": { "type": "string", "description": "Filter by assignee." }, + "sort": { + "type": "string", + "enum": ["priority", "created", "updated"], + "description": "Sort order." + } + } + }) + } + + async fn execute(&self, input: Value, ctx: ToolContext) -> Result { + let params: BeadsListInput = serde_json::from_value(input)?; + let wd = ctx.working_dir.as_deref().unwrap_or_else(|| Path::new(".")); + let project = crate::beads::BeadsProject::open(wd) + .map_err(|e| anyhow::anyhow!("Failed to open beads project: {e}"))?; + + let mut filters = crate::beads::ListFilters::default(); + match params.status.as_deref() { + Some("open") => { filters.statuses = Some(vec![crate::beads::Status::Open]); } + Some("in_progress") => { filters.statuses = Some(vec![crate::beads::Status::InProgress]); } + Some("blocked") => { filters.statuses = Some(vec![crate::beads::Status::Blocked]); } + Some("closed") => { filters.include_closed = true; filters.statuses = Some(vec![crate::beads::Status::Closed]); } + Some("all") => { filters.include_closed = true; filters.include_deferred = true; } + _ => { + filters.statuses = Some(vec![ + crate::beads::Status::Open, crate::beads::Status::InProgress, crate::beads::Status::Blocked, + ]); + } + } + if let Some(label) = ¶ms.label { filters.labels = Some(vec![label.clone()]); } + if let Some(assignee) = ¶ms.assignee { filters.assignee = Some(assignee.clone()); } + if let Some(sort) = ¶ms.sort { filters.sort = Some(sort.clone()); } + filters.limit = params.limit.or(Some(50)); + + let issues = project.storage().list_issues(&filters)?; + let items: Vec = issues.into_iter().map(|i| json!({ + "id": i.id, "title": i.title, "status": i.status.as_str(), + "priority": i.priority.to_string(), "assignee": i.assignee, + "labels": i.labels, + "created_at": i.created_at.to_rfc3339(), + "updated_at": i.updated_at.to_rfc3339(), + })).collect(); + + Ok(ToolOutput::new(serde_json::to_string_pretty(&json!({"issues": items}))?) + .with_title(format!("Issues: {}", items.len()))) + } +} + +// ─── BeadsCreateTool ─────────────────────────────────────────────────────── + +pub struct BeadsCreateTool; + +impl BeadsCreateTool { + pub fn new() -> Self { Self } +} + +#[derive(Deserialize)] +struct BeadsCreateInput { + title: String, + #[serde(default)] description: Option, + #[serde(default)] priority: Option, + #[serde(default)] labels: Vec, + #[serde(default)] assignee: Option, + #[serde(default)] issue_type: Option, +} + +#[async_trait] +impl Tool for BeadsCreateTool { + fn name(&self) -> &str { "beads_create" } + fn description(&self) -> &str { "Create a new beads issue/task." } + + fn parameters_schema(&self) -> Value { + json!({ + "type": "object", "required": ["title"], + "properties": { + "title": { "type": "string", "description": "Issue title." }, + "description": { "type": "string" }, + "priority": { + "type": "integer", + "description": "0=critical, 1=high, 2=medium, 3=low, 4=backlog", + "default": 2 + }, + "labels": { "type": "array", "items": { "type": "string" } }, + "assignee": { "type": "string" }, + "issue_type": { + "type": "string", "enum": ["task", "bug", "feature", "epic", "chore"] + } + } + }) + } + + async fn execute(&self, input: Value, ctx: ToolContext) -> Result { + let params: BeadsCreateInput = serde_json::from_value(input)?; + let wd = ctx.working_dir.as_deref().unwrap_or_else(|| Path::new(".")); + let project = crate::beads::BeadsProject::open_or_init(wd, "bead") + .map_err(|e| anyhow::anyhow!("Failed to open beads project: {e}"))?; + + let priority = crate::beads::Priority(params.priority.unwrap_or(2).clamp(0, 4)); + let issue_type = match params.issue_type.as_deref() { + Some("bug") => crate::beads::IssueType::Bug, + Some("feature") => crate::beads::IssueType::Feature, + Some("epic") => crate::beads::IssueType::Epic, + Some("chore") => crate::beads::IssueType::Chore, + _ => crate::beads::IssueType::Task, + }; + + let id = format!("bead-{}", short_id()); + let now = chrono::Utc::now(); + let issue = crate::beads::Issue { + id, title: params.title, description: params.description, + priority, issue_type, labels: params.labels, + assignee: params.assignee, status: crate::beads::Status::Open, + created_at: now, updated_at: now, + ..default_issue() + }; + project.storage_mut().create_issue(&issue, &ctx.session_id) + .context("Failed to create issue")?; + project.flush()?; + Ok(ToolOutput::new(format!("Created issue `{}`", issue.id)) + .with_title(issue.id.clone()) + .with_metadata(json!({"id": issue.id}))) + } +} + +// ─── BeadsReadyTool ──────────────────────────────────────────────────────── + +pub struct BeadsReadyTool; + +impl BeadsReadyTool { + pub fn new() -> Self { Self } +} + +#[derive(Deserialize)] +struct BeadsReadyInput { + #[serde(default = "default_ready_limit")] + limit: usize, +} +fn default_ready_limit() -> usize { 10 } + +#[async_trait] +impl Tool for BeadsReadyTool { + fn name(&self) -> &str { "beads_ready" } + fn description(&self) -> &str { + "Show beads issues ready-to-work (no blockers, highest priority first)." + } + + fn parameters_schema(&self) -> Value { + json!({ "type": "object", "properties": { + "limit": { "type": "integer", "description": "Max results (default 10)." } + }}) + } + + async fn execute(&self, input: Value, ctx: ToolContext) -> Result { + let params: BeadsReadyInput = serde_json::from_value(input)?; + let wd = ctx.working_dir.as_deref().unwrap_or_else(|| Path::new(".")); + let project = crate::beads::BeadsProject::open(wd) + .map_err(|e| anyhow::anyhow!("Failed to open beads project: {e}"))?; + + let manager = crate::beads::BeadsTaskManager::new(&project); + let ready = manager.ready_tasks(params.limit)?; + let items: Vec = ready.into_iter().map(|i| json!({ + "id": i.id, "title": i.title, "priority": i.priority.to_string(), + "assignee": i.assignee, "labels": i.labels, + "created_at": i.created_at.to_rfc3339(), + })).collect(); + + Ok(ToolOutput::new(serde_json::to_string_pretty(&json!({"ready": items}))?) + .with_title(format!("Ready: {} items", items.len()))) + } +} + +// ─── BeadsClaimTool ──────────────────────────────────────────────────────── + +pub struct BeadsClaimTool; + +impl BeadsClaimTool { + pub fn new() -> Self { Self } +} + +#[derive(Deserialize)] +struct BeadsClaimInput { id: String } + +#[async_trait] +impl Tool for BeadsClaimTool { + fn name(&self) -> &str { "beads_claim" } + fn description(&self) -> &str { + "Claim a beads issue (set status to in_progress)." + } + + fn parameters_schema(&self) -> Value { + json!({ "type": "object", "required": ["id"], "properties": { + "id": { "type": "string", "description": "Issue ID to claim." } + }}) + } + + async fn execute(&self, input: Value, ctx: ToolContext) -> Result { + let params: BeadsClaimInput = serde_json::from_value(input)?; + let wd = ctx.working_dir.as_deref().unwrap_or_else(|| Path::new(".")); + let project = crate::beads::BeadsProject::open(wd) + .map_err(|e| anyhow::anyhow!("Failed to open beads project: {e}"))?; + + let manager = crate::beads::BeadsTaskManager::new(&project); + let issue = manager.set_status(¶ms.id, crate::beads::Status::InProgress, &ctx.session_id) + .map_err(|e| anyhow::anyhow!("Failed to claim issue: {e}"))?; + + Ok(ToolOutput::new(format!("Claimed issue `{}`", issue.id)) + .with_title(issue.title) + .with_metadata(json!({"id": issue.id, "status": "in_progress"}))) + } +} + +// ─── BeadsCloseTool ──────────────────────────────────────────────────────── + +pub struct BeadsCloseTool; + +impl BeadsCloseTool { + pub fn new() -> Self { Self } +} + +#[derive(Deserialize)] +struct BeadsCloseInput { + id: String, + #[serde(default)] reason: String, +} + +#[async_trait] +impl Tool for BeadsCloseTool { + fn name(&self) -> &str { "beads_close" } + fn description(&self) -> &str { "Close a beads issue with an optional reason." } + + fn parameters_schema(&self) -> Value { + json!({ "type": "object", "required": ["id"], "properties": { + "id": { "type": "string", "description": "Issue ID to close." }, + "reason": { "type": "string", "description": "Reason for closing." } + }}) + } + + async fn execute(&self, input: Value, ctx: ToolContext) -> Result { + let params: BeadsCloseInput = serde_json::from_value(input)?; + let wd = ctx.working_dir.as_deref().unwrap_or_else(|| Path::new(".")); + let project = crate::beads::BeadsProject::open(wd) + .map_err(|e| anyhow::anyhow!("Failed to open beads project: {e}"))?; + + let manager = crate::beads::BeadsTaskManager::new(&project); + let issue = manager.close_task(¶ms.id, ¶ms.reason, &ctx.session_id)?; + + Ok(ToolOutput::new(format!("Closed issue `{}`", issue.id)) + .with_title(issue.title) + .with_metadata(json!({"id": issue.id, "status": "closed"}))) + } +} + +// ─── BeadsDepTool ────────────────────────────────────────────────────────── + +pub struct BeadsDepTool; + +impl BeadsDepTool { + pub fn new() -> Self { Self } +} + +#[derive(Deserialize)] +struct BeadsDepInput { + action: String, + issue: String, + depends_on: String, +} + +#[async_trait] +impl Tool for BeadsDepTool { + fn name(&self) -> &str { "beads_dep" } + fn description(&self) -> &str { + "Add or remove a beads dependency. 'issue' blocks on 'depends_on'." + } + + fn parameters_schema(&self) -> Value { + json!({ "type": "object", "required": ["action", "issue", "depends_on"], "properties": { + "action": { "type": "string", "enum": ["add", "remove"] }, + "issue": { "type": "string", "description": "Issue that blocks." }, + "depends_on": { "type": "string", "description": "Issue that is blocked by." } + }}) + } + + async fn execute(&self, input: Value, ctx: ToolContext) -> Result { + let params: BeadsDepInput = serde_json::from_value(input)?; + let wd = ctx.working_dir.as_deref().unwrap_or_else(|| Path::new(".")); + let project = crate::beads::BeadsProject::open(wd) + .map_err(|e| anyhow::anyhow!("Failed to open beads project: {e}"))?; + + let manager = crate::beads::BeadsTaskManager::new(&project); + match params.action.as_str() { + "add" => { + manager.add_dependency(¶ms.issue, ¶ms.depends_on, &ctx.session_id)?; + Ok(ToolOutput::new(format!("Added dep: {} blocks on {}", params.issue, params.depends_on))) + } + "remove" => { + manager.remove_dependency(¶ms.issue, ¶ms.depends_on, &ctx.session_id)?; + Ok(ToolOutput::new(format!("Removed dep: {} → {}", params.issue, params.depends_on))) + } + _ => Err(anyhow::anyhow!("Unknown action: {}", params.action)), + } + } +} + +// ─── Helpers ─────────────────────────────────────────────────────────────── + +fn short_id() -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos(); + format!("{:x}", nanos & 0xFFFF_FFFF) +} + +fn default_issue() -> crate::beads::Issue { + let now = chrono::Utc::now(); + crate::beads::Issue { + id: String::new(), content_hash: None, title: String::new(), + description: None, design: None, acceptance_criteria: None, notes: None, + status: crate::beads::Status::Open, + priority: crate::beads::Priority(2), + issue_type: crate::beads::IssueType::Task, + assignee: None, owner: None, estimated_minutes: None, + created_at: now, created_by: None, updated_at: now, + closed_at: None, close_reason: None, closed_by_session: None, + due_at: None, defer_until: None, external_ref: None, + source_system: Some("jcode".to_string()), + source_repo: None, source_repo_path: None, agent_context: None, + labels: Vec::new(), + deleted_at: None, deleted_by: None, delete_reason: None, + original_type: None, compaction_level: None, compacted_at: None, + compacted_at_commit: None, original_size: None, + sender: None, ephemeral: false, pinned: false, is_template: false, + dependencies: Vec::new(), comments: Vec::new(), + } +} diff --git a/crates/jcode-app-core/src/tool/mod.rs b/crates/jcode-app-core/src/tool/mod.rs index b03887dcdf..73976af7e0 100644 --- a/crates/jcode-app-core/src/tool/mod.rs +++ b/crates/jcode-app-core/src/tool/mod.rs @@ -21,6 +21,8 @@ mod ffs_multi_grep; mod ffs_outline; mod ffs_symbol; mod gmail; +mod beads; + mod goal; mod hashline_edit; mod invalid; @@ -383,6 +385,12 @@ impl Registry { "initiative", goal::InitiativeTool::new, ); + Self::insert_tool_timed(&mut m, &mut timings, "beads_list", beads::BeadsListTool::new); + Self::insert_tool_timed(&mut m, &mut timings, "beads_create", beads::BeadsCreateTool::new); + Self::insert_tool_timed(&mut m, &mut timings, "beads_ready", beads::BeadsReadyTool::new); + Self::insert_tool_timed(&mut m, &mut timings, "beads_claim", beads::BeadsClaimTool::new); + Self::insert_tool_timed(&mut m, &mut timings, "beads_close", beads::BeadsCloseTool::new); + Self::insert_tool_timed(&mut m, &mut timings, "beads_dep", beads::BeadsDepTool::new); Self::insert_tool_timed(&mut m, &mut timings, "gmail", gmail::GmailTool::new); Self::insert_tool_timed( &mut m, diff --git a/crates/jcode-base/Cargo.toml b/crates/jcode-base/Cargo.toml index 0e850e6264..a6b69c872e 100644 --- a/crates/jcode-base/Cargo.toml +++ b/crates/jcode-base/Cargo.toml @@ -115,7 +115,8 @@ jcode-selfdev-types = { path = "../jcode-selfdev-types" } jcode-session-types = { path = "../jcode-session-types" } jcode-storage = { path = "../jcode-storage" } jcode-redact = { path = "../jcode-redact" } -jcode-task-types = { path = "../jcode-task-types" } +jcode-beads-bridge = { path = "../jcode-beads-bridge" } +beads_rust = { path = "../../../beads_rust" } jcode-tool-core = { path = "../jcode-tool-core" } jcode-tool-types = { path = "../jcode-tool-types" } jcode-side-panel-types = { path = "../jcode-side-panel-types" } diff --git a/crates/jcode-base/src/beads.rs b/crates/jcode-base/src/beads.rs new file mode 100644 index 0000000000..ac46d36762 --- /dev/null +++ b/crates/jcode-base/src/beads.rs @@ -0,0 +1,7 @@ +//! Beads-rs integration for jcode. +//! +//! This module re-exports the `jcode-beads-bridge` crate so that jcode code can +//! access beads_rust functionality via `crate::beads::*` instead of importing +//! `jcode_beads_bridge` directly. + +pub use jcode_beads_bridge::*; diff --git a/crates/jcode-base/src/goal.rs b/crates/jcode-base/src/goal.rs index f9e10834b1..82924f7204 100644 --- a/crates/jcode-base/src/goal.rs +++ b/crates/jcode-base/src/goal.rs @@ -1,11 +1,80 @@ +//! Goal management backed by beads_rust Epic issues. + use anyhow::{Context, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use std::hash::{Hash, Hasher}; -use std::path::{Path, PathBuf}; +use std::path::Path; + +use crate::side_panel::{SidePanelSnapshot, snapshot_for_session, write_markdown_page, focus_page}; + +// Re-export beads-backed types +pub use jcode_beads_bridge::mapping::{Goal, GoalMilestone, GoalStep, ToBeadsEpic, ToJcodeGoal}; -pub use jcode_task_types::{Goal, GoalMilestone, GoalScope, GoalStatus, GoalStep, GoalUpdate}; +/// Goal status enum. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum GoalStatus { + Draft, + #[default] + Active, + Paused, + Blocked, + Completed, + Archived, + Abandoned, +} + +impl GoalStatus { + pub fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "draft" => Some(Self::Draft), + "active" => Some(Self::Active), + "paused" => Some(Self::Paused), + "blocked" => Some(Self::Blocked), + "completed" => Some(Self::Completed), + "archived" => Some(Self::Archived), + "abandoned" => Some(Self::Abandoned), + _ => None, + } + } + pub fn as_str(&self) -> &'static str { + match self { + Self::Draft => "draft", + Self::Active => "active", + Self::Paused => "paused", + Self::Blocked => "blocked", + Self::Completed => "completed", + Self::Archived => "archived", + Self::Abandoned => "abandoned", + } + } + + pub fn sort_rank(self) -> u8 { + match self { + Self::Active => 0, + Self::Blocked => 1, + Self::Draft => 2, + Self::Paused => 3, + Self::Completed => 4, + Self::Archived => 5, + Self::Abandoned => 6, + } + } + + pub fn is_resumable(self) -> bool { + matches!(self, Self::Active | Self::Blocked | Self::Draft) + } +} + +/// Goal update record. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct GoalUpdate { + pub at: DateTime, + pub summary: String, +} + +/// Goal display mode. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GoalDisplayMode { Auto, @@ -26,6 +95,7 @@ impl GoalDisplayMode { } } +/// Goal creation input. #[derive(Debug, Clone, Default)] pub struct GoalCreateInput { pub id: Option, @@ -41,6 +111,7 @@ pub struct GoalCreateInput { pub progress_percent: Option, } +/// Goal update input. #[derive(Debug, Clone, Default)] pub struct GoalUpdateInput { pub title: Option, @@ -56,302 +127,241 @@ pub struct GoalUpdateInput { pub checkpoint_summary: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -struct GoalAttachment { - goal_id: String, - scope: GoalScope, - #[serde(default, skip_serializing_if = "Option::is_none")] - project_hash: Option, - title: String, - attached_at: DateTime, +/// Goal scope. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum GoalScope { + Global, + #[default] + Project, } +impl GoalScope { + pub fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "global" => Some(Self::Global), + "project" => Some(Self::Project), + _ => None, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + Self::Global => "global", + Self::Project => "project", + } + } +} + +/// Display result for a goal. #[derive(Debug, Clone)] pub struct GoalDisplayResult { pub goal: Goal, - pub snapshot: crate::side_panel::SidePanelSnapshot, + pub snapshot: SidePanelSnapshot, +} + +// ─── Beads-backed operations ─────────────────────────────────────────────── + +fn open_beads(working_dir: Option<&Path>) -> Result { + let wd = match working_dir { + Some(p) => p.to_path_buf(), + None => std::env::current_dir().context("no cwd")?, + }; + jcode_beads_bridge::BeadsProject::open(&wd) + .map_err(|e| anyhow::anyhow!("open beads project: {e}")) } +/// Create a goal (Epic issue) in beads_rust storage. pub fn create_goal(input: GoalCreateInput, working_dir: Option<&Path>) -> Result { - if input.title.trim().is_empty() { - anyhow::bail!("goal title cannot be empty"); - } - let mut goal = Goal::new(&input.title, input.scope); - if let Some(id) = input.id.as_deref().map(str::trim).filter(|s| !s.is_empty()) { - goal.id = jcode_task_types::sanitize_goal_id(id); - } - goal.id = next_available_goal_id(&goal.id, goal.scope, working_dir)?; - goal.description = input.description.unwrap_or_default().trim().to_string(); - goal.why = input.why.unwrap_or_default().trim().to_string(); - goal.success_criteria = trim_vec(input.success_criteria); - goal.milestones = input.milestones; - goal.next_steps = trim_vec(input.next_steps); - goal.blockers = trim_vec(input.blockers); - goal.current_milestone_id = input.current_milestone_id; - goal.progress_percent = input.progress_percent.map(|p| p.min(100)); - goal.updated_at = Utc::now(); - save_goal(&goal, working_dir)?; - sync_goal_memory(&goal, working_dir)?; + let project = open_beads(working_dir)?; + let id = input.id.clone().unwrap_or_else(|| format!("goal-{}", short_id())); + + let goal = Goal { + id, + title: input.title, + scope: GoalScope::Project.as_str().to_string(), + status: GoalStatus::Active.as_str().to_string(), + description: input.description.unwrap_or_default(), + why: input.why.unwrap_or_default(), + milestones: input.milestones, + next_steps: input.next_steps, + blockers: input.blockers, + progress_percent: input.progress_percent, + created_at: chrono::Utc::now(), + updated_at: chrono::Utc::now(), + }; + + let issue = goal.to_epic(); + project.storage_mut().create_issue(&issue, "jcode")?; + project.flush()?; Ok(goal) } +/// Update a goal. pub fn update_goal( id: &str, - scope_hint: Option, + _scope_hint: Option, working_dir: Option<&Path>, - update: GoalUpdateInput, + input: GoalUpdateInput, ) -> Result> { - let Some(mut goal) = load_goal(id, scope_hint, working_dir)? else { - return Ok(None); - }; - - if let Some(title) = update - .title - .as_deref() - .map(str::trim) - .filter(|s| !s.is_empty()) - { - goal.title = title.to_string(); - } - if let Some(description) = update.description { - goal.description = description.trim().to_string(); - } - if let Some(why) = update.why { - goal.why = why.trim().to_string(); - } - if let Some(status) = update.status { - goal.status = status; - } - if let Some(criteria) = update.success_criteria { - goal.success_criteria = trim_vec(criteria); - } - if let Some(milestones) = update.milestones { - goal.milestones = milestones; - } - if let Some(next_steps) = update.next_steps { - goal.next_steps = trim_vec(next_steps); - } - if let Some(blockers) = update.blockers { - goal.blockers = trim_vec(blockers); - } - if let Some(current_milestone_id) = update.current_milestone_id { - goal.current_milestone_id = current_milestone_id.map(|s| s.trim().to_string()); - } - if let Some(progress_percent) = update.progress_percent { - goal.progress_percent = progress_percent.map(|p| p.min(100)); + let project = open_beads(working_dir)?; + use beads_rust::storage::sqlite::IssueUpdate; + let mut update = IssueUpdate::default(); + if let Some(title) = &input.title { + update.title = Some(title.clone()); + } + if let Some(status) = input.status { + let s = match status { + GoalStatus::Active => beads_rust::model::Status::InProgress, + GoalStatus::Blocked => beads_rust::model::Status::Blocked, + GoalStatus::Completed => beads_rust::model::Status::Closed, + GoalStatus::Draft => beads_rust::model::Status::Draft, + GoalStatus::Paused | GoalStatus::Archived | GoalStatus::Abandoned => { + beads_rust::model::Status::Deferred + } + }; + update.status = Some(s); } - if let Some(summary) = update - .checkpoint_summary - .as_deref() - .map(str::trim) - .filter(|s| !s.is_empty()) - { - goal.updates.push(GoalUpdate { - at: Utc::now(), - summary: summary.to_string(), - }); + if let Some(desc) = &input.description { + update.description = Some(Some(desc.clone())); } - goal.updated_at = Utc::now(); - save_goal(&goal, working_dir)?; - sync_goal_memory(&goal, working_dir)?; - Ok(Some(goal)) + project.storage_mut().update_issue(id, &update, "jcode")?; + project.flush()?; + load_goal(id, None, working_dir) } +/// Load a single goal from beads_rust storage. pub fn load_goal( id: &str, - scope_hint: Option, + _scope_hint: Option, working_dir: Option<&Path>, ) -> Result> { - let id = jcode_task_types::sanitize_goal_id(id); - let mut candidates = Vec::new(); - match scope_hint { - Some(GoalScope::Global) => candidates.push(goal_file_in_dir(&global_goals_dir()?, &id)), - Some(GoalScope::Project) => { - if let Some(dir) = project_goals_dir(working_dir)? { - candidates.push(goal_file_in_dir(&dir, &id)); - } - } - None => { - if let Some(dir) = project_goals_dir(working_dir)? { - candidates.push(goal_file_in_dir(&dir, &id)); - } - candidates.push(goal_file_in_dir(&global_goals_dir()?, &id)); - } - } - - for path in candidates { - if path.exists() { - let goal: Goal = crate::storage::read_json(&path) - .with_context(|| format!("failed to read goal {}", path.display()))?; - return Ok(Some(goal)); - } - } - Ok(None) + let project = match open_beads(working_dir) { + Ok(p) => p, + Err(_) => return Ok(None), + }; + Ok(project.storage().get_issue(id).ok().flatten().map(|i| i.to_goal())) } +/// List all relevant goals (Epic issues). pub fn list_relevant_goals(working_dir: Option<&Path>) -> Result> { - let mut goals = load_goals_in_dir(&global_goals_dir()?)?; - if let Some(project_dir) = project_goals_dir(working_dir)? { - goals.extend(load_goals_in_dir(&project_dir)?); - } - sort_goals(&mut goals); - Ok(goals) + let project = open_beads(working_dir)?; + let filters = beads_rust::storage::ListFilters { + types: Some(vec![beads_rust::model::IssueType::Epic]), + ..Default::default() + }; + let issues = project.storage().list_issues(&filters)?; + Ok(issues.into_iter().map(|i| i.to_goal()).collect()) } +/// Resume a goal for a session. pub fn resume_goal(session_id: &str, working_dir: Option<&Path>) -> Result> { - if let Some(goal) = load_attached_goal(session_id, working_dir)? - && goal.status.is_resumable() - { - return Ok(Some(goal)); - } - - let mut goals = list_relevant_goals(working_dir)?; - goals.retain(|goal| goal.status.is_resumable()); - Ok(goals.into_iter().next()) + let goals = list_relevant_goals(working_dir)?; + Ok(goals.into_iter().find(|g| g.id == session_id || g.title.contains(session_id))) } +/// Attach a goal to a session (adds a label). pub fn attach_goal_to_session( session_id: &str, goal: &Goal, working_dir: Option<&Path>, ) -> Result<()> { - let attachment = GoalAttachment { - goal_id: goal.id.clone(), - scope: goal.scope, - project_hash: if goal.scope == GoalScope::Project { - Some(project_hash(working_dir.ok_or_else(|| { - anyhow::anyhow!("working_dir required for project goal") - })?)) - } else { - None - }, - title: goal.title.clone(), - attached_at: Utc::now(), - }; - let path = session_attachment_path(session_id)?; - crate::storage::write_json_fast(&path, &attachment) + let project = open_beads(working_dir)?; + project.storage_mut().add_label(&goal.id, &format!("session:{session_id}"), "jcode")?; + project.flush()?; + Ok(()) } +/// Load the goal attached to a session. pub fn load_attached_goal(session_id: &str, working_dir: Option<&Path>) -> Result> { - let path = session_attachment_path(session_id)?; - if !path.exists() { - return Ok(None); - } - let attachment: GoalAttachment = crate::storage::read_json(&path)?; - if attachment.scope == GoalScope::Project { - let Some(dir) = working_dir else { - return Ok(None); - }; - let current_hash = project_hash(dir); - if attachment.project_hash.as_deref() != Some(current_hash.as_str()) { - return Ok(None); - } - } - load_goal(&attachment.goal_id, Some(attachment.scope), working_dir) + let project = open_beads(working_dir)?; + let issues = project.storage().list_issues(&beads_rust::storage::ListFilters { + types: Some(vec![beads_rust::model::IssueType::Epic]), + labels: Some(vec![format!("session:{session_id}")]), + ..Default::default() + })?; + Ok(issues.into_iter().next().map(|i| i.to_goal())) } +// ─── Side-panel / UI helpers (beads-aware) ───────────────────────────────── + +/// Open goals overview in side panel. pub fn open_goals_overview_for_session( session_id: &str, working_dir: Option<&Path>, focus: bool, -) -> Result { +) -> Result { let goals = list_relevant_goals(working_dir)?; - crate::side_panel::write_markdown_page( - session_id, - "goals", - Some("Goals"), - &render_goals_overview(&goals), - focus, - ) + let content = format_goals_overview(&goals); + let page_id = "goals"; + write_markdown_page(session_id, page_id, Some("Goals (beads)"), &content, focus)?; + if focus { + focus_page(session_id, page_id) + } else { + snapshot_for_session(session_id) + } } +/// Refresh goals overview in side panel. pub fn refresh_goals_overview_for_session( session_id: &str, working_dir: Option<&Path>, -) -> Result> { - let snapshot = crate::side_panel::snapshot_for_session(session_id)?; - if !snapshot.pages.iter().any(|page| page.id == "goals") { - return Ok(None); +) -> Result> { + let snapshot = snapshot_for_session(session_id)?; + if snapshot.pages.iter().any(|page| page.id == "goals") { + open_goals_overview_for_session(session_id, working_dir, false).map(Some) + } else { + Ok(None) } +} - let focus = snapshot.focused_page_id.as_deref() == Some("goals"); - Ok(Some(open_goals_overview_for_session( - session_id, - working_dir, - focus, - )?)) +/// Write goal detail page to side panel. +pub fn write_goal_page( + session_id: &str, + working_dir: Option<&Path>, + goal: &Goal, + display: GoalDisplayMode, +) -> Result { + let _ = (working_dir, display); + let content = format_goal_detail(goal); + let page_id = goal_page_id(&goal.id); + write_markdown_page(session_id, &page_id, Some(&format!("Epic: {}", goal.title)), &content, true)?; + focus_page(session_id, &page_id) } +/// Open a goal detail in the side panel. pub fn open_goal_for_session( session_id: &str, working_dir: Option<&Path>, id: &str, explicit_focus: bool, ) -> Result> { - let Some(goal) = load_goal(id, None, working_dir)? else { - return Ok(None); + let goal = match load_goal(id, None, working_dir)? { + Some(g) => g, + None => return Ok(None), }; - let snapshot = write_goal_page( - session_id, - working_dir, - &goal, - if explicit_focus { - GoalDisplayMode::Focus - } else { - GoalDisplayMode::Auto - }, - )?; + let snapshot = write_goal_page(session_id, working_dir, &goal, + if explicit_focus { GoalDisplayMode::Focus } else { GoalDisplayMode::Auto })?; Ok(Some(GoalDisplayResult { goal, snapshot })) } +/// Resume a goal and show it in the side panel. pub fn resume_goal_for_session( session_id: &str, working_dir: Option<&Path>, explicit_focus: bool, ) -> Result> { - let Some(goal) = resume_goal(session_id, working_dir)? else { - return Ok(None); + let goal = match resume_goal(session_id, working_dir)? { + Some(g) => g, + None => return Ok(None), }; - let snapshot = write_goal_page( - session_id, - working_dir, - &goal, - if explicit_focus { - GoalDisplayMode::Focus - } else { - GoalDisplayMode::Auto - }, - )?; + let snapshot = write_goal_page(session_id, working_dir, &goal, + if explicit_focus { GoalDisplayMode::Focus } else { GoalDisplayMode::Auto })?; Ok(Some(GoalDisplayResult { goal, snapshot })) } -pub fn write_goal_page( - session_id: &str, - working_dir: Option<&Path>, - goal: &Goal, - display: GoalDisplayMode, -) -> Result { - let page_id = goal_page_id(&goal.id); - let page_title = format!("Goal: {}", goal.title); - let focus = match display { - GoalDisplayMode::None => false, - GoalDisplayMode::Focus => true, - GoalDisplayMode::UpdateOnly => false, - GoalDisplayMode::Auto => should_focus_goal_page(session_id, &page_id)?, - }; - let snapshot = crate::side_panel::write_markdown_page( - session_id, - &page_id, - Some(&page_title), - &render_goal_detail(goal), - focus, - )?; - attach_goal_to_session(session_id, goal, working_dir)?; - Ok(snapshot) -} - pub fn goal_page_id(id: &str) -> String { - format!("goal.{}", jcode_task_types::sanitize_goal_id(id)) + format!("goal-{id}") } pub fn header_badge( @@ -359,356 +369,60 @@ pub fn header_badge( snapshot: &crate::side_panel::SidePanelSnapshot, ) -> Option { if let Some(page) = snapshot.focused_page() - && page.id.starts_with("goal.") + && page.id.starts_with("goal-") { - return Some(format!("🎯 {}*", truncate_title(&page.title, 28))); + return Some(format!("🎯 beads:{}", page.title)); } - let goals = list_relevant_goals(working_dir).ok()?; - let active: Vec<_> = goals - .into_iter() - .filter(|goal| { - matches!( - goal.status, - GoalStatus::Active | GoalStatus::Blocked | GoalStatus::Draft - ) - }) - .collect(); - match active.as_slice() { - [] => None, - [goal] => Some(format!("🎯 {}", truncate_title(&goal.title, 28))), - many => Some(format!("🎯 {} active", many.len())), - } -} - -pub fn render_goals_overview(goals: &[Goal]) -> String { - let mut out = String::from("# Goals\n\n"); if goals.is_empty() { - out.push_str( - "No goals yet. Use the `goal` tool or `/goals show ` after creating one.\n", - ); - return out; - } - - for goal in goals { - out.push_str(&format!( - "## {}\n- Status: {}\n- Scope: {}\n", - goal.title, - goal.status.as_str(), - goal.scope.as_str() - )); - if let Some(progress) = goal.progress_percent { - out.push_str(&format!("- Progress: {}%\n", progress)); - } - if let Some(milestone) = goal.current_milestone() { - out.push_str(&format!("- Current milestone: {}\n", milestone.title)); - } - if let Some(next_step) = goal.next_steps.first() { - out.push_str(&format!("- Next step: {}\n", next_step)); - } - out.push_str(&format!("- Id: `{}`\n\n", goal.id)); - } - out -} - -pub fn render_goal_detail(goal: &Goal) -> String { - let mut out = format!( - "# Goal: {}\n\n**Status:** {} \n**Scope:** {} \n**Updated:** {} \n", - goal.title, - goal.status.as_str(), - goal.scope.as_str(), - goal.updated_at.format("%Y-%m-%d %H:%M") - ); - if let Some(progress) = goal.progress_percent { - out.push_str(&format!("**Progress:** {}% \n", progress)); + return None; } - out.push('\n'); - - if !goal.description.trim().is_empty() { - out.push_str("## Description\n"); - out.push_str(goal.description.trim()); - out.push_str("\n\n"); - } - if !goal.why.trim().is_empty() { - out.push_str("## Why\n"); - out.push_str(goal.why.trim()); - out.push_str("\n\n"); - } - if !goal.success_criteria.is_empty() { - out.push_str("## Success criteria\n"); - for item in &goal.success_criteria { - out.push_str(&format!("- {}\n", item)); - } - out.push('\n'); - } - if let Some(milestone) = goal.current_milestone() { - out.push_str(&format!("## Current milestone\n### {}\n", milestone.title)); - if milestone.steps.is_empty() { - out.push_str(&format!("- Status: {}\n\n", milestone.status)); - } else { - for step in &milestone.steps { - let checked = if step.status == "completed" { "x" } else { " " }; - out.push_str(&format!("- [{}] {}\n", checked, step.content)); - } - out.push('\n'); - } - } - if !goal.milestones.is_empty() { - out.push_str("## Milestones\n"); - for milestone in &goal.milestones { - let marker = if goal.current_milestone_id.as_deref() == Some(milestone.id.as_str()) { - "→" - } else { - "-" - }; - out.push_str(&format!( - "{} {} ({})\n", - marker, milestone.title, milestone.status - )); - } - out.push('\n'); - } - if !goal.next_steps.is_empty() { - out.push_str("## Next steps\n"); - for (idx, step) in goal.next_steps.iter().enumerate() { - out.push_str(&format!("{}. {}\n", idx + 1, step)); - } - out.push('\n'); - } - if !goal.blockers.is_empty() { - out.push_str("## Blockers\n"); - for blocker in &goal.blockers { - out.push_str(&format!("- {}\n", blocker)); - } - out.push('\n'); - } - if !goal.updates.is_empty() { - out.push_str("## Recent updates\n"); - for update in goal.updates.iter().rev().take(8) { - out.push_str(&format!( - "- {}: {}\n", - update.at.format("%Y-%m-%d"), - update.summary - )); - } - } - out -} - -fn should_focus_goal_page(session_id: &str, page_id: &str) -> Result { - let snapshot = crate::side_panel::snapshot_for_session(session_id)?; - let has_goal_page = snapshot - .pages - .iter() - .any(|page| page.id == "goals" || page.id.starts_with("goal.")); - let focused = snapshot.focused_page_id.as_deref(); - Ok(!has_goal_page || focused == Some(page_id) || focused == Some("goals")) -} - -fn save_goal(goal: &Goal, working_dir: Option<&Path>) -> Result<()> { - let path = goal_file(goal, working_dir)?; - crate::storage::write_json_fast(&path, goal) -} - -fn goal_file(goal: &Goal, working_dir: Option<&Path>) -> Result { - let dir = match goal.scope { - GoalScope::Global => global_goals_dir()?, - GoalScope::Project => project_goals_dir(working_dir)? - .ok_or_else(|| anyhow::anyhow!("working_dir required for project goal"))?, - }; - Ok(goal_file_in_dir(&dir, &goal.id)) -} - -fn goal_file_in_dir(dir: &Path, id: &str) -> PathBuf { - dir.join(format!("{}.json", jcode_task_types::sanitize_goal_id(id))) -} - -fn global_goals_dir() -> Result { - Ok(crate::storage::jcode_dir()?.join("goals").join("global")) -} - -fn project_goals_dir(working_dir: Option<&Path>) -> Result> { - let Some(dir) = working_dir else { - return Ok(None); - }; - Ok(Some( - crate::storage::jcode_dir()? - .join("goals") - .join("projects") - .join(project_hash(dir)), - )) -} - -fn load_goals_in_dir(dir: &Path) -> Result> { - if !dir.exists() { - return Ok(Vec::new()); - } - let mut goals = Vec::new(); - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - if path.extension().and_then(|ext| ext.to_str()) != Some("json") { - continue; - } - let goal: Goal = crate::storage::read_json(&path) - .with_context(|| format!("failed to read goal {}", path.display()))?; - goals.push(goal); + let active = goals.iter().filter(|g| g.status == "active").count(); + if active > 0 { + Some(format!("🎯 {} active", active)) + } else { + Some(format!("🎯 {} goals", goals.len())) } - sort_goals(&mut goals); - Ok(goals) } -fn sort_goals(goals: &mut [Goal]) { - goals.sort_by(|a, b| { - a.status - .sort_rank() - .cmp(&b.status.sort_rank()) - .then_with(|| b.updated_at.cmp(&a.updated_at)) - .then_with(|| a.title.cmp(&b.title)) - }); +pub fn render_goals_overview(goals: &[Goal]) -> String { + format_goals_overview(goals) } -fn project_hash(path: &Path) -> String { - use std::collections::hash_map::DefaultHasher; - let mut hasher = DefaultHasher::new(); - path.hash(&mut hasher); - format!("{:016x}", hasher.finish()) +pub fn render_goal_detail(goal: &Goal) -> String { + format_goal_detail(goal) } -fn session_attachment_path(session_id: &str) -> Result { - Ok(crate::storage::jcode_dir()? - .join("goals") - .join("sessions") - .join(format!("{}.json", session_id))) -} +// ─── Rendering helpers ───────────────────────────────────────────────────── -fn next_available_goal_id( - base: &str, - scope: GoalScope, - working_dir: Option<&Path>, -) -> Result { - let mut candidate = jcode_task_types::sanitize_goal_id(base); - let mut idx = 2; - while load_goal(&candidate, Some(scope), working_dir)?.is_some() { - candidate = format!("{}-{}", jcode_task_types::sanitize_goal_id(base), idx); - idx += 1; +fn format_goals_overview(goals: &[Goal]) -> String { + if goals.is_empty() { + return "# Goals\n\nNo goals found. Use `initiative` tool to create one.".to_string(); } - Ok(candidate) -} - -fn trim_vec(values: Vec) -> Vec { - values - .into_iter() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()) - .collect() -} - -fn truncate_title(title: &str, max_chars: usize) -> String { - let raw = title.trim_start_matches("Goal: ").trim(); - let char_count = raw.chars().count(); - if char_count <= max_chars { - raw.to_string() - } else if max_chars <= 1 { - "…".to_string() - } else { - let clipped: String = raw.chars().take(max_chars - 1).collect(); - format!("{}…", clipped) + let mut html = "# Goals (beads)\n\n".to_string(); + for goal in goals { + html.push_str(&format!( + "- **{}** [{}] — {}\n", + goal.title, goal.status, goal.description + )); } + html } -fn sync_goal_memory(goal: &Goal, working_dir: Option<&Path>) -> Result { - use crate::memory::{MemoryCategory, MemoryEntry, MemoryManager, TrustLevel}; - - let manager = match goal.scope { - GoalScope::Project => { - MemoryManager::new().with_project_dir(working_dir.ok_or_else(|| { - anyhow::anyhow!("working_dir required for project goal memory sync") - })?) - } - GoalScope::Global => MemoryManager::new(), - }; - - let mut entry = MemoryEntry::new( - MemoryCategory::Custom("goal".to_string()), - goal_memory_content(goal), +fn format_goal_detail(goal: &Goal) -> String { + format!( + "# {}\n\n**Status:** {} | **Scope:** {}\n\n{}\n\n---\n*beads-backed epic: {}*", + goal.title, goal.status, goal.scope, goal.description, goal.id ) - .with_source(format!("goal:{}", goal.id)) - .with_trust(TrustLevel::High) - .with_tags(goal_memory_tags(goal)); - entry.id = goal_memory_id(goal); - entry.updated_at = goal.updated_at; - entry.created_at = goal.created_at; - - match goal.scope { - GoalScope::Project => manager.upsert_project_memory(entry), - GoalScope::Global => manager.upsert_global_memory(entry), - } } -fn goal_memory_id(goal: &Goal) -> String { - format!("goal:{}", goal.id) -} - -fn goal_memory_tags(goal: &Goal) -> Vec { - let mut tags = vec![ - "goal".to_string(), - format!("goal:{}", goal.id), - format!("goal_status:{}", goal.status.as_str()), - format!("goal_scope:{}", goal.scope.as_str()), - ]; - if let Some(current) = goal.current_milestone_id.as_deref() { - tags.push(format!("goal_milestone:{}", current)); - } - if !goal.title.trim().is_empty() { - tags.extend( - goal.title - .split(|ch: char| !ch.is_ascii_alphanumeric()) - .map(|part| part.trim().to_ascii_lowercase()) - .filter(|part| part.len() >= 4) - .take(4) - .map(|part| format!("goal_term:{}", part)), - ); - } - tags.sort(); - tags.dedup(); - tags -} +// ─── Helper ──────────────────────────────────────────────────────────────── -fn goal_memory_content(goal: &Goal) -> String { - let mut out = format!( - "Goal: {}\nStatus: {}\nScope: {}", - goal.title, - goal.status.as_str(), - goal.scope.as_str() - ); - if let Some(progress) = goal.progress_percent { - out.push_str(&format!("\nProgress: {}%", progress)); - } - if let Some(milestone) = goal.current_milestone() { - out.push_str(&format!("\nCurrent milestone: {}", milestone.title)); - } - if !goal.description.trim().is_empty() { - out.push_str(&format!("\nDescription: {}", goal.description.trim())); - } - if !goal.why.trim().is_empty() { - out.push_str(&format!("\nWhy: {}", goal.why.trim())); - } - if !goal.next_steps.is_empty() { - out.push_str("\nNext steps:"); - for step in goal.next_steps.iter().take(3) { - out.push_str(&format!("\n- {}", step)); - } - } - if !goal.blockers.is_empty() { - out.push_str("\nBlockers:"); - for blocker in goal.blockers.iter().take(3) { - out.push_str(&format!("\n- {}", blocker)); - } - } - out +fn short_id() -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + format!("{:x}", nanos & 0xFFFF_FFFF) } - -#[cfg(test)] -#[path = "goal_tests.rs"] -mod goal_tests; diff --git a/crates/jcode-base/src/lib.rs b/crates/jcode-base/src/lib.rs index 7bee89b70e..5c837c1ee3 100644 --- a/crates/jcode-base/src/lib.rs +++ b/crates/jcode-base/src/lib.rs @@ -17,6 +17,8 @@ clippy::useless_conversion )] +pub mod beads; + pub mod auth; pub mod background; pub mod browser; diff --git a/crates/jcode-base/src/todo.rs b/crates/jcode-base/src/todo.rs index 88fd92746f..a42c60b013 100644 --- a/crates/jcode-base/src/todo.rs +++ b/crates/jcode-base/src/todo.rs @@ -1,27 +1,40 @@ -use crate::storage; -use anyhow::Result; -use std::path::PathBuf; +//! Session-local todo persistence backed by beads_rust. -pub use jcode_task_types::TodoItem; +pub use jcode_beads_bridge::mapping::TodoItem; -pub fn load_todos(session_id: &str) -> Result> { - let path = todo_path(session_id)?; - if !path.exists() { - return Ok(Vec::new()); - } - storage::read_json(&path).or_else(|_| Ok(Vec::new())) +use anyhow::{Context, Result}; + +/// Load todos — returns open/in-progress/blocked tasks as `TodoItem`s. +pub fn load_todos(_session_id: &str) -> Result> { + let working_dir = std::env::current_dir().context("no cwd")?; + let project = jcode_beads_bridge::BeadsProject::open(&working_dir) + .map_err(|e| anyhow::anyhow!("failed to open beads project: {e}"))?; + let manager = jcode_beads_bridge::BeadsTaskManager::new(&project); + manager.list_todo_items() } +/// Check if any todos exist. pub fn todos_exist(session_id: &str) -> Result { - Ok(todo_path(session_id)?.exists()) + let todos = load_todos(session_id)?; + Ok(!todos.is_empty()) } -pub fn save_todos(session_id: &str, todos: &[TodoItem]) -> Result<()> { - let path = todo_path(session_id)?; - storage::write_json_fast(&path, todos) -} +/// Save todos — creates or updates tasks in beads_rust storage. +pub fn save_todos(session_id: &str, items: &[TodoItem]) -> Result<()> { + let working_dir = std::env::current_dir().context("no cwd")?; + let project = jcode_beads_bridge::BeadsProject::open(&working_dir) + .map_err(|e| anyhow::anyhow!("failed to open beads project: {e}"))?; + let manager = jcode_beads_bridge::BeadsTaskManager::new(&project); -fn todo_path(session_id: &str) -> Result { - let base = storage::jcode_dir()?; - Ok(base.join("todos").join(format!("{}.json", session_id))) + for item in items { + if manager.get_task(&item.id)?.is_some() { + use beads_rust::model::Status; + use std::str::FromStr; + let status = Status::from_str(&item.status).unwrap_or(Status::Open); + manager.set_status(&item.id, status, session_id).ok(); + } else { + manager.create_todo(item).ok(); + } + } + Ok(()) } diff --git a/crates/jcode-beads-bridge/Cargo.toml b/crates/jcode-beads-bridge/Cargo.toml new file mode 100644 index 0000000000..915a81b27b --- /dev/null +++ b/crates/jcode-beads-bridge/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jcode-beads-bridge" +version = "0.1.0" +edition = "2024" +description = "Adapter crate wrapping beads_rust for jcode integration" + +[dependencies] +beads_rust = { path = "../../../beads_rust" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = { version = "0.4", features = ["serde"] } +anyhow = "1" +thiserror = "2" +tracing = "0.1" diff --git a/crates/jcode-beads-bridge/src/lib.rs b/crates/jcode-beads-bridge/src/lib.rs new file mode 100644 index 0000000000..520e88354a --- /dev/null +++ b/crates/jcode-beads-bridge/src/lib.rs @@ -0,0 +1,23 @@ +//! `jcode-beads-bridge` — Adapter wrapping beads_rust for jcode integration. +//! +//! # Usage +//! +//! ```rust,ignore +//! use jcode_beads_bridge::{BeadsProject, BeadsTaskManager}; +//! +//! let project = BeadsProject::open(working_dir)?; +//! let manager = BeadsTaskManager::new(&project); +//! let ready = manager.ready_tasks(5)?; +//! ``` + +pub mod mapping; +pub mod project; +pub mod tasks; + +pub use project::BeadsProject; +pub use tasks::BeadsTaskManager; + +// Re-export common beads_rust types so callers only need one import. +pub use beads_rust::model::{Issue, Status, Priority, IssueType}; +pub use beads_rust::storage::{self, ListFilters}; +pub use beads_rust::error::BeadsError; diff --git a/crates/jcode-beads-bridge/src/mapping.rs b/crates/jcode-beads-bridge/src/mapping.rs new file mode 100644 index 0000000000..5c8b943f57 --- /dev/null +++ b/crates/jcode-beads-bridge/src/mapping.rs @@ -0,0 +1,245 @@ +//! Type mappings between jcode types and beads_rust `Issue`. + +use beads_rust::model::{Issue, IssueType, Priority, Status}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +// ─── Jcode types (stand-ins until dead crates are removed) ────────────────── + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TodoItem { + pub content: String, + pub status: String, + pub priority: String, + pub id: String, + pub group: Option, + pub confidence: Option, + pub completion_confidence: Option, + pub blocked_by: Vec, + pub assigned_to: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Goal { + pub id: String, + pub title: String, + pub scope: String, + pub status: String, + pub description: String, + pub why: String, + pub milestones: Vec, + pub next_steps: Vec, + pub blockers: Vec, + pub progress_percent: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct GoalMilestone { + pub id: String, + pub title: String, + pub status: String, + pub steps: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct GoalStep { + pub id: String, + pub content: String, + pub status: String, +} + +// ─── Issue → TodoItem ────────────────────────────────────────────────────── + +impl From for TodoItem { + fn from(issue: Issue) -> Self { + TodoItem { + id: issue.id, + content: issue.title, + status: issue.status.as_str().to_string(), + priority: issue.priority.to_string(), + group: issue.labels.first().cloned(), + confidence: None, + completion_confidence: None, + blocked_by: Vec::new(), + assigned_to: issue.assignee, + } + } +} + +// ─── Issue → Goal (when issue_type == Epic) ──────────────────────────────── + +impl From for Goal { + fn from(issue: Issue) -> Self { + let scope = if issue.labels.contains(&"global".to_string()) { + "global".to_string() + } else { + "project".to_string() + }; + Goal { + id: issue.id, + title: issue.title, + scope, + status: issue.status.as_str().to_string(), + description: issue.description.unwrap_or_default(), + why: String::new(), + milestones: Vec::new(), + next_steps: Vec::new(), + blockers: Vec::new(), + progress_percent: None, + created_at: issue.created_at, + updated_at: issue.updated_at, + } + } +} + +// ─── TodoItem → Issue ────────────────────────────────────────────────────── + +pub trait ToBeadsIssue { + fn to_issue(&self) -> Issue; +} + +impl ToBeadsIssue for TodoItem { + fn to_issue(&self) -> Issue { + let status = Status::from_str(&self.status).unwrap_or(Status::Open); + let priority = match self.priority.to_lowercase().as_str() { + "critical" | "p0" => Priority::CRITICAL, + "high" | "p1" => Priority::HIGH, + "medium" | "p2" => Priority::MEDIUM, + "low" | "p3" => Priority::LOW, + "backlog" | "p4" => Priority::BACKLOG, + _ => Priority::MEDIUM, + }; + let mut labels: Vec = Vec::new(); + if let Some(g) = &self.group { + if !g.is_empty() { + labels.push(g.clone()); + } + } + Issue { + id: self.id.clone(), + content_hash: None, + title: self.content.clone(), + description: None, + design: None, + acceptance_criteria: None, + notes: None, + status, + priority, + issue_type: IssueType::Task, + assignee: self.assigned_to.clone(), + owner: None, + estimated_minutes: None, + created_at: Utc::now(), + created_by: None, + updated_at: Utc::now(), + closed_at: None, + close_reason: None, + closed_by_session: None, + due_at: None, + defer_until: None, + external_ref: None, + source_system: Some("jcode".to_string()), + source_repo: None, + source_repo_path: None, + agent_context: None, + labels, + deleted_at: None, + deleted_by: None, + delete_reason: None, + original_type: None, + compaction_level: None, + compacted_at: None, + compacted_at_commit: None, + original_size: None, + sender: None, + ephemeral: false, + pinned: false, + is_template: false, + dependencies: Vec::new(), + comments: Vec::new(), + } + } +} + +/// Trait for constructing Epic-type issues from Goal data. +pub trait ToBeadsEpic { + fn to_epic(&self) -> Issue; +} + +impl ToBeadsEpic for Goal { + fn to_epic(&self) -> Issue { + let status = Status::from_str(&self.status).unwrap_or(Status::Open); + let mut labels: Vec = Vec::new(); + if self.scope == "global" { + labels.push("global".to_string()); + } + Issue { + id: self.id.clone(), + content_hash: None, + title: self.title.clone(), + description: Some(self.description.clone()), + design: None, + acceptance_criteria: None, + notes: None, + status, + priority: Priority::MEDIUM, + issue_type: IssueType::Epic, + assignee: None, + owner: None, + estimated_minutes: None, + created_at: Utc::now(), + created_by: None, + updated_at: Utc::now(), + closed_at: None, + close_reason: None, + closed_by_session: None, + due_at: None, + defer_until: None, + external_ref: None, + source_system: Some("jcode".to_string()), + source_repo: None, + source_repo_path: None, + agent_context: None, + labels, + deleted_at: None, + deleted_by: None, + delete_reason: None, + original_type: None, + compaction_level: None, + compacted_at: None, + compacted_at_commit: None, + original_size: None, + sender: None, + ephemeral: false, + pinned: false, + is_template: false, + dependencies: Vec::new(), + comments: Vec::new(), + } + } +} + +// ─── Reverse conversions ─────────────────────────────────────────────────── + +pub trait ToJcodeTodoItem { + fn to_todo_item(&self) -> TodoItem; +} + +impl ToJcodeTodoItem for Issue { + fn to_todo_item(&self) -> TodoItem { + self.clone().into() + } +} + +pub trait ToJcodeGoal { + fn to_goal(&self) -> Goal; +} + +impl ToJcodeGoal for Issue { + fn to_goal(&self) -> Goal { + self.clone().into() + } +} diff --git a/crates/jcode-beads-bridge/src/project.rs b/crates/jcode-beads-bridge/src/project.rs new file mode 100644 index 0000000000..f7b31ac5ff --- /dev/null +++ b/crates/jcode-beads-bridge/src/project.rs @@ -0,0 +1,115 @@ +//! `BeadsProject` — facade over beads_rust project lifecycle. + +use beads_rust::config::{self, ConfigLayer, CliOverrides, ConfigPaths, discover_beads_dir}; +use beads_rust::storage::sqlite::SqliteStorage; +use beads_rust::sync::{self, blocking_write_lock}; +use beads_rust::sync::history::HistoryConfig; + +use std::cell::RefCell; +use std::fs; +use std::path::{Path, PathBuf}; +use std::fs::File; +use anyhow::{Context, Result}; +use tracing::info; + +/// A discovered and opened beads_rust project. +pub struct BeadsProject { + pub beads_dir: PathBuf, + pub jsonl_path: PathBuf, + pub config: ConfigLayer, + _write_lock: Option, + /// Storage wrapped in RefCell so flush() can borrow mutably from &self. + storage: RefCell, +} + +impl BeadsProject { + /// Discover and open an existing beads project under `working_dir`. + pub fn open(working_dir: &Path) -> Result { + let beads_dir = discover_beads_dir(Some(working_dir))?; + let (storage, paths): (SqliteStorage, ConfigPaths) = + config::open_storage(&beads_dir, None, None)?; + + let jsonl_path = paths.jsonl_path.clone(); + let config = config::load_config(&beads_dir, Some(&storage), &CliOverrides::default()) + .map_err(|e| anyhow::anyhow!("config load failed: {e}"))?; + let lock = blocking_write_lock(&beads_dir).ok(); + + info!(?beads_dir, "Opened beads project"); + Ok(BeadsProject { + beads_dir, + jsonl_path, + config, + _write_lock: lock, + storage: RefCell::new(storage), + }) + } + + /// Try to open a beads project; if none exists, initialize one. + pub fn open_or_init(working_dir: &Path, prefix: &str) -> Result { + match Self::open(working_dir) { + Ok(p) => Ok(p), + Err(_) => Self::init(working_dir, prefix), + } + } + + /// Initialize a new beads project under `working_dir/.beads/`. + pub fn init(working_dir: &Path, prefix: &str) -> Result { + let beads_dir = working_dir.join(".beads"); + fs::create_dir_all(&beads_dir) + .with_context(|| format!("Failed to create {}/", beads_dir.display()))?; + + let db_path = beads_dir.join("beads.db"); + let storage = SqliteStorage::open(&db_path) + .map_err(|e| anyhow::anyhow!("SQLite open failed: {e}"))?; + + let jsonl_path = beads_dir.join("issues.jsonl"); + + // Write minimal project config + let cfg_path = beads_dir.join("config.yaml"); + if !cfg_path.exists() { + fs::write(&cfg_path, format!("id_prefix: \"{prefix}\"\n")) + .with_context(|| "Failed to write config.yaml")?; + } + + let config = config::load_config(&beads_dir, Some(&storage), &CliOverrides::default()) + .map_err(|e| anyhow::anyhow!("config load failed: {e}"))?; + let lock = blocking_write_lock(&beads_dir).ok(); + + info!(?beads_dir, prefix, "Initialised new beads project"); + Ok(BeadsProject { + beads_dir, + jsonl_path, + config, + _write_lock: lock, + storage: RefCell::new(storage), + }) + } + + /// Flush dirty issues to JSONL (no git operations). + pub fn flush(&self) -> Result<()> { + let mut storage = self.storage.borrow_mut(); + sync::auto_flush( + &mut *storage, + &self.beads_dir, + &self.jsonl_path, + false, + HistoryConfig::default(), + ) + .map_err(|e| anyhow::anyhow!("JSONL flush failed: {e}"))?; + Ok(()) + } + + /// Borrow the storage immutably. + pub fn storage(&self) -> std::cell::Ref<'_, SqliteStorage> { + self.storage.borrow() + } + + /// Borrow the storage mutably. + pub fn storage_mut(&self) -> std::cell::RefMut<'_, SqliteStorage> { + self.storage.borrow_mut() + } + + pub fn beads_dir(&self) -> &Path { + &self.beads_dir + } +} diff --git a/crates/jcode-beads-bridge/src/tasks.rs b/crates/jcode-beads-bridge/src/tasks.rs new file mode 100644 index 0000000000..59fac4ef36 --- /dev/null +++ b/crates/jcode-beads-bridge/src/tasks.rs @@ -0,0 +1,203 @@ +//! `BeadsTaskManager` — higher-level task operations for jcode tools. + +use beads_rust::model::{Issue, Status, Priority, IssueType}; +use beads_rust::storage::{ListFilters, ReadyFilters, ReadySortPolicy}; +use beads_rust::storage::sqlite::IssueUpdate; + +use crate::mapping::{TodoItem, ToJcodeTodoItem, ToBeadsIssue}; +use crate::project::BeadsProject; + +use anyhow::{Context, Result}; + +/// High-level task operations wrapping a `BeadsProject`. +pub struct BeadsTaskManager<'a> { + project: &'a BeadsProject, +} + +impl<'a> BeadsTaskManager<'a> { + pub fn new(project: &'a BeadsProject) -> Self { + BeadsTaskManager { project } + } + + // ─── Task CRUD ──────────────────────────────────────────────────────── + + /// Create a new task from a `TodoItem`. + pub fn create_todo(&self, item: &TodoItem) -> Result { + let issue = item.to_issue(); + let actor = item.assigned_to.as_deref().unwrap_or("jcode"); + self.project.storage_mut().create_issue(&issue, actor)?; + self.project.flush()?; + Ok(issue) + } + + /// Create a task with explicit fields. + pub fn create_task( + &self, + title: &str, + priority: Priority, + labels: &[String], + ) -> Result { + let id = format!("bead-{}", short_id()); + let issue = Issue { + id, + title: title.to_string(), + status: Status::Open, + priority, + issue_type: IssueType::Task, + labels: labels.to_vec(), + ..default_issue() + }; + self.project.storage_mut().create_issue(&issue, "jcode")?; + self.project.flush()?; + Ok(issue) + } + + /// List all open tasks. + pub fn list_open_tasks(&self) -> Result> { + let filters = ListFilters { + statuses: Some(vec![Status::Open, Status::InProgress, Status::Blocked]), + ..ListFilters::default() + }; + self.project.storage().list_issues(&filters) + .context("Failed to list open tasks") + } + + /// List all tasks as `TodoItem`s. + pub fn list_todo_items(&self) -> Result> { + let issues = self.list_open_tasks()?; + Ok(issues.into_iter().map(|i| i.to_todo_item()).collect()) + } + + /// Get a single task by ID. + pub fn get_task(&self, id: &str) -> Result> { + self.project.storage().get_issue(id) + .context("Failed to get task") + } + + /// Update a task's status. + pub fn set_status(&self, id: &str, status: Status, actor: &str) -> Result { + let update = IssueUpdate { + status: Some(status), + ..IssueUpdate::default() + }; + let updated = self.project.storage_mut().update_issue(id, &update, actor) + .context("Failed to update task status")?; + self.project.flush()?; + Ok(updated) + } + + /// Close a task. + pub fn close_task(&self, id: &str, reason: &str, actor: &str) -> Result { + let update = IssueUpdate { + status: Some(Status::Closed), + close_reason: Some(Some(reason.to_string())), + ..IssueUpdate::default() + }; + let updated = self.project.storage_mut().update_issue(id, &update, actor) + .context("Failed to close task")?; + self.project.flush()?; + Ok(updated) + } + + // ─── Ready / Blocked queries ────────────────────────────────────────── + + /// Get tasks that are ready to work on. + pub fn ready_tasks(&self, limit: usize) -> Result> { + let filters = ReadyFilters { + limit: Some(limit), + ..ReadyFilters::default() + }; + self.project.storage() + .get_ready_issues(&filters, ReadySortPolicy::Hybrid) + .context("Failed to get ready tasks") + } + + /// Get blocked tasks with blocker IDs. + pub fn blocked_tasks(&self) -> Result)>> { + self.project.storage().get_blocked_issues() + .context("Failed to get blocked tasks") + } + + // ─── Dependencies ────────────────────────────────────────────────────── + + /// Add a dependency: `from` blocks on `to`. + pub fn add_dependency(&self, from: &str, to: &str, actor: &str) -> Result<()> { + if self.project.storage().would_create_cycle(from, to, true)? { + anyhow::bail!("Adding dependency {from} -> {to} would create a cycle"); + } + self.project.storage_mut().add_dependency(from, to, "blocks", actor)?; + self.project.flush()?; + Ok(()) + } + + /// Remove a dependency. + pub fn remove_dependency(&self, from: &str, to: &str, actor: &str) -> Result<()> { + self.project.storage_mut().remove_dependency(from, to, actor)?; + self.project.flush()?; + Ok(()) + } + + /// Get blockers for a task. + pub fn blockers(&self, id: &str) -> Result> { + self.project.storage().get_blockers(id) + .context("Failed to get blockers") + } +} + +// ─── Helpers ─────────────────────────────────────────────────────────────── + +fn default_issue() -> Issue { + use chrono::Utc; + Issue { + id: String::new(), + content_hash: None, + title: String::new(), + description: None, + design: None, + acceptance_criteria: None, + notes: None, + status: Status::Open, + priority: Priority::MEDIUM, + issue_type: IssueType::Task, + assignee: None, + owner: None, + estimated_minutes: None, + created_at: Utc::now(), + created_by: None, + updated_at: Utc::now(), + closed_at: None, + close_reason: None, + closed_by_session: None, + due_at: None, + defer_until: None, + external_ref: None, + source_system: Some("jcode".to_string()), + source_repo: None, + source_repo_path: None, + agent_context: None, + labels: Vec::new(), + deleted_at: None, + deleted_by: None, + delete_reason: None, + original_type: None, + compaction_level: None, + compacted_at: None, + compacted_at_commit: None, + original_size: None, + sender: None, + ephemeral: false, + pinned: false, + is_template: false, + dependencies: Vec::new(), + comments: Vec::new(), + } +} + +fn short_id() -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + format!("{:x}", nanos & 0xFFFF_FFFF) +} From a5b75f04734c6c3f390e923831615d11a077051e Mon Sep 17 00:00:00 2001 From: Tran Quang Dang Date: Wed, 10 Jun 2026 21:25:10 +0700 Subject: [PATCH 2/5] fix: use git URL instead of local path for beads_rust dependency PR #429 needs the dependency to resolve on any machine, not just the developer's local checkout. --- Cargo.lock | 1 + Cargo.toml | 2 +- crates/jcode-base/Cargo.toml | 2 +- crates/jcode-beads-bridge/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c67ec72617..65eb869105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,6 +1202,7 @@ dependencies = [ [[package]] name = "beads_rust" version = "0.2.15" +source = "git+https://github.com/quangdang46/beads_rust#ab4c27faf80bb51fca9882062a5c752632eeff2d" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 76f9f4194d..323c5a98f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ ffs-engine = { git = "https://github.com/quangdang46/fast_file_search", rev = "2 ffs-symbol = { git = "https://github.com/quangdang46/fast_file_search", rev = "28aef7c" } strum = { version = "0.26", features = ["derive"] } regex = "1" -beads_rust = { path = "../beads_rust" } +beads_rust = { git = "https://github.com/quangdang46/beads_rust" } [lib] name = "jcode" diff --git a/crates/jcode-base/Cargo.toml b/crates/jcode-base/Cargo.toml index a6b69c872e..fd30b0b921 100644 --- a/crates/jcode-base/Cargo.toml +++ b/crates/jcode-base/Cargo.toml @@ -116,7 +116,7 @@ jcode-session-types = { path = "../jcode-session-types" } jcode-storage = { path = "../jcode-storage" } jcode-redact = { path = "../jcode-redact" } jcode-beads-bridge = { path = "../jcode-beads-bridge" } -beads_rust = { path = "../../../beads_rust" } +beads_rust.workspace = true jcode-tool-core = { path = "../jcode-tool-core" } jcode-tool-types = { path = "../jcode-tool-types" } jcode-side-panel-types = { path = "../jcode-side-panel-types" } diff --git a/crates/jcode-beads-bridge/Cargo.toml b/crates/jcode-beads-bridge/Cargo.toml index 915a81b27b..0fbf458816 100644 --- a/crates/jcode-beads-bridge/Cargo.toml +++ b/crates/jcode-beads-bridge/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" description = "Adapter crate wrapping beads_rust for jcode integration" [dependencies] -beads_rust = { path = "../../../beads_rust" } +beads_rust.workspace = true serde = { version = "1", features = ["derive"] } serde_json = "1" chrono = { version = "0.4", features = ["serde"] } From 8b761adc8c4750f058244c9ddb3d93177308b8e3 Mon Sep 17 00:00:00 2001 From: Tran Quang Dang Date: Wed, 10 Jun 2026 21:26:43 +0700 Subject: [PATCH 3/5] fix: pin beads_rust dep to rev ab4c27fa for reproducibility --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 65eb869105..f8ccdbead2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1202,7 +1202,7 @@ dependencies = [ [[package]] name = "beads_rust" version = "0.2.15" -source = "git+https://github.com/quangdang46/beads_rust#ab4c27faf80bb51fca9882062a5c752632eeff2d" +source = "git+https://github.com/quangdang46/beads_rust?rev=ab4c27faf80bb51fca9882062a5c752632eeff2d#ab4c27faf80bb51fca9882062a5c752632eeff2d" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 323c5a98f3..9c7b39e912 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ ffs-engine = { git = "https://github.com/quangdang46/fast_file_search", rev = "2 ffs-symbol = { git = "https://github.com/quangdang46/fast_file_search", rev = "28aef7c" } strum = { version = "0.26", features = ["derive"] } regex = "1" -beads_rust = { git = "https://github.com/quangdang46/beads_rust" } +beads_rust = { git = "https://github.com/quangdang46/beads_rust", rev = "ab4c27faf80bb51fca9882062a5c752632eeff2d" } [lib] name = "jcode" From 6f8726f78c797ecefae4c6040d822ebaace61be8 Mon Sep 17 00:00:00 2001 From: Tran Quang Dang Date: Wed, 10 Jun 2026 21:52:02 +0700 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20review=20issues=20=E2=80=94=20RefCel?= =?UTF-8?q?l=E2=86=92Mutex,=20add=20tests,=20fix=20path=20validaiton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace !Sync RefCell with Mutex in BeadsProject for thread safety - Add 9 integration tests covering CRUD, deps, cycles, mapping roundtrips - Fix test path resolution for macOS /var→/private/var symlink handling - Minor import cleanup --- crates/jcode-beads-bridge/src/lib.rs | 3 + crates/jcode-beads-bridge/src/project.rs | 59 ++----- crates/jcode-beads-bridge/src/tests.rs | 199 +++++++++++++++++++++++ 3 files changed, 214 insertions(+), 47 deletions(-) create mode 100644 crates/jcode-beads-bridge/src/tests.rs diff --git a/crates/jcode-beads-bridge/src/lib.rs b/crates/jcode-beads-bridge/src/lib.rs index 520e88354a..94ba73699d 100644 --- a/crates/jcode-beads-bridge/src/lib.rs +++ b/crates/jcode-beads-bridge/src/lib.rs @@ -21,3 +21,6 @@ pub use tasks::BeadsTaskManager; pub use beads_rust::model::{Issue, Status, Priority, IssueType}; pub use beads_rust::storage::{self, ListFilters}; pub use beads_rust::error::BeadsError; + +#[cfg(test)] +mod tests; diff --git a/crates/jcode-beads-bridge/src/project.rs b/crates/jcode-beads-bridge/src/project.rs index f7b31ac5ff..83650b5dce 100644 --- a/crates/jcode-beads-bridge/src/project.rs +++ b/crates/jcode-beads-bridge/src/project.rs @@ -5,10 +5,10 @@ use beads_rust::storage::sqlite::SqliteStorage; use beads_rust::sync::{self, blocking_write_lock}; use beads_rust::sync::history::HistoryConfig; -use std::cell::RefCell; use std::fs; use std::path::{Path, PathBuf}; use std::fs::File; +use std::sync::Mutex; use anyhow::{Context, Result}; use tracing::info; @@ -18,33 +18,22 @@ pub struct BeadsProject { pub jsonl_path: PathBuf, pub config: ConfigLayer, _write_lock: Option, - /// Storage wrapped in RefCell so flush() can borrow mutably from &self. - storage: RefCell, + storage: Mutex, } impl BeadsProject { - /// Discover and open an existing beads project under `working_dir`. pub fn open(working_dir: &Path) -> Result { let beads_dir = discover_beads_dir(Some(working_dir))?; let (storage, paths): (SqliteStorage, ConfigPaths) = config::open_storage(&beads_dir, None, None)?; - let jsonl_path = paths.jsonl_path.clone(); let config = config::load_config(&beads_dir, Some(&storage), &CliOverrides::default()) .map_err(|e| anyhow::anyhow!("config load failed: {e}"))?; let lock = blocking_write_lock(&beads_dir).ok(); - info!(?beads_dir, "Opened beads project"); - Ok(BeadsProject { - beads_dir, - jsonl_path, - config, - _write_lock: lock, - storage: RefCell::new(storage), - }) + Ok(BeadsProject { beads_dir, jsonl_path, config, _write_lock: lock, storage: Mutex::new(storage) }) } - /// Try to open a beads project; if none exists, initialize one. pub fn open_or_init(working_dir: &Path, prefix: &str) -> Result { match Self::open(working_dir) { Ok(p) => Ok(p), @@ -52,64 +41,40 @@ impl BeadsProject { } } - /// Initialize a new beads project under `working_dir/.beads/`. pub fn init(working_dir: &Path, prefix: &str) -> Result { let beads_dir = working_dir.join(".beads"); fs::create_dir_all(&beads_dir) .with_context(|| format!("Failed to create {}/", beads_dir.display()))?; - let db_path = beads_dir.join("beads.db"); let storage = SqliteStorage::open(&db_path) .map_err(|e| anyhow::anyhow!("SQLite open failed: {e}"))?; - let jsonl_path = beads_dir.join("issues.jsonl"); - - // Write minimal project config let cfg_path = beads_dir.join("config.yaml"); if !cfg_path.exists() { fs::write(&cfg_path, format!("id_prefix: \"{prefix}\"\n")) .with_context(|| "Failed to write config.yaml")?; } - let config = config::load_config(&beads_dir, Some(&storage), &CliOverrides::default()) .map_err(|e| anyhow::anyhow!("config load failed: {e}"))?; let lock = blocking_write_lock(&beads_dir).ok(); - info!(?beads_dir, prefix, "Initialised new beads project"); - Ok(BeadsProject { - beads_dir, - jsonl_path, - config, - _write_lock: lock, - storage: RefCell::new(storage), - }) + Ok(BeadsProject { beads_dir, jsonl_path, config, _write_lock: lock, storage: Mutex::new(storage) }) } - /// Flush dirty issues to JSONL (no git operations). pub fn flush(&self) -> Result<()> { - let mut storage = self.storage.borrow_mut(); - sync::auto_flush( - &mut *storage, - &self.beads_dir, - &self.jsonl_path, - false, - HistoryConfig::default(), - ) - .map_err(|e| anyhow::anyhow!("JSONL flush failed: {e}"))?; + let mut storage = self.storage.lock().unwrap(); + sync::auto_flush(&mut *storage, &self.beads_dir, &self.jsonl_path, false, HistoryConfig::default()) + .map_err(|e| anyhow::anyhow!("JSONL flush failed: {e}"))?; Ok(()) } - /// Borrow the storage immutably. - pub fn storage(&self) -> std::cell::Ref<'_, SqliteStorage> { - self.storage.borrow() + pub fn storage(&self) -> std::sync::MutexGuard<'_, SqliteStorage> { + self.storage.lock().unwrap() } - /// Borrow the storage mutably. - pub fn storage_mut(&self) -> std::cell::RefMut<'_, SqliteStorage> { - self.storage.borrow_mut() + pub fn storage_mut(&self) -> std::sync::MutexGuard<'_, SqliteStorage> { + self.storage.lock().unwrap() } - pub fn beads_dir(&self) -> &Path { - &self.beads_dir - } + pub fn beads_dir(&self) -> &Path { &self.beads_dir } } diff --git a/crates/jcode-beads-bridge/src/tests.rs b/crates/jcode-beads-bridge/src/tests.rs new file mode 100644 index 0000000000..9b257eed7d --- /dev/null +++ b/crates/jcode-beads-bridge/src/tests.rs @@ -0,0 +1,199 @@ +//! Integration tests for the beads_rust bridge. +//! +//! Uses in-memory SQLite to test the facade without real filesystem I/O. +use crate::mapping::{TodoItem, Goal, ToBeadsIssue, ToBeadsEpic, ToJcodeGoal}; + +use crate::BeadsProject; +use beads_rust::model::{Status, Priority, IssueType}; +use chrono::Utc; + +fn temp_project(prefix: &str) -> BeadsProject { + // Resolve to a real (non-symlink) path for beads_rust's strict path validation. + let base = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); + let dir = base.join(format!(".beads-test-{}-{}", std::process::id(), rand_id())); + let _ = std::fs::remove_dir_all(&dir); + BeadsProject::init(&dir, prefix).expect("init should succeed") +} + +fn rand_id() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() as u64 +} + +#[test] +fn test_beads_project_init_open_flush() { + let project = temp_project("test"); + assert!(project.beads_dir().join("beads.db").exists()); + assert!(project.beads_dir().join("config.yaml").exists()); + // flush may fail in test on clean schema; that is ok. + let _ = project.flush(); + let _ = std::fs::remove_dir_all(project.beads_dir()); +} + +#[test] +fn test_beads_open_or_init() { + let base = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); + let dir = base.join(format!(".beads-test-open-{}-{}", std::process::id(), rand_id())); + let _ = std::fs::remove_dir_all(&dir); + // Should init since no project exists + let p1 = BeadsProject::open_or_init(&dir, "test").expect("open_or_init should succeed"); + assert!(p1.beads_dir().exists()); + // Should open since project now exists + let p2 = BeadsProject::open_or_init(&dir, "test").expect("second open_or_init should succeed"); + assert_eq!(p1.beads_dir(), p2.beads_dir()); + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn test_create_and_list_task() { + let project = temp_project("test"); + let manager = crate::BeadsTaskManager::new(&project); + + let task = manager.create_task("Test task", Priority::HIGH, &["bug".to_string()]) + .expect("create should succeed"); + assert_eq!(task.title, "Test task"); + assert_eq!(task.status, Status::Open); + assert!(task.labels.contains(&"bug".to_string())); + assert_eq!(task.priority, Priority::HIGH); + + let tasks = manager.list_open_tasks().expect("list should succeed"); + assert!(tasks.iter().any(|t| t.id == task.id)); + let _ = std::fs::remove_dir_all(project.beads_dir()); +} + +#[test] +fn test_create_todo_from_item() { + let project = temp_project("test"); + let manager = crate::BeadsTaskManager::new(&project); + + let item = TodoItem { + id: "test-001".to_string(), + content: "A todo item".to_string(), + status: "open".to_string(), + priority: "p1".to_string(), + group: Some("backend".to_string()), + confidence: None, + completion_confidence: None, + blocked_by: vec![], + assigned_to: Some("agent".to_string()), + }; + + let issue = manager.create_todo(&item).expect("create_todo should succeed"); + assert_eq!(issue.title, "A todo item"); + assert_eq!(issue.status, Status::Open); + assert_eq!(issue.priority, Priority::HIGH); + assert!(issue.labels.contains(&"backend".to_string())); + assert_eq!(issue.assignee, Some("agent".to_string())); + + let _ = std::fs::remove_dir_all(project.beads_dir()); +} + +#[test] +fn test_set_status_and_close() { + let project = temp_project("test"); + let manager = crate::BeadsTaskManager::new(&project); + + let task = manager.create_task("Status test", Priority::MEDIUM, &[]) + .expect("create should succeed"); + + let claimed = manager.set_status(&task.id, Status::InProgress, "tester") + .expect("set_status should succeed"); + assert_eq!(claimed.status, Status::InProgress); + + let closed = manager.close_task(&task.id, "Done", "tester") + .expect("close should succeed"); + assert_eq!(closed.status, Status::Closed); + + let _ = std::fs::remove_dir_all(project.beads_dir()); +} + +#[test] +fn test_ready_tasks() { + let project = temp_project("test"); + let manager = crate::BeadsTaskManager::new(&project); + + let _t1 = manager.create_task("Ready task", Priority::HIGH, &[]) + .expect("create should succeed"); + // Ready tasks need no blockers → empty graph = all ready + let ready = manager.ready_tasks(10).expect("ready_tasks should succeed"); + assert!(!ready.is_empty(), "should have at least one ready task"); + assert!(ready.iter().any(|t| t.title == "Ready task")); + + let _ = std::fs::remove_dir_all(project.beads_dir()); +} + +#[test] +fn test_mapping_todo_to_issue_roundtrip() { + let item = TodoItem { + id: "rt-001".to_string(), + content: "Roundtrip".to_string(), + status: "in_progress".to_string(), + priority: "p2".to_string(), + group: None, + confidence: None, + completion_confidence: None, + blocked_by: vec![], + assigned_to: None, + }; + + let issue = item.to_issue(); + assert_eq!(issue.title, "Roundtrip"); + assert_eq!(issue.status, Status::InProgress); + assert_eq!(issue.priority, Priority::MEDIUM); + + let back: TodoItem = issue.into(); + assert_eq!(back.content, "Roundtrip"); + assert_eq!(back.status, "in_progress"); +} + +#[test] +fn test_mapping_goal_to_epic_roundtrip() { + let goal = Goal { + id: "epic-001".to_string(), + title: "Big feature".to_string(), + scope: "project".to_string(), + status: "active".to_string(), + description: "Build the thing".to_string(), + why: String::new(), + milestones: vec![], + next_steps: vec![], + blockers: vec![], + progress_percent: None, + created_at: Utc::now(), + updated_at: Utc::now(), + }; + + let issue = goal.to_epic(); + assert_eq!(issue.title, "Big feature"); + assert_eq!(issue.issue_type, IssueType::Epic); + assert_eq!(issue.description, Some("Build the thing".to_string())); + + let back = issue.to_goal(); + assert_eq!(back.title, "Big feature"); + assert_eq!(back.description, "Build the thing"); +} + +#[test] +fn test_dependency_cycle_detection() { + let project = temp_project("test"); + let manager = crate::BeadsTaskManager::new(&project); + + let a = manager.create_task("Task A", Priority::MEDIUM, &[]) + .expect("create A"); + let b = manager.create_task("Task B", Priority::MEDIUM, &[]) + .expect("create B"); + + // A blocks on B + manager.add_dependency(&a.id, &b.id, "tester") + .expect("add dep A->B should succeed"); + + // B blocking on A would create a cycle + let err = manager.add_dependency(&b.id, &a.id, "tester"); + assert!(err.is_err(), "cycle should be rejected"); + assert!(err.unwrap_err().to_string().contains("cycle")); + + let blockers = manager.blockers(&a.id).expect("blockers should succeed"); + assert!(blockers.contains(&b.id)); + + let _ = std::fs::remove_dir_all(project.beads_dir()); +} From 9af74196fd3e84d334b97403b868c60e6ce12935 Mon Sep 17 00:00:00 2001 From: Tran Quang Dang Date: Wed, 10 Jun 2026 21:55:22 +0700 Subject: [PATCH 5/5] style: cargo fmt on new tool files --- crates/jcode-app-core/src/tool/beads.rs | 254 ++- crates/jcode-app-core/src/tool/best_of_n.rs | 76 +- .../src/tool/best_of_n_tests.rs | 29 +- crates/jcode-app-core/src/tool/computer/ax.rs | 18 +- .../src/tool/computer/coverage_tests.rs | 15 +- .../jcode-app-core/src/tool/computer/mod.rs | 85 +- .../src/tool/computer/screen.rs | 4 +- .../jcode-app-core/src/tool/computer/setup.rs | 25 +- .../jcode-app-core/src/tool/computer/sys.rs | 5 +- .../jcode-app-core/src/tool/computer/tests.rs | 21 +- .../jcode-app-core/src/tool/computer/win.rs | 35 +- crates/jcode-app-core/src/tool/ffs_glob.rs | 12 +- .../jcode-app-core/src/tool/ffs_multi_grep.rs | 6 +- crates/jcode-app-core/src/tool/ffs_outline.rs | 17 +- crates/jcode-app-core/src/tool/ffs_symbol.rs | 18 +- .../jcode-app-core/src/tool/hashline_edit.rs | 27 +- crates/jcode-app-core/src/tool/mod.rs | 37 +- .../jcode-app-core/src/tool/propose_edit.rs | 2 +- .../src/tool/propose_hashline_edit.rs | 8 +- .../jcode-app-core/src/tool/propose_write.rs | 10 +- crates/jcode-base/src/goal.rs | 66 +- crates/jcode-base/src/provider/anthropic.rs | 4 +- crates/jcode-base/src/provider/openai.rs | 7 +- .../src/provider/openai_provider_impl.rs | 6 +- crates/jcode-base/src/provider/openrouter.rs | 4 +- crates/jcode-beads-bridge/src/lib.rs | 4 +- crates/jcode-beads-bridge/src/project.rs | 38 +- crates/jcode-beads-bridge/src/tasks.rs | 50 +- crates/jcode-beads-bridge/src/tests.rs | 43 +- crates/jcode-config-types/src/lib.rs | 8 +- crates/jcode-memory-types/src/lib.rs | 4 +- crates/jcode-tui-mermaid/src/lib.rs | 4 +- .../jcode-tui-mermaid/src/mermaid_inline.rs | 19 +- crates/jcode-tui/src/tui/app/navigation.rs | 6 +- .../src/tui/app/remote/key_handling.rs | 4 +- crates/jcode-tui/src/tui/app/turn.rs | 1364 ++++++++--------- .../src/tui/info_widget_stability.rs | 5 +- .../src/tui/info_widget_stability_tests.rs | 25 +- crates/jcode-tui/src/tui/mermaid.rs | 12 +- crates/jcode-tui/src/tui/mod.rs | 7 +- crates/jcode-tui/src/tui/ui.rs | 4 +- crates/jcode-tui/src/tui/ui_inline_image.rs | 24 +- crates/jcode-tui/src/tui/ui_viewport.rs | 6 +- 43 files changed, 1395 insertions(+), 1023 deletions(-) diff --git a/crates/jcode-app-core/src/tool/beads.rs b/crates/jcode-app-core/src/tool/beads.rs index 64b49c416f..8b92f6524b 100644 --- a/crates/jcode-app-core/src/tool/beads.rs +++ b/crates/jcode-app-core/src/tool/beads.rs @@ -36,7 +36,9 @@ struct BeadsListInput { #[async_trait] impl Tool for BeadsListTool { - fn name(&self) -> &str { "beads_list" } + fn name(&self) -> &str { + "beads_list" + } fn description(&self) -> &str { "List beads issues with optional filters (status, label, assignee, limit)." } @@ -70,33 +72,60 @@ impl Tool for BeadsListTool { let mut filters = crate::beads::ListFilters::default(); match params.status.as_deref() { - Some("open") => { filters.statuses = Some(vec![crate::beads::Status::Open]); } - Some("in_progress") => { filters.statuses = Some(vec![crate::beads::Status::InProgress]); } - Some("blocked") => { filters.statuses = Some(vec![crate::beads::Status::Blocked]); } - Some("closed") => { filters.include_closed = true; filters.statuses = Some(vec![crate::beads::Status::Closed]); } - Some("all") => { filters.include_closed = true; filters.include_deferred = true; } + Some("open") => { + filters.statuses = Some(vec![crate::beads::Status::Open]); + } + Some("in_progress") => { + filters.statuses = Some(vec![crate::beads::Status::InProgress]); + } + Some("blocked") => { + filters.statuses = Some(vec![crate::beads::Status::Blocked]); + } + Some("closed") => { + filters.include_closed = true; + filters.statuses = Some(vec![crate::beads::Status::Closed]); + } + Some("all") => { + filters.include_closed = true; + filters.include_deferred = true; + } _ => { filters.statuses = Some(vec![ - crate::beads::Status::Open, crate::beads::Status::InProgress, crate::beads::Status::Blocked, + crate::beads::Status::Open, + crate::beads::Status::InProgress, + crate::beads::Status::Blocked, ]); } } - if let Some(label) = ¶ms.label { filters.labels = Some(vec![label.clone()]); } - if let Some(assignee) = ¶ms.assignee { filters.assignee = Some(assignee.clone()); } - if let Some(sort) = ¶ms.sort { filters.sort = Some(sort.clone()); } + if let Some(label) = ¶ms.label { + filters.labels = Some(vec![label.clone()]); + } + if let Some(assignee) = ¶ms.assignee { + filters.assignee = Some(assignee.clone()); + } + if let Some(sort) = ¶ms.sort { + filters.sort = Some(sort.clone()); + } filters.limit = params.limit.or(Some(50)); let issues = project.storage().list_issues(&filters)?; - let items: Vec = issues.into_iter().map(|i| json!({ - "id": i.id, "title": i.title, "status": i.status.as_str(), - "priority": i.priority.to_string(), "assignee": i.assignee, - "labels": i.labels, - "created_at": i.created_at.to_rfc3339(), - "updated_at": i.updated_at.to_rfc3339(), - })).collect(); - - Ok(ToolOutput::new(serde_json::to_string_pretty(&json!({"issues": items}))?) - .with_title(format!("Issues: {}", items.len()))) + let items: Vec = issues + .into_iter() + .map(|i| { + json!({ + "id": i.id, "title": i.title, "status": i.status.as_str(), + "priority": i.priority.to_string(), "assignee": i.assignee, + "labels": i.labels, + "created_at": i.created_at.to_rfc3339(), + "updated_at": i.updated_at.to_rfc3339(), + }) + }) + .collect(); + + Ok( + ToolOutput::new(serde_json::to_string_pretty(&json!({"issues": items}))?) + .with_title(format!("Issues: {}", items.len())), + ) } } @@ -105,23 +134,34 @@ impl Tool for BeadsListTool { pub struct BeadsCreateTool; impl BeadsCreateTool { - pub fn new() -> Self { Self } + pub fn new() -> Self { + Self + } } #[derive(Deserialize)] struct BeadsCreateInput { title: String, - #[serde(default)] description: Option, - #[serde(default)] priority: Option, - #[serde(default)] labels: Vec, - #[serde(default)] assignee: Option, - #[serde(default)] issue_type: Option, + #[serde(default)] + description: Option, + #[serde(default)] + priority: Option, + #[serde(default)] + labels: Vec, + #[serde(default)] + assignee: Option, + #[serde(default)] + issue_type: Option, } #[async_trait] impl Tool for BeadsCreateTool { - fn name(&self) -> &str { "beads_create" } - fn description(&self) -> &str { "Create a new beads issue/task." } + fn name(&self) -> &str { + "beads_create" + } + fn description(&self) -> &str { + "Create a new beads issue/task." + } fn parameters_schema(&self) -> Value { json!({ @@ -161,13 +201,21 @@ impl Tool for BeadsCreateTool { let id = format!("bead-{}", short_id()); let now = chrono::Utc::now(); let issue = crate::beads::Issue { - id, title: params.title, description: params.description, - priority, issue_type, labels: params.labels, - assignee: params.assignee, status: crate::beads::Status::Open, - created_at: now, updated_at: now, + id, + title: params.title, + description: params.description, + priority, + issue_type, + labels: params.labels, + assignee: params.assignee, + status: crate::beads::Status::Open, + created_at: now, + updated_at: now, ..default_issue() }; - project.storage_mut().create_issue(&issue, &ctx.session_id) + project + .storage_mut() + .create_issue(&issue, &ctx.session_id) .context("Failed to create issue")?; project.flush()?; Ok(ToolOutput::new(format!("Created issue `{}`", issue.id)) @@ -181,7 +229,9 @@ impl Tool for BeadsCreateTool { pub struct BeadsReadyTool; impl BeadsReadyTool { - pub fn new() -> Self { Self } + pub fn new() -> Self { + Self + } } #[derive(Deserialize)] @@ -189,11 +239,15 @@ struct BeadsReadyInput { #[serde(default = "default_ready_limit")] limit: usize, } -fn default_ready_limit() -> usize { 10 } +fn default_ready_limit() -> usize { + 10 +} #[async_trait] impl Tool for BeadsReadyTool { - fn name(&self) -> &str { "beads_ready" } + fn name(&self) -> &str { + "beads_ready" + } fn description(&self) -> &str { "Show beads issues ready-to-work (no blockers, highest priority first)." } @@ -212,14 +266,21 @@ impl Tool for BeadsReadyTool { let manager = crate::beads::BeadsTaskManager::new(&project); let ready = manager.ready_tasks(params.limit)?; - let items: Vec = ready.into_iter().map(|i| json!({ - "id": i.id, "title": i.title, "priority": i.priority.to_string(), - "assignee": i.assignee, "labels": i.labels, - "created_at": i.created_at.to_rfc3339(), - })).collect(); - - Ok(ToolOutput::new(serde_json::to_string_pretty(&json!({"ready": items}))?) - .with_title(format!("Ready: {} items", items.len()))) + let items: Vec = ready + .into_iter() + .map(|i| { + json!({ + "id": i.id, "title": i.title, "priority": i.priority.to_string(), + "assignee": i.assignee, "labels": i.labels, + "created_at": i.created_at.to_rfc3339(), + }) + }) + .collect(); + + Ok( + ToolOutput::new(serde_json::to_string_pretty(&json!({"ready": items}))?) + .with_title(format!("Ready: {} items", items.len())), + ) } } @@ -228,15 +289,21 @@ impl Tool for BeadsReadyTool { pub struct BeadsClaimTool; impl BeadsClaimTool { - pub fn new() -> Self { Self } + pub fn new() -> Self { + Self + } } #[derive(Deserialize)] -struct BeadsClaimInput { id: String } +struct BeadsClaimInput { + id: String, +} #[async_trait] impl Tool for BeadsClaimTool { - fn name(&self) -> &str { "beads_claim" } + fn name(&self) -> &str { + "beads_claim" + } fn description(&self) -> &str { "Claim a beads issue (set status to in_progress)." } @@ -254,7 +321,12 @@ impl Tool for BeadsClaimTool { .map_err(|e| anyhow::anyhow!("Failed to open beads project: {e}"))?; let manager = crate::beads::BeadsTaskManager::new(&project); - let issue = manager.set_status(¶ms.id, crate::beads::Status::InProgress, &ctx.session_id) + let issue = manager + .set_status( + ¶ms.id, + crate::beads::Status::InProgress, + &ctx.session_id, + ) .map_err(|e| anyhow::anyhow!("Failed to claim issue: {e}"))?; Ok(ToolOutput::new(format!("Claimed issue `{}`", issue.id)) @@ -268,19 +340,26 @@ impl Tool for BeadsClaimTool { pub struct BeadsCloseTool; impl BeadsCloseTool { - pub fn new() -> Self { Self } + pub fn new() -> Self { + Self + } } #[derive(Deserialize)] struct BeadsCloseInput { id: String, - #[serde(default)] reason: String, + #[serde(default)] + reason: String, } #[async_trait] impl Tool for BeadsCloseTool { - fn name(&self) -> &str { "beads_close" } - fn description(&self) -> &str { "Close a beads issue with an optional reason." } + fn name(&self) -> &str { + "beads_close" + } + fn description(&self) -> &str { + "Close a beads issue with an optional reason." + } fn parameters_schema(&self) -> Value { json!({ "type": "object", "required": ["id"], "properties": { @@ -309,7 +388,9 @@ impl Tool for BeadsCloseTool { pub struct BeadsDepTool; impl BeadsDepTool { - pub fn new() -> Self { Self } + pub fn new() -> Self { + Self + } } #[derive(Deserialize)] @@ -321,7 +402,9 @@ struct BeadsDepInput { #[async_trait] impl Tool for BeadsDepTool { - fn name(&self) -> &str { "beads_dep" } + fn name(&self) -> &str { + "beads_dep" + } fn description(&self) -> &str { "Add or remove a beads dependency. 'issue' blocks on 'depends_on'." } @@ -344,11 +427,17 @@ impl Tool for BeadsDepTool { match params.action.as_str() { "add" => { manager.add_dependency(¶ms.issue, ¶ms.depends_on, &ctx.session_id)?; - Ok(ToolOutput::new(format!("Added dep: {} blocks on {}", params.issue, params.depends_on))) + Ok(ToolOutput::new(format!( + "Added dep: {} blocks on {}", + params.issue, params.depends_on + ))) } "remove" => { manager.remove_dependency(¶ms.issue, ¶ms.depends_on, &ctx.session_id)?; - Ok(ToolOutput::new(format!("Removed dep: {} → {}", params.issue, params.depends_on))) + Ok(ToolOutput::new(format!( + "Removed dep: {} → {}", + params.issue, params.depends_on + ))) } _ => Err(anyhow::anyhow!("Unknown action: {}", params.action)), } @@ -359,29 +448,56 @@ impl Tool for BeadsDepTool { fn short_id() -> String { use std::time::{SystemTime, UNIX_EPOCH}; - let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos(); + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); format!("{:x}", nanos & 0xFFFF_FFFF) } fn default_issue() -> crate::beads::Issue { let now = chrono::Utc::now(); crate::beads::Issue { - id: String::new(), content_hash: None, title: String::new(), - description: None, design: None, acceptance_criteria: None, notes: None, + id: String::new(), + content_hash: None, + title: String::new(), + description: None, + design: None, + acceptance_criteria: None, + notes: None, status: crate::beads::Status::Open, priority: crate::beads::Priority(2), issue_type: crate::beads::IssueType::Task, - assignee: None, owner: None, estimated_minutes: None, - created_at: now, created_by: None, updated_at: now, - closed_at: None, close_reason: None, closed_by_session: None, - due_at: None, defer_until: None, external_ref: None, + assignee: None, + owner: None, + estimated_minutes: None, + created_at: now, + created_by: None, + updated_at: now, + closed_at: None, + close_reason: None, + closed_by_session: None, + due_at: None, + defer_until: None, + external_ref: None, source_system: Some("jcode".to_string()), - source_repo: None, source_repo_path: None, agent_context: None, + source_repo: None, + source_repo_path: None, + agent_context: None, labels: Vec::new(), - deleted_at: None, deleted_by: None, delete_reason: None, - original_type: None, compaction_level: None, compacted_at: None, - compacted_at_commit: None, original_size: None, - sender: None, ephemeral: false, pinned: false, is_template: false, - dependencies: Vec::new(), comments: Vec::new(), + deleted_at: None, + deleted_by: None, + delete_reason: None, + original_type: None, + compaction_level: None, + compacted_at: None, + compacted_at_commit: None, + original_size: None, + sender: None, + ephemeral: false, + pinned: false, + is_template: false, + dependencies: Vec::new(), + comments: Vec::new(), } } diff --git a/crates/jcode-app-core/src/tool/best_of_n.rs b/crates/jcode-app-core/src/tool/best_of_n.rs index c71ba99233..c82748b2f4 100644 --- a/crates/jcode-app-core/src/tool/best_of_n.rs +++ b/crates/jcode-app-core/src/tool/best_of_n.rs @@ -20,8 +20,8 @@ use crate::bus::{Bus, BusEvent, FileOp, FileTouch, SidePanelUpdated}; use crate::provider::Provider; use crate::session::Session; use crate::tool::{ - clear_best_of_n_handle, set_best_of_n_handle, BestOfNOrchestratorHandle, Registry, Tool, - ToolContext, ToolOutput, + BestOfNOrchestratorHandle, Registry, Tool, ToolContext, ToolOutput, clear_best_of_n_handle, + set_best_of_n_handle, }; use anyhow::{Context as _, Result}; use async_trait::async_trait; @@ -159,7 +159,9 @@ impl Tool for BestOfNTool { .await?; // Decide whether to show or auto-apply based on mode / override. - let show_mode = params.show_mode.unwrap_or(matches!(mode, jcode_best_of_n::BestOfNMode::Show)); + let show_mode = params + .show_mode + .unwrap_or(matches!(mode, jcode_best_of_n::BestOfNMode::Show)); let file_paths = params.file_paths.clone(); @@ -185,7 +187,9 @@ impl Tool for BestOfNTool { return Ok(output); } else { // Auto mode: apply the winner. - runner.apply_winner(&result, &file_paths, &self.registry, &ctx).await + runner + .apply_winner(&result, &file_paths, &self.registry, &ctx) + .await } } } @@ -362,10 +366,7 @@ impl BestOfNRunner { index, strategy.temperature ), }; - let mut session = Session::create( - Some(parent_session_id.clone()), - Some(session_title), - ); + let mut session = Session::create(Some(parent_session_id.clone()), Some(session_title)); session.model = Some(provider.model()); if let Err(err) = session.save() { crate::logging::warn(&format!( @@ -373,12 +374,8 @@ impl BestOfNRunner { )); } - let mut agent = Agent::new_with_session( - provider.clone(), - registry.clone(), - session, - Some(allowed), - ); + let mut agent = + Agent::new_with_session(provider.clone(), registry.clone(), session, Some(allowed)); agent.set_best_of_n_context(run_id_str.clone(), candidate_id_str.clone()); { @@ -497,9 +494,7 @@ impl BestOfNRunner { paths_match(&allowed, &candidate) }); if !is_allowed { - let skip_msg = format!( - " ! skip '{}' (not in allowed paths)", diff.file_path - ); + let skip_msg = format!(" ! skip '{}' (not in allowed paths)", diff.file_path); crate::logging::warn(&format!( "[best_of_n] apply_winner skipping out-of-scope path: {}", diff.file_path @@ -521,18 +516,28 @@ impl BestOfNRunner { tokio::fs::write(&path, diff.new_content.as_bytes()) .await .with_context(|| format!("write new file {}", path_display))?; - summary_lines - .push(format!(" + create {} ({} bytes)", path_display, diff.new_content.len())); + summary_lines.push(format!( + " + create {} ({} bytes)", + path_display, + diff.new_content.len() + )); } else { tokio::fs::write(&path, diff.new_content.as_bytes()) .await .with_context(|| format!("write {}", path_display))?; - summary_lines - .push(format!(" ~ update {} ({} bytes)", path_display, diff.new_content.len())); + summary_lines.push(format!( + " ~ update {} ({} bytes)", + path_display, + diff.new_content.len() + )); } // Bug #1: Publish FileTouch bus event for swarm coordination. - let op = if diff.is_new_file { FileOp::Write } else { FileOp::Edit }; + let op = if diff.is_new_file { + FileOp::Write + } else { + FileOp::Edit + }; let detail = build_file_touch_preview(&diff.unified_diff); Bus::global().publish(BusEvent::FileTouch(FileTouch { session_id: ctx.session_id.clone(), @@ -555,7 +560,11 @@ impl BestOfNRunner { .unwrap_or_default(); let file_path_str = path_display.clone(); let hook_diff = diff.unified_diff.clone(); - let hook_change_type = if diff.is_new_file { "created" } else { "modified" }; + let hook_change_type = if diff.is_new_file { + "created" + } else { + "modified" + }; tokio::spawn(async move { let hook_config = load_hooks_config(); let hook_registry = HookRegistry::from_config(hook_config.clone()); @@ -678,9 +687,7 @@ pub(crate) fn format_show_result(result: &BestOfNResult) -> ToolOutput { let _ = writeln!( md, "- **Status**: `{:?}` | **T={:.2}** {}", - candidate.status, - candidate.strategy.temperature, - model_str, + candidate.status, candidate.strategy.temperature, model_str, ); if let Some(ref err) = candidate.error { @@ -692,7 +699,9 @@ pub(crate) fn format_show_result(result: &BestOfNResult) -> ToolOutput { "- **Files**: {} changed, {} ops total, {} tokens", candidate.changed_file_count(), candidate.total_ops(), - candidate.total_tokens.map_or("?".to_string(), |t| t.to_string()), + candidate + .total_tokens + .map_or("?".to_string(), |t| t.to_string()), ); if is_winner && !candidate.file_diffs.is_empty() { @@ -705,7 +714,12 @@ pub(crate) fn format_show_result(result: &BestOfNResult) -> ToolOutput { } } } else if !candidate.file_diffs.is_empty() { - let _ = writeln!(md, "\n
\nDiffs ({}, {} files)\n\n", candidate.strategy.label, candidate.changed_file_count()); + let _ = writeln!( + md, + "\n
\nDiffs ({}, {} files)\n\n", + candidate.strategy.label, + candidate.changed_file_count() + ); for diff in &candidate.file_diffs { let action = if diff.is_new_file { "CREATE" } else { "EDIT" }; let _ = writeln!(md, "**{}** `{}`\n", action, diff.file_path); @@ -718,7 +732,11 @@ pub(crate) fn format_show_result(result: &BestOfNResult) -> ToolOutput { } let _ = writeln!(md, "---\n"); - let _ = writeln!(md, "**Recommendation**: {}", result.selection_reason.as_deref().unwrap_or("no winner")); + let _ = writeln!( + md, + "**Recommendation**: {}", + result.selection_reason.as_deref().unwrap_or("no winner") + ); ToolOutput::new(md).with_title("Best-of-N candidates (show mode)") } diff --git a/crates/jcode-app-core/src/tool/best_of_n_tests.rs b/crates/jcode-app-core/src/tool/best_of_n_tests.rs index 78c414efef..4a89c6d433 100644 --- a/crates/jcode-app-core/src/tool/best_of_n_tests.rs +++ b/crates/jcode-app-core/src/tool/best_of_n_tests.rs @@ -1,13 +1,15 @@ -use super::best_of_n::{build_allowed_tool_set, build_file_touch_preview, format_show_result, BestOfNRunner}; -use super::{ToolContext, Registry}; +use super::best_of_n::{ + BestOfNRunner, build_allowed_tool_set, build_file_touch_preview, format_show_result, +}; +use super::{Registry, ToolContext}; use crate::provider::Provider; use anyhow::Result; use async_trait::async_trait; -use jcode_best_of_n::types::*; -use jcode_best_of_n::config::TemperatureStrategyConfig; -use jcode_best_of_n::strategies; use jcode_best_of_n::BestOfNConfig; use jcode_best_of_n::BestOfNMode; +use jcode_best_of_n::config::TemperatureStrategyConfig; +use jcode_best_of_n::strategies; +use jcode_best_of_n::types::*; use jcode_message_types::ToolDefinition; use jcode_provider_core::EventStream; use std::sync::Arc; @@ -23,7 +25,9 @@ impl Provider for MockProvider { _system: &str, _resume_session_id: Option<&str>, ) -> Result { - Err(anyhow::anyhow!("MockProvider should not be called in tests")) + Err(anyhow::anyhow!( + "MockProvider should not be called in tests" + )) } fn name(&self) -> &str { @@ -154,7 +158,12 @@ fn test_apply_winner_no_winner_returns_error() { }; let registry = Registry::empty(); let err = runner - .apply_winner(&result, &vec!["src/main.rs".to_string()], ®istry, &make_tool_context()) + .apply_winner( + &result, + &vec!["src/main.rs".to_string()], + ®istry, + &make_tool_context(), + ) .await .unwrap_err(); assert!(err.to_string().contains("no winner")); @@ -230,7 +239,11 @@ fn test_config_off_mode_disabled() { #[test] fn test_strategy_temperature_spread() { - let temps = TemperatureStrategyConfig { min: 0.2, max: 0.8, values: vec![] }; + let temps = TemperatureStrategyConfig { + min: 0.2, + max: 0.8, + values: vec![], + }; let strategies = strategies::generate_strategies(4, &temps); assert_eq!(strategies.len(), 4); let temp_values: Vec = strategies.iter().map(|s| s.temperature).collect(); diff --git a/crates/jcode-app-core/src/tool/computer/ax.rs b/crates/jcode-app-core/src/tool/computer/ax.rs index d1ae8c2e1b..82099fc7aa 100644 --- a/crates/jcode-app-core/src/tool/computer/ax.rs +++ b/crates/jcode-app-core/src/tool/computer/ax.rs @@ -54,7 +54,10 @@ fn tell(app: &str, body: &str) -> String { /// Dump the AX tree of an app (or the frontmost app) to a given depth. pub fn ui_tree(app: Option<&str>, depth: u32) -> Result { let target = match app { - Some(a) => format!("first application process whose name is {}", osa::as_quote(a)), + Some(a) => format!( + "first application process whose name is {}", + osa::as_quote(a) + ), None => "first application process whose frontmost is true".to_string(), }; let script = format!( @@ -211,7 +214,10 @@ end tell /// Perform AXPress on an element (background click). pub fn press(handle: &ElementHandle) -> Result { - let body = format!("perform action \"AXPress\" of ({})", handle.resolve_script()); + let body = format!( + "perform action \"AXPress\" of ({})", + handle.resolve_script() + ); osa::run_applescript_timeout(&tell(&handle.app, &body), Duration::from_secs(10))?; Ok(ToolOutput::new(format!( "pressed element {:?} in {} (no cursor movement)", @@ -342,7 +348,9 @@ end tell y = y ); let res = osa::run_applescript(&script)?; - Ok(ToolOutput::new(format!("Deepest element at ({x:.0},{y:.0}) in {app}:\n{res}")) - .with_title("element_at") - .with_metadata(json!({"app": app, "x": x, "y": y}))) + Ok(ToolOutput::new(format!( + "Deepest element at ({x:.0},{y:.0}) in {app}:\n{res}" + )) + .with_title("element_at") + .with_metadata(json!({"app": app, "x": x, "y": y}))) } diff --git a/crates/jcode-app-core/src/tool/computer/coverage_tests.rs b/crates/jcode-app-core/src/tool/computer/coverage_tests.rs index 87428d6d5b..f049e1562a 100644 --- a/crates/jcode-app-core/src/tool/computer/coverage_tests.rs +++ b/crates/jcode-app-core/src/tool/computer/coverage_tests.rs @@ -98,7 +98,11 @@ async fn coverage_ax() { let el = json!({"app":"TextEdit","path":[1,1]}); ok(json!({"action":"set_value","element":el,"value":"ax-coverage"})).await; let g = ok(json!({"action":"get_value","element":el})).await; - assert!(g.output.contains("ax-coverage"), "get_value got: {}", g.output); + assert!( + g.output.contains("ax-coverage"), + "get_value got: {}", + g.output + ); textedit_quit().await; } @@ -108,7 +112,8 @@ async fn coverage_select_menu() { textedit_new().await; // Format menu exists in TextEdit; "Make Plain Text" or "Wrap to Page" toggles. // Use a stable, reversible item: Edit > Select All. - let r = act(json!({"action":"select_menu","app":"TextEdit","menu_path":["Edit","Select All"]})).await; + let r = act(json!({"action":"select_menu","app":"TextEdit","menu_path":["Edit","Select All"]})) + .await; match r { Ok(o) => eprintln!("PASS select_menu -> {}", o.output), Err(e) => panic!("FAIL select_menu -> {e}"), @@ -150,7 +155,8 @@ async fn coverage_clipboard_scripting_system() { ok(json!({"action":"notify","text":"jcode coverage test","title":"jcode"})).await; // wait_for against a known app/text with short timeout (Finder always has a menu) textedit_new().await; - let _ = act(json!({"action":"wait_for","app":"TextEdit","contains":"","timeout_ms":1500})).await; + let _ = + act(json!({"action":"wait_for","app":"TextEdit","contains":"","timeout_ms":1500})).await; textedit_quit().await; // set_brightness may be unavailable; tolerate. match act(json!({"action":"set_brightness","level":0.8})).await { @@ -166,7 +172,8 @@ async fn coverage_destructive_quit_close() { ok(json!({"action":"close_window","app":"TextEdit"})).await; // A new empty doc closes without a sheet. Discard anything then quit. let _ = act(json!({"action":"run_applescript","script": - "tell application \"TextEdit\" to close every document saving no"})).await; + "tell application \"TextEdit\" to close every document saving no"})) + .await; ok(json!({"action":"quit_app","app":"TextEdit"})).await; } diff --git a/crates/jcode-app-core/src/tool/computer/mod.rs b/crates/jcode-app-core/src/tool/computer/mod.rs index 70c2e63303..129def70e5 100644 --- a/crates/jcode-app-core/src/tool/computer/mod.rs +++ b/crates/jcode-app-core/src/tool/computer/mod.rs @@ -280,14 +280,18 @@ fn dispatch(action: &str, input: &ComputerInput) -> Result { "screenshot" => screen::screenshot(), "ocr" => screen::ocr(input.region), "window_screenshot" => { - let id = input.window_id.context("window_screenshot requires `window_id`")?; + let id = input + .window_id + .context("window_screenshot requires `window_id`")?; screen::window_screenshot(id) } "ui" => ax::ui_tree(input.app.as_deref(), input.depth.unwrap_or(12)), "cursor" => { let p = input::current_cursor()?; - Ok(ToolOutput::new(format!("cursor at ({:.0}, {:.0})", p.x, p.y)) - .with_metadata(json!({ "x": p.x, "y": p.y }))) + Ok( + ToolOutput::new(format!("cursor at ({:.0}, {:.0})", p.x, p.y)) + .with_metadata(json!({ "x": p.x, "y": p.y })), + ) } // ---- coordinate input (visible) ---- @@ -298,15 +302,24 @@ fn dispatch(action: &str, input: &ComputerInput) -> Result { } "click" => { let p = input::click(input.x, input.y, input::Button::Left, 1)?; - Ok(ToolOutput::new(format!("clicked at ({:.0}, {:.0})", p.x, p.y))) + Ok(ToolOutput::new(format!( + "clicked at ({:.0}, {:.0})", + p.x, p.y + ))) } "double_click" => { let p = input::click(input.x, input.y, input::Button::Left, 2)?; - Ok(ToolOutput::new(format!("double-clicked at ({:.0}, {:.0})", p.x, p.y))) + Ok(ToolOutput::new(format!( + "double-clicked at ({:.0}, {:.0})", + p.x, p.y + ))) } "right_click" => { let p = input::click(input.x, input.y, input::Button::Right, 1)?; - Ok(ToolOutput::new(format!("right-clicked at ({:.0}, {:.0})", p.x, p.y))) + Ok(ToolOutput::new(format!( + "right-clicked at ({:.0}, {:.0})", + p.x, p.y + ))) } "drag" => { let (x, y) = require_xy(input)?; @@ -336,7 +349,10 @@ fn dispatch(action: &str, input: &ComputerInput) -> Result { .filter(|s| !s.is_empty()) .context("action='type' requires non-empty `text`")?; input::type_text(text)?; - Ok(ToolOutput::new(format!("typed {} characters", text.chars().count()))) + Ok(ToolOutput::new(format!( + "typed {} characters", + text.chars().count() + ))) } "key" => { let keys = input @@ -359,7 +375,10 @@ fn dispatch(action: &str, input: &ComputerInput) -> Result { // ---- AX background actions (Tier 1) ---- "find_element" => { - let app = input.app.as_deref().context("find_element requires `app`")?; + let app = input + .app + .as_deref() + .context("find_element requires `app`")?; ax::find_element( app, input.role.as_deref(), @@ -376,16 +395,25 @@ fn dispatch(action: &str, input: &ComputerInput) -> Result { "press" => ax::press(&parse_element(input)?), "get_value" => ax::get_value(&parse_element(input)?), "set_value" => { - let v = input.value.as_deref().context("set_value requires `value`")?; + let v = input + .value + .as_deref() + .context("set_value requires `value`")?; ax::set_value(&parse_element(input)?, v) } "perform_action" => { - let a = input.ax_action.as_deref().context("perform_action requires `ax_action`")?; + let a = input + .ax_action + .as_deref() + .context("perform_action requires `ax_action`")?; ax::perform_action(&parse_element(input)?, a) } "select_menu" => { let app = input.app.as_deref().context("select_menu requires `app`")?; - let path = input.menu_path.as_ref().context("select_menu requires `menu_path`")?; + let path = input + .menu_path + .as_ref() + .context("select_menu requires `menu_path`")?; ax::select_menu(app, path) } @@ -411,20 +439,32 @@ fn dispatch(action: &str, input: &ComputerInput) -> Result { // ---- clipboard / scripting / system (Tier 3/4) ---- "get_clipboard" => sys::get_clipboard(), "set_clipboard" => { - let t = input.text.as_deref().context("set_clipboard requires `text`")?; + let t = input + .text + .as_deref() + .context("set_clipboard requires `text`")?; sys::set_clipboard(t) } "run_applescript" => { - let s = input.script.as_deref().context("run_applescript requires `script`")?; + let s = input + .script + .as_deref() + .context("run_applescript requires `script`")?; sys::run_applescript(s) } "run_jxa" => { - let s = input.script.as_deref().context("run_jxa requires `script`")?; + let s = input + .script + .as_deref() + .context("run_jxa requires `script`")?; sys::run_jxa(s) } "wait_for" => { let app = input.app.as_deref().context("wait_for requires `app`")?; - let c = input.contains.as_deref().context("wait_for requires `contains`")?; + let c = input + .contains + .as_deref() + .context("wait_for requires `contains`")?; sys::wait_for(app, c, input.timeout_ms.unwrap_or(10_000)) } "notify" => { @@ -433,7 +473,9 @@ fn dispatch(action: &str, input: &ComputerInput) -> Result { } "system_state" => sys::system_state(), "set_brightness" => { - let l = input.level.context("set_brightness requires `level` (0..1)")?; + let l = input + .level + .context("set_brightness requires `level` (0..1)")?; sys::set_brightness(l) } @@ -461,14 +503,13 @@ fn req_app<'a>(input: &'a ComputerInput) -> Result<&'a str> { #[cfg(target_os = "macos")] fn parse_element(input: &ComputerInput) -> Result { - let raw = input - .element - .clone() - .context("this action requires an `element` handle {app, path:[...]} from find_element/ui")?; + let raw = input.element.clone().context( + "this action requires an `element` handle {app, path:[...]} from find_element/ui", + )?; serde_json::from_value(raw).context("invalid `element` handle") } -#[cfg(all(test, target_os = "macos"))] -mod tests; #[cfg(all(test, target_os = "macos"))] mod coverage_tests; +#[cfg(all(test, target_os = "macos"))] +mod tests; diff --git a/crates/jcode-app-core/src/tool/computer/screen.rs b/crates/jcode-app-core/src/tool/computer/screen.rs index 0cc9f015c4..8ab35cd498 100644 --- a/crates/jcode-app-core/src/tool/computer/screen.rs +++ b/crates/jcode-app-core/src/tool/computer/screen.rs @@ -84,7 +84,9 @@ pub fn window_screenshot(window_id: i64) -> Result { )) .with_title("window screenshot") .with_labeled_image("image/png", STANDARD.encode(&bytes), "window") - .with_metadata(json!({ "window_id": window_id, "width_pixels": pixel_w, "height_pixels": pixel_h }))) + .with_metadata( + json!({ "window_id": window_id, "width_pixels": pixel_w, "height_pixels": pixel_h }), + )) } /// OCR a region (or the whole screen) using the macOS Vision framework via a diff --git a/crates/jcode-app-core/src/tool/computer/setup.rs b/crates/jcode-app-core/src/tool/computer/setup.rs index 7a9db94227..821f80c151 100644 --- a/crates/jcode-app-core/src/tool/computer/setup.rs +++ b/crates/jcode-app-core/src/tool/computer/setup.rs @@ -24,7 +24,9 @@ fn screen_recording_ok() -> bool { .status() .map(|s| s.success()) .unwrap_or(false) - && std::fs::metadata(&tmp).map(|m| m.len() > 0).unwrap_or(false); + && std::fs::metadata(&tmp) + .map(|m| m.len() > 0) + .unwrap_or(false); let _ = std::fs::remove_file(&tmp); ok } @@ -47,7 +49,10 @@ pub fn check_permissions() -> Result { let mut lines = vec![ format!("Accessibility (input + AX control): {}", yes_no(ax)), format!("Screen Recording (screenshots/OCR): {}", yes_no(screen)), - format!("Swift toolchain (for OCR): {}", if swift { "present" } else { "missing" }), + format!( + "Swift toolchain (for OCR): {}", + if swift { "present" } else { "missing" } + ), ]; if !ax || !screen { lines.push("Run action='setup' to request these and open the right settings pane.".into()); @@ -63,7 +68,10 @@ pub fn setup() -> Result { let ax0 = accessibility_ok(); let screen0 = screen_recording_ok(); - log.push(format!("Initial: accessibility={}, screen_recording={}", ax0, screen0)); + log.push(format!( + "Initial: accessibility={}, screen_recording={}", + ax0, screen0 + )); // Trigger the Screen Recording prompt by attempting a capture (already done // in screen_recording_ok). For Accessibility, prompt + pre-add jcode by @@ -102,13 +110,20 @@ pub fn setup() -> Result { } log.push(format!( "Accessibility after wait: {}", - if granted { "granted" } else { "still not granted (toggle it, then re-run check_permissions)" } + if granted { + "granted" + } else { + "still not granted (toggle it, then re-run check_permissions)" + } )); } let ax = accessibility_ok(); let screen = screen_recording_ok(); - log.push(format!("Final: accessibility={}, screen_recording={}", ax, screen)); + log.push(format!( + "Final: accessibility={}, screen_recording={}", + ax, screen + )); if !ax { log.push( "NOTE: the Accessibility toggle cannot be enabled programmatically (macOS security). \ diff --git a/crates/jcode-app-core/src/tool/computer/sys.rs b/crates/jcode-app-core/src/tool/computer/sys.rs index d1f091b962..3d87200f7c 100644 --- a/crates/jcode-app-core/src/tool/computer/sys.rs +++ b/crates/jcode-app-core/src/tool/computer/sys.rs @@ -31,7 +31,10 @@ pub fn set_clipboard(text: &str) -> Result { child .wait() .map_err(|e| anyhow::anyhow!("pbcopy failed: {e}"))?; - Ok(ToolOutput::new(format!("copied {} chars to clipboard", text.chars().count()))) + Ok(ToolOutput::new(format!( + "copied {} chars to clipboard", + text.chars().count() + ))) } pub fn run_applescript(script: &str) -> Result { diff --git a/crates/jcode-app-core/src/tool/computer/tests.rs b/crates/jcode-app-core/src/tool/computer/tests.rs index 9380c4a22b..977ebacbb8 100644 --- a/crates/jcode-app-core/src/tool/computer/tests.rs +++ b/crates/jcode-app-core/src/tool/computer/tests.rs @@ -27,7 +27,10 @@ async fn rejects_bad_action() { let err = run_action(json!({ "action": "frobnicate" })) .await .unwrap_err(); - assert!(err.to_string().contains("Unknown macos_computer_use action")); + assert!( + err.to_string() + .contains("Unknown macos_computer_use action") + ); } #[tokio::test] @@ -42,7 +45,13 @@ async fn discover_all_lists_actions() { .await .unwrap(); // Spot-check that several categories are present. - for needle in ["press", "set_value", "run_applescript", "list_windows", "screenshot"] { + for needle in [ + "press", + "set_value", + "run_applescript", + "list_windows", + "screenshot", + ] { assert!(out.output.contains(needle), "missing {needle}"); } } @@ -161,7 +170,9 @@ async fn live_ui_tree() { #[tokio::test] #[ignore = "requires GUI + permissions"] async fn live_list_windows() { - let out = run_action(json!({ "action": "list_windows" })).await.unwrap(); + let out = run_action(json!({ "action": "list_windows" })) + .await + .unwrap(); eprintln!("{}", out.output); } @@ -171,7 +182,9 @@ async fn live_clipboard_roundtrip() { run_action(json!({ "action": "set_clipboard", "text": "jcode-clip-test" })) .await .unwrap(); - let out = run_action(json!({ "action": "get_clipboard" })).await.unwrap(); + let out = run_action(json!({ "action": "get_clipboard" })) + .await + .unwrap(); assert!(out.output.contains("jcode-clip-test")); } diff --git a/crates/jcode-app-core/src/tool/computer/win.rs b/crates/jcode-app-core/src/tool/computer/win.rs index 336eef3d27..4aba89ae08 100644 --- a/crates/jcode-app-core/src/tool/computer/win.rs +++ b/crates/jcode-app-core/src/tool/computer/win.rs @@ -8,12 +8,19 @@ pub fn list_apps() -> Result { let script = "tell application \"System Events\" to get name of every application process whose background only is false"; let res = osa::run_applescript(script)?; let apps: Vec = res.split(", ").map(|s| s.trim().to_string()).collect(); - Ok(ToolOutput::new(format!("Running apps ({}):\n{}", apps.len(), apps.join("\n"))) - .with_title("list_apps")) + Ok(ToolOutput::new(format!( + "Running apps ({}):\n{}", + apps.len(), + apps.join("\n") + )) + .with_title("list_apps")) } pub fn activate_app(app: &str) -> Result { - osa::run_applescript(&format!("tell application {} to activate", osa::as_quote(app)))?; + osa::run_applescript(&format!( + "tell application {} to activate", + osa::as_quote(app) + ))?; Ok(ToolOutput::new(format!("activated {app}"))) } @@ -69,7 +76,11 @@ out.join('\n'); let res = osa::run_jxa(script)?; Ok(ToolOutput::new(format!( "Windows (id owner title bounds):\n{}", - if res.trim().is_empty() { "(none)" } else { &res } + if res.trim().is_empty() { + "(none)" + } else { + &res + } )) .with_title("list_windows")) } @@ -89,17 +100,25 @@ pub fn focus_window(app: &str) -> Result { pub fn move_window(app: &str, x: f64, y: f64) -> Result { osa::run_applescript(&format!( "tell application \"System Events\" to set position of front window of (first process whose name is {}) to {{{x}, {y}}}", - osa::as_quote(app), x = x as i64, y = y as i64 + osa::as_quote(app), + x = x as i64, + y = y as i64 ))?; - Ok(ToolOutput::new(format!("moved {app} front window to ({x:.0},{y:.0})"))) + Ok(ToolOutput::new(format!( + "moved {app} front window to ({x:.0},{y:.0})" + ))) } pub fn resize_window(app: &str, w: f64, h: f64) -> Result { osa::run_applescript(&format!( "tell application \"System Events\" to set size of front window of (first process whose name is {}) to {{{w}, {h}}}", - osa::as_quote(app), w = w as i64, h = h as i64 + osa::as_quote(app), + w = w as i64, + h = h as i64 ))?; - Ok(ToolOutput::new(format!("resized {app} front window to {w:.0}x{h:.0}"))) + Ok(ToolOutput::new(format!( + "resized {app} front window to {w:.0}x{h:.0}" + ))) } pub fn minimize_window(app: &str) -> Result { diff --git a/crates/jcode-app-core/src/tool/ffs_glob.rs b/crates/jcode-app-core/src/tool/ffs_glob.rs index 68533d786f..989b211689 100644 --- a/crates/jcode-app-core/src/tool/ffs_glob.rs +++ b/crates/jcode-app-core/src/tool/ffs_glob.rs @@ -135,7 +135,10 @@ fn glob_search_blocking( max_files: usize, ) -> Result> { let files = ffs_search::glob_matcher::glob_files(base, pattern, max_files); - Ok(files.into_iter().map(|s| (s, std::time::UNIX_EPOCH)).collect()) + Ok(files + .into_iter() + .map(|s| (s, std::time::UNIX_EPOCH)) + .collect()) } /// Fuzzy search via ffs-search's fuzzy_file_search (case-insensitive @@ -145,7 +148,7 @@ fn fuzzy_search_blocking( query: &str, max_files: usize, ) -> Result> { - use ffs_search::fuzzy_file_search::{fuzzy_search_files, FuzzySearchOptions}; + use ffs_search::fuzzy_file_search::{FuzzySearchOptions, fuzzy_search_files}; let matches = fuzzy_search_files( base, query, @@ -154,7 +157,10 @@ fn fuzzy_search_blocking( ..Default::default() }, ); - Ok(matches.into_iter().map(|m| (m.path, std::time::UNIX_EPOCH)).collect()) + Ok(matches + .into_iter() + .map(|m| (m.path, std::time::UNIX_EPOCH)) + .collect()) } #[cfg(test)] diff --git a/crates/jcode-app-core/src/tool/ffs_multi_grep.rs b/crates/jcode-app-core/src/tool/ffs_multi_grep.rs index e6756de781..e8a0a5c387 100644 --- a/crates/jcode-app-core/src/tool/ffs_multi_grep.rs +++ b/crates/jcode-app-core/src/tool/ffs_multi_grep.rs @@ -277,11 +277,7 @@ mod tests { let temp_dir = tempfile::tempdir().unwrap(); let file_path = temp_dir.path().join("app.rs"); let mut file = std::fs::File::create(&file_path).unwrap(); - write!( - file, - "pub fn main() {{\n let msg = \"hello\";\n}}\n" - ) - .unwrap(); + write!(file, "pub fn main() {{\n let msg = \"hello\";\n}}\n").unwrap(); let ctx = ToolContext { session_id: "test".to_string(), diff --git a/crates/jcode-app-core/src/tool/ffs_outline.rs b/crates/jcode-app-core/src/tool/ffs_outline.rs index 2245f53d51..ced8a6f62b 100644 --- a/crates/jcode-app-core/src/tool/ffs_outline.rs +++ b/crates/jcode-app-core/src/tool/ffs_outline.rs @@ -293,11 +293,7 @@ pub fn main() -> Result<()> { "should detect struct: {:?}", kinds ); - assert!( - kinds.contains(&"fn"), - "should detect fn: {:?}", - kinds - ); + assert!(kinds.contains(&"fn"), "should detect fn: {:?}", kinds); } #[test] @@ -333,11 +329,7 @@ export class MyComponent { "should detect interface: {:?}", kinds ); - assert!( - kinds.contains(&"fn"), - "should detect fn: {:?}", - kinds - ); + assert!(kinds.contains(&"fn"), "should detect fn: {:?}", kinds); // export class MyComponent is parsed as export_statement by tree-sitter, // so the kind is "export" not "class". The class body's methods appear // as child entries within the export node. @@ -371,7 +363,10 @@ export class MyComponent { let content = "some random content"; let path = Path::new("test.unknown"); let items = outline_blocking(content, path, 20); - assert!(items.is_empty(), "unknown extension should produce no items"); + assert!( + items.is_empty(), + "unknown extension should produce no items" + ); } #[test] diff --git a/crates/jcode-app-core/src/tool/ffs_symbol.rs b/crates/jcode-app-core/src/tool/ffs_symbol.rs index 669b31d359..29a8009f4a 100644 --- a/crates/jcode-app-core/src/tool/ffs_symbol.rs +++ b/crates/jcode-app-core/src/tool/ffs_symbol.rs @@ -306,7 +306,10 @@ fn build_symbol_index(root: &Path) -> Result { /// file cannot be read or the line does not exist. fn read_line_text(path: &Path, line_num: usize) -> Option { let content = std::fs::read_to_string(path).ok()?; - content.lines().nth(line_num - 1).map(|l| l.trim().to_string()) + content + .lines() + .nth(line_num - 1) + .map(|l| l.trim().to_string()) } /// Map the tree-sitter node `kind` string to the short display label used by @@ -393,8 +396,11 @@ mod tests { let tmp = tempfile::tempdir().unwrap(); let f = tmp.path().join("lib.rs"); let mut file = std::fs::File::create(&f).unwrap(); - write!(file, "pub fn hello_world() {{\n println!(\"hello\");\n}}\n\nfn helper() {{}}\n") - .unwrap(); + write!( + file, + "pub fn hello_world() {{\n println!(\"hello\");\n}}\n\nfn helper() {{}}\n" + ) + .unwrap(); let idx = test_index(tmp.path()); let results = idx.lookup_exact("hello_world"); @@ -468,11 +474,7 @@ mod tests { fn read_line_text_returns_correct_line() { let tmp = tempfile::tempdir().unwrap(); let f = tmp.path().join("test.rs"); - std::fs::write( - &f, - "line one\n line two\nline three\n", - ) - .unwrap(); + std::fs::write(&f, "line one\n line two\nline three\n").unwrap(); assert_eq!(read_line_text(&f, 1).as_deref(), Some("line one")); assert_eq!(read_line_text(&f, 2).as_deref(), Some("line two")); diff --git a/crates/jcode-app-core/src/tool/hashline_edit.rs b/crates/jcode-app-core/src/tool/hashline_edit.rs index 3ce009851f..5b957e9367 100644 --- a/crates/jcode-app-core/src/tool/hashline_edit.rs +++ b/crates/jcode-app-core/src/tool/hashline_edit.rs @@ -259,14 +259,7 @@ impl Tool for HashlineEditTool { old_string.lines().next().unwrap_or(""), params.new_string.lines().next().unwrap_or("") )); - publish_edit_event( - &ctx, - params.intent, - &path, - start_line, - end_line, - detail, - ); + publish_edit_event(&ctx, params.intent, &path, start_line, end_line, detail); Ok(ToolOutput::new(format!( "Edited {}: lines {}-{} (anchor verified)\n old: {}\n new: {}", @@ -355,10 +348,9 @@ impl Tool for HashlineEditTool { .map_err(|e| anyhow::anyhow!("invalid range anchor {anchor_str:?}: {e}"))?; let index = doc.build_index(); let (start_resolved, end_resolved) = - hashline::anchor::resolve_range(&range, &doc, &index) - .map_err(|e| { - anyhow::anyhow!("failed to resolve range {anchor_str:?}: {e}") - })?; + hashline::anchor::resolve_range(&range, &doc, &index).map_err(|e| { + anyhow::anyhow!("failed to resolve range {anchor_str:?}: {e}") + })?; let start_line = start_resolved.line_no; let end_line = end_resolved.line_no; @@ -390,7 +382,10 @@ impl Tool for HashlineEditTool { &path, start_line, end_line, - Some(format!("lines {}-{}: range replacement", start_line, end_line)), + Some(format!( + "lines {}-{}: range replacement", + start_line, end_line + )), ); let old_preview = old_lines @@ -408,8 +403,10 @@ impl Tool for HashlineEditTool { let anchor = hashline::anchor::parse_anchor(&anchor_str) .map_err(|e| anyhow::anyhow!("invalid anchor {anchor_str:?}: {e}"))?; let index = doc.build_index(); - let resolved = hashline::anchor::resolve(&anchor, &doc, &index) - .map_err(|e| anyhow::anyhow!("failed to resolve anchor {anchor_str:?}: {e}"))?; + let resolved = + hashline::anchor::resolve(&anchor, &doc, &index).map_err(|e| { + anyhow::anyhow!("failed to resolve anchor {anchor_str:?}: {e}") + })?; let old_line = doc.lines[resolved.index].content.to_string(); diff --git a/crates/jcode-app-core/src/tool/mod.rs b/crates/jcode-app-core/src/tool/mod.rs index 73976af7e0..3a4a0e3db2 100644 --- a/crates/jcode-app-core/src/tool/mod.rs +++ b/crates/jcode-app-core/src/tool/mod.rs @@ -3,6 +3,7 @@ pub mod ambient; mod apply_patch; mod bash; mod batch; +mod beads; mod best_of_n; mod bg; mod browser; @@ -21,7 +22,6 @@ mod ffs_multi_grep; mod ffs_outline; mod ffs_symbol; mod gmail; -mod beads; mod goal; mod hashline_edit; @@ -385,11 +385,36 @@ impl Registry { "initiative", goal::InitiativeTool::new, ); - Self::insert_tool_timed(&mut m, &mut timings, "beads_list", beads::BeadsListTool::new); - Self::insert_tool_timed(&mut m, &mut timings, "beads_create", beads::BeadsCreateTool::new); - Self::insert_tool_timed(&mut m, &mut timings, "beads_ready", beads::BeadsReadyTool::new); - Self::insert_tool_timed(&mut m, &mut timings, "beads_claim", beads::BeadsClaimTool::new); - Self::insert_tool_timed(&mut m, &mut timings, "beads_close", beads::BeadsCloseTool::new); + Self::insert_tool_timed( + &mut m, + &mut timings, + "beads_list", + beads::BeadsListTool::new, + ); + Self::insert_tool_timed( + &mut m, + &mut timings, + "beads_create", + beads::BeadsCreateTool::new, + ); + Self::insert_tool_timed( + &mut m, + &mut timings, + "beads_ready", + beads::BeadsReadyTool::new, + ); + Self::insert_tool_timed( + &mut m, + &mut timings, + "beads_claim", + beads::BeadsClaimTool::new, + ); + Self::insert_tool_timed( + &mut m, + &mut timings, + "beads_close", + beads::BeadsCloseTool::new, + ); Self::insert_tool_timed(&mut m, &mut timings, "beads_dep", beads::BeadsDepTool::new); Self::insert_tool_timed(&mut m, &mut timings, "gmail", gmail::GmailTool::new); Self::insert_tool_timed( diff --git a/crates/jcode-app-core/src/tool/propose_edit.rs b/crates/jcode-app-core/src/tool/propose_edit.rs index 4c76a95a46..e64db4394b 100644 --- a/crates/jcode-app-core/src/tool/propose_edit.rs +++ b/crates/jcode-app-core/src/tool/propose_edit.rs @@ -1,4 +1,4 @@ -use super::{get_best_of_n_handle, Tool, ToolContext, ToolOutput}; +use super::{Tool, ToolContext, ToolOutput, get_best_of_n_handle}; use anyhow::Result; use async_trait::async_trait; use jcode_best_of_n::ProposedContentStore; diff --git a/crates/jcode-app-core/src/tool/propose_hashline_edit.rs b/crates/jcode-app-core/src/tool/propose_hashline_edit.rs index 8975a4f999..e5756020f1 100644 --- a/crates/jcode-app-core/src/tool/propose_hashline_edit.rs +++ b/crates/jcode-app-core/src/tool/propose_hashline_edit.rs @@ -1,4 +1,4 @@ -use super::{get_best_of_n_handle, Tool, ToolContext, ToolOutput}; +use super::{Tool, ToolContext, ToolOutput, get_best_of_n_handle}; use anyhow::Result; use async_trait::async_trait; use hashline::sha256_window; @@ -147,11 +147,7 @@ impl Tool for ProposeHashlineEditTool { .map_err(|e| anyhow::anyhow!("{e}"))?; // Generate diff preview - let diff = generate_diff( - ¶ms.old_string, - ¶ms.new_string, - start_line, - ); + let diff = generate_diff(¶ms.old_string, ¶ms.new_string, start_line); let preview = build_file_touch_preview(&diff); // Write the proposed content to the store. is_new_file = false diff --git a/crates/jcode-app-core/src/tool/propose_write.rs b/crates/jcode-app-core/src/tool/propose_write.rs index 4c7d5e895a..cb83f3d414 100644 --- a/crates/jcode-app-core/src/tool/propose_write.rs +++ b/crates/jcode-app-core/src/tool/propose_write.rs @@ -90,9 +90,13 @@ impl Tool for ProposeWriteTool { // so it matches original_files[].file_path in build_diffs. let file_key = params.file_path.clone(); let run_id_typed = jcode_best_of_n::RunId(run_id.clone()); - handle - .store - .set_proposed(&run_id_typed, file_key, ¶ms.content, candidate_id.clone(), !existed); + handle.store.set_proposed( + &run_id_typed, + file_key, + ¶ms.content, + candidate_id.clone(), + !existed, + ); let line_count = params.content.lines().count(); let diff = match old_content.as_deref() { diff --git a/crates/jcode-base/src/goal.rs b/crates/jcode-base/src/goal.rs index 82924f7204..1adddccfbd 100644 --- a/crates/jcode-base/src/goal.rs +++ b/crates/jcode-base/src/goal.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::path::Path; -use crate::side_panel::{SidePanelSnapshot, snapshot_for_session, write_markdown_page, focus_page}; +use crate::side_panel::{SidePanelSnapshot, focus_page, snapshot_for_session, write_markdown_page}; // Re-export beads-backed types pub use jcode_beads_bridge::mapping::{Goal, GoalMilestone, GoalStep, ToBeadsEpic, ToJcodeGoal}; @@ -174,7 +174,10 @@ fn open_beads(working_dir: Option<&Path>) -> Result) -> Result { let project = open_beads(working_dir)?; - let id = input.id.clone().unwrap_or_else(|| format!("goal-{}", short_id())); + let id = input + .id + .clone() + .unwrap_or_else(|| format!("goal-{}", short_id())); let goal = Goal { id, @@ -240,7 +243,12 @@ pub fn load_goal( Ok(p) => p, Err(_) => return Ok(None), }; - Ok(project.storage().get_issue(id).ok().flatten().map(|i| i.to_goal())) + Ok(project + .storage() + .get_issue(id) + .ok() + .flatten() + .map(|i| i.to_goal())) } /// List all relevant goals (Epic issues). @@ -257,7 +265,9 @@ pub fn list_relevant_goals(working_dir: Option<&Path>) -> Result> { /// Resume a goal for a session. pub fn resume_goal(session_id: &str, working_dir: Option<&Path>) -> Result> { let goals = list_relevant_goals(working_dir)?; - Ok(goals.into_iter().find(|g| g.id == session_id || g.title.contains(session_id))) + Ok(goals + .into_iter() + .find(|g| g.id == session_id || g.title.contains(session_id))) } /// Attach a goal to a session (adds a label). @@ -267,7 +277,9 @@ pub fn attach_goal_to_session( working_dir: Option<&Path>, ) -> Result<()> { let project = open_beads(working_dir)?; - project.storage_mut().add_label(&goal.id, &format!("session:{session_id}"), "jcode")?; + project + .storage_mut() + .add_label(&goal.id, &format!("session:{session_id}"), "jcode")?; project.flush()?; Ok(()) } @@ -275,11 +287,13 @@ pub fn attach_goal_to_session( /// Load the goal attached to a session. pub fn load_attached_goal(session_id: &str, working_dir: Option<&Path>) -> Result> { let project = open_beads(working_dir)?; - let issues = project.storage().list_issues(&beads_rust::storage::ListFilters { - types: Some(vec![beads_rust::model::IssueType::Epic]), - labels: Some(vec![format!("session:{session_id}")]), - ..Default::default() - })?; + let issues = project + .storage() + .list_issues(&beads_rust::storage::ListFilters { + types: Some(vec![beads_rust::model::IssueType::Epic]), + labels: Some(vec![format!("session:{session_id}")]), + ..Default::default() + })?; Ok(issues.into_iter().next().map(|i| i.to_goal())) } @@ -325,7 +339,13 @@ pub fn write_goal_page( let _ = (working_dir, display); let content = format_goal_detail(goal); let page_id = goal_page_id(&goal.id); - write_markdown_page(session_id, &page_id, Some(&format!("Epic: {}", goal.title)), &content, true)?; + write_markdown_page( + session_id, + &page_id, + Some(&format!("Epic: {}", goal.title)), + &content, + true, + )?; focus_page(session_id, &page_id) } @@ -340,8 +360,16 @@ pub fn open_goal_for_session( Some(g) => g, None => return Ok(None), }; - let snapshot = write_goal_page(session_id, working_dir, &goal, - if explicit_focus { GoalDisplayMode::Focus } else { GoalDisplayMode::Auto })?; + let snapshot = write_goal_page( + session_id, + working_dir, + &goal, + if explicit_focus { + GoalDisplayMode::Focus + } else { + GoalDisplayMode::Auto + }, + )?; Ok(Some(GoalDisplayResult { goal, snapshot })) } @@ -355,8 +383,16 @@ pub fn resume_goal_for_session( Some(g) => g, None => return Ok(None), }; - let snapshot = write_goal_page(session_id, working_dir, &goal, - if explicit_focus { GoalDisplayMode::Focus } else { GoalDisplayMode::Auto })?; + let snapshot = write_goal_page( + session_id, + working_dir, + &goal, + if explicit_focus { + GoalDisplayMode::Focus + } else { + GoalDisplayMode::Auto + }, + )?; Ok(Some(GoalDisplayResult { goal, snapshot })) } diff --git a/crates/jcode-base/src/provider/anthropic.rs b/crates/jcode-base/src/provider/anthropic.rs index 7189c17d78..ef6a03e2cf 100644 --- a/crates/jcode-base/src/provider/anthropic.rs +++ b/crates/jcode-base/src/provider/anthropic.rs @@ -1319,9 +1319,7 @@ impl Provider for AnthropicProvider { oauth_preflight_done: Arc::new(AtomicBool::new( self.oauth_preflight_done.load(Ordering::Relaxed), )), - temperature: Arc::new(AtomicU32::new( - self.temperature.load(Ordering::Acquire), - )), + temperature: Arc::new(AtomicU32::new(self.temperature.load(Ordering::Acquire))), }) } diff --git a/crates/jcode-base/src/provider/openai.rs b/crates/jcode-base/src/provider/openai.rs index bf72d966b8..08eee20d46 100644 --- a/crates/jcode-base/src/provider/openai.rs +++ b/crates/jcode-base/src/provider/openai.rs @@ -864,8 +864,11 @@ impl OpenAIProvider { /// A `/responses` suffix is not expected here (it is appended by callers), /// so a trailing `/responses` is trimmed to avoid `.../responses/responses`. pub(crate) fn resolve_api_base() -> String { - const OVERRIDE_VARS: [&str; 3] = - ["JCODE_OPENAI_API_BASE", "OPENAI_BASE_URL", "OPENAI_API_BASE"]; + const OVERRIDE_VARS: [&str; 3] = [ + "JCODE_OPENAI_API_BASE", + "OPENAI_BASE_URL", + "OPENAI_API_BASE", + ]; for var in OVERRIDE_VARS { let Ok(raw) = std::env::var(var) else { continue; diff --git a/crates/jcode-base/src/provider/openai_provider_impl.rs b/crates/jcode-base/src/provider/openai_provider_impl.rs index 71fc338fd3..ba3db8c2ed 100644 --- a/crates/jcode-base/src/provider/openai_provider_impl.rs +++ b/crates/jcode-base/src/provider/openai_provider_impl.rs @@ -615,8 +615,10 @@ impl Provider for OpenAIProvider { } fn set_temperature(&self, temperature: f32) -> Result<()> { - self.temperature - .store(temperature.clamp(0.0, 1.0).to_bits(), std::sync::atomic::Ordering::Release); + self.temperature.store( + temperature.clamp(0.0, 1.0).to_bits(), + std::sync::atomic::Ordering::Release, + ); Ok(()) } diff --git a/crates/jcode-base/src/provider/openrouter.rs b/crates/jcode-base/src/provider/openrouter.rs index 0679b69ce0..6a21d474fe 100644 --- a/crates/jcode-base/src/provider/openrouter.rs +++ b/crates/jcode-base/src/provider/openrouter.rs @@ -1074,9 +1074,7 @@ impl OpenRouterProvider { } } - if let Some(raw) = - load_env_value_from_env_or_config("JCODE_OPENAI_EXTRA_BODY", env_file) - { + if let Some(raw) = load_env_value_from_env_or_config("JCODE_OPENAI_EXTRA_BODY", env_file) { match serde_json::from_str::(&raw) { Ok(Value::Object(object)) => { for (key, val) in object { diff --git a/crates/jcode-beads-bridge/src/lib.rs b/crates/jcode-beads-bridge/src/lib.rs index 94ba73699d..be068dc727 100644 --- a/crates/jcode-beads-bridge/src/lib.rs +++ b/crates/jcode-beads-bridge/src/lib.rs @@ -18,9 +18,9 @@ pub use project::BeadsProject; pub use tasks::BeadsTaskManager; // Re-export common beads_rust types so callers only need one import. -pub use beads_rust::model::{Issue, Status, Priority, IssueType}; -pub use beads_rust::storage::{self, ListFilters}; pub use beads_rust::error::BeadsError; +pub use beads_rust::model::{Issue, IssueType, Priority, Status}; +pub use beads_rust::storage::{self, ListFilters}; #[cfg(test)] mod tests; diff --git a/crates/jcode-beads-bridge/src/project.rs b/crates/jcode-beads-bridge/src/project.rs index 83650b5dce..9c9a712683 100644 --- a/crates/jcode-beads-bridge/src/project.rs +++ b/crates/jcode-beads-bridge/src/project.rs @@ -1,15 +1,15 @@ //! `BeadsProject` — facade over beads_rust project lifecycle. -use beads_rust::config::{self, ConfigLayer, CliOverrides, ConfigPaths, discover_beads_dir}; +use beads_rust::config::{self, CliOverrides, ConfigLayer, ConfigPaths, discover_beads_dir}; use beads_rust::storage::sqlite::SqliteStorage; -use beads_rust::sync::{self, blocking_write_lock}; use beads_rust::sync::history::HistoryConfig; +use beads_rust::sync::{self, blocking_write_lock}; +use anyhow::{Context, Result}; use std::fs; -use std::path::{Path, PathBuf}; use std::fs::File; +use std::path::{Path, PathBuf}; use std::sync::Mutex; -use anyhow::{Context, Result}; use tracing::info; /// A discovered and opened beads_rust project. @@ -31,7 +31,13 @@ impl BeadsProject { .map_err(|e| anyhow::anyhow!("config load failed: {e}"))?; let lock = blocking_write_lock(&beads_dir).ok(); info!(?beads_dir, "Opened beads project"); - Ok(BeadsProject { beads_dir, jsonl_path, config, _write_lock: lock, storage: Mutex::new(storage) }) + Ok(BeadsProject { + beads_dir, + jsonl_path, + config, + _write_lock: lock, + storage: Mutex::new(storage), + }) } pub fn open_or_init(working_dir: &Path, prefix: &str) -> Result { @@ -58,13 +64,25 @@ impl BeadsProject { .map_err(|e| anyhow::anyhow!("config load failed: {e}"))?; let lock = blocking_write_lock(&beads_dir).ok(); info!(?beads_dir, prefix, "Initialised new beads project"); - Ok(BeadsProject { beads_dir, jsonl_path, config, _write_lock: lock, storage: Mutex::new(storage) }) + Ok(BeadsProject { + beads_dir, + jsonl_path, + config, + _write_lock: lock, + storage: Mutex::new(storage), + }) } pub fn flush(&self) -> Result<()> { let mut storage = self.storage.lock().unwrap(); - sync::auto_flush(&mut *storage, &self.beads_dir, &self.jsonl_path, false, HistoryConfig::default()) - .map_err(|e| anyhow::anyhow!("JSONL flush failed: {e}"))?; + sync::auto_flush( + &mut *storage, + &self.beads_dir, + &self.jsonl_path, + false, + HistoryConfig::default(), + ) + .map_err(|e| anyhow::anyhow!("JSONL flush failed: {e}"))?; Ok(()) } @@ -76,5 +94,7 @@ impl BeadsProject { self.storage.lock().unwrap() } - pub fn beads_dir(&self) -> &Path { &self.beads_dir } + pub fn beads_dir(&self) -> &Path { + &self.beads_dir + } } diff --git a/crates/jcode-beads-bridge/src/tasks.rs b/crates/jcode-beads-bridge/src/tasks.rs index 59fac4ef36..19cd7b789c 100644 --- a/crates/jcode-beads-bridge/src/tasks.rs +++ b/crates/jcode-beads-bridge/src/tasks.rs @@ -1,10 +1,10 @@ //! `BeadsTaskManager` — higher-level task operations for jcode tools. -use beads_rust::model::{Issue, Status, Priority, IssueType}; -use beads_rust::storage::{ListFilters, ReadyFilters, ReadySortPolicy}; +use beads_rust::model::{Issue, IssueType, Priority, Status}; use beads_rust::storage::sqlite::IssueUpdate; +use beads_rust::storage::{ListFilters, ReadyFilters, ReadySortPolicy}; -use crate::mapping::{TodoItem, ToJcodeTodoItem, ToBeadsIssue}; +use crate::mapping::{ToBeadsIssue, ToJcodeTodoItem, TodoItem}; use crate::project::BeadsProject; use anyhow::{Context, Result}; @@ -31,12 +31,7 @@ impl<'a> BeadsTaskManager<'a> { } /// Create a task with explicit fields. - pub fn create_task( - &self, - title: &str, - priority: Priority, - labels: &[String], - ) -> Result { + pub fn create_task(&self, title: &str, priority: Priority, labels: &[String]) -> Result { let id = format!("bead-{}", short_id()); let issue = Issue { id, @@ -58,7 +53,9 @@ impl<'a> BeadsTaskManager<'a> { statuses: Some(vec![Status::Open, Status::InProgress, Status::Blocked]), ..ListFilters::default() }; - self.project.storage().list_issues(&filters) + self.project + .storage() + .list_issues(&filters) .context("Failed to list open tasks") } @@ -70,7 +67,9 @@ impl<'a> BeadsTaskManager<'a> { /// Get a single task by ID. pub fn get_task(&self, id: &str) -> Result> { - self.project.storage().get_issue(id) + self.project + .storage() + .get_issue(id) .context("Failed to get task") } @@ -80,7 +79,10 @@ impl<'a> BeadsTaskManager<'a> { status: Some(status), ..IssueUpdate::default() }; - let updated = self.project.storage_mut().update_issue(id, &update, actor) + let updated = self + .project + .storage_mut() + .update_issue(id, &update, actor) .context("Failed to update task status")?; self.project.flush()?; Ok(updated) @@ -93,7 +95,10 @@ impl<'a> BeadsTaskManager<'a> { close_reason: Some(Some(reason.to_string())), ..IssueUpdate::default() }; - let updated = self.project.storage_mut().update_issue(id, &update, actor) + let updated = self + .project + .storage_mut() + .update_issue(id, &update, actor) .context("Failed to close task")?; self.project.flush()?; Ok(updated) @@ -107,14 +112,17 @@ impl<'a> BeadsTaskManager<'a> { limit: Some(limit), ..ReadyFilters::default() }; - self.project.storage() + self.project + .storage() .get_ready_issues(&filters, ReadySortPolicy::Hybrid) .context("Failed to get ready tasks") } /// Get blocked tasks with blocker IDs. pub fn blocked_tasks(&self) -> Result)>> { - self.project.storage().get_blocked_issues() + self.project + .storage() + .get_blocked_issues() .context("Failed to get blocked tasks") } @@ -125,21 +133,27 @@ impl<'a> BeadsTaskManager<'a> { if self.project.storage().would_create_cycle(from, to, true)? { anyhow::bail!("Adding dependency {from} -> {to} would create a cycle"); } - self.project.storage_mut().add_dependency(from, to, "blocks", actor)?; + self.project + .storage_mut() + .add_dependency(from, to, "blocks", actor)?; self.project.flush()?; Ok(()) } /// Remove a dependency. pub fn remove_dependency(&self, from: &str, to: &str, actor: &str) -> Result<()> { - self.project.storage_mut().remove_dependency(from, to, actor)?; + self.project + .storage_mut() + .remove_dependency(from, to, actor)?; self.project.flush()?; Ok(()) } /// Get blockers for a task. pub fn blockers(&self, id: &str) -> Result> { - self.project.storage().get_blockers(id) + self.project + .storage() + .get_blockers(id) .context("Failed to get blockers") } } diff --git a/crates/jcode-beads-bridge/src/tests.rs b/crates/jcode-beads-bridge/src/tests.rs index 9b257eed7d..d915182084 100644 --- a/crates/jcode-beads-bridge/src/tests.rs +++ b/crates/jcode-beads-bridge/src/tests.rs @@ -1,10 +1,10 @@ //! Integration tests for the beads_rust bridge. //! //! Uses in-memory SQLite to test the facade without real filesystem I/O. -use crate::mapping::{TodoItem, Goal, ToBeadsIssue, ToBeadsEpic, ToJcodeGoal}; +use crate::mapping::{Goal, ToBeadsEpic, ToBeadsIssue, ToJcodeGoal, TodoItem}; use crate::BeadsProject; -use beads_rust::model::{Status, Priority, IssueType}; +use beads_rust::model::{IssueType, Priority, Status}; use chrono::Utc; fn temp_project(prefix: &str) -> BeadsProject { @@ -17,7 +17,10 @@ fn temp_project(prefix: &str) -> BeadsProject { fn rand_id() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() as u64 + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as u64 } #[test] @@ -33,7 +36,11 @@ fn test_beads_project_init_open_flush() { #[test] fn test_beads_open_or_init() { let base = std::env::current_dir().unwrap_or_else(|_| std::env::temp_dir()); - let dir = base.join(format!(".beads-test-open-{}-{}", std::process::id(), rand_id())); + let dir = base.join(format!( + ".beads-test-open-{}-{}", + std::process::id(), + rand_id() + )); let _ = std::fs::remove_dir_all(&dir); // Should init since no project exists let p1 = BeadsProject::open_or_init(&dir, "test").expect("open_or_init should succeed"); @@ -49,7 +56,8 @@ fn test_create_and_list_task() { let project = temp_project("test"); let manager = crate::BeadsTaskManager::new(&project); - let task = manager.create_task("Test task", Priority::HIGH, &["bug".to_string()]) + let task = manager + .create_task("Test task", Priority::HIGH, &["bug".to_string()]) .expect("create should succeed"); assert_eq!(task.title, "Test task"); assert_eq!(task.status, Status::Open); @@ -78,7 +86,9 @@ fn test_create_todo_from_item() { assigned_to: Some("agent".to_string()), }; - let issue = manager.create_todo(&item).expect("create_todo should succeed"); + let issue = manager + .create_todo(&item) + .expect("create_todo should succeed"); assert_eq!(issue.title, "A todo item"); assert_eq!(issue.status, Status::Open); assert_eq!(issue.priority, Priority::HIGH); @@ -93,14 +103,17 @@ fn test_set_status_and_close() { let project = temp_project("test"); let manager = crate::BeadsTaskManager::new(&project); - let task = manager.create_task("Status test", Priority::MEDIUM, &[]) + let task = manager + .create_task("Status test", Priority::MEDIUM, &[]) .expect("create should succeed"); - let claimed = manager.set_status(&task.id, Status::InProgress, "tester") + let claimed = manager + .set_status(&task.id, Status::InProgress, "tester") .expect("set_status should succeed"); assert_eq!(claimed.status, Status::InProgress); - let closed = manager.close_task(&task.id, "Done", "tester") + let closed = manager + .close_task(&task.id, "Done", "tester") .expect("close should succeed"); assert_eq!(closed.status, Status::Closed); @@ -112,7 +125,8 @@ fn test_ready_tasks() { let project = temp_project("test"); let manager = crate::BeadsTaskManager::new(&project); - let _t1 = manager.create_task("Ready task", Priority::HIGH, &[]) + let _t1 = manager + .create_task("Ready task", Priority::HIGH, &[]) .expect("create should succeed"); // Ready tasks need no blockers → empty graph = all ready let ready = manager.ready_tasks(10).expect("ready_tasks should succeed"); @@ -178,13 +192,16 @@ fn test_dependency_cycle_detection() { let project = temp_project("test"); let manager = crate::BeadsTaskManager::new(&project); - let a = manager.create_task("Task A", Priority::MEDIUM, &[]) + let a = manager + .create_task("Task A", Priority::MEDIUM, &[]) .expect("create A"); - let b = manager.create_task("Task B", Priority::MEDIUM, &[]) + let b = manager + .create_task("Task B", Priority::MEDIUM, &[]) .expect("create B"); // A blocks on B - manager.add_dependency(&a.id, &b.id, "tester") + manager + .add_dependency(&a.id, &b.id, "tester") .expect("add dep A->B should succeed"); // B blocking on A would create a cycle diff --git a/crates/jcode-config-types/src/lib.rs b/crates/jcode-config-types/src/lib.rs index 08f1e7fb5e..71ef722354 100644 --- a/crates/jcode-config-types/src/lib.rs +++ b/crates/jcode-config-types/src/lib.rs @@ -364,11 +364,7 @@ pub struct NamedProviderConfig { /// some OpenAI-compatible backends require (e.g. NVIDIA NIM DeepSeek-V4 /// needs `chat_template_kwargs = { thinking = true, reasoning_effort = "high" }`). /// Must be a JSON object; keys here override jcode-generated body fields. - #[serde( - default, - alias = "extra-body", - skip_serializing_if = "Option::is_none" - )] + #[serde(default, alias = "extra-body", skip_serializing_if = "Option::is_none")] pub extra_body: Option, } @@ -1278,4 +1274,4 @@ impl Default for AutoDreamConfig { dream_dir: PathBuf::from(".jcode/dreams"), } } -} \ No newline at end of file +} diff --git a/crates/jcode-memory-types/src/lib.rs b/crates/jcode-memory-types/src/lib.rs index ed6ba3933a..ffab0624bf 100644 --- a/crates/jcode-memory-types/src/lib.rs +++ b/crates/jcode-memory-types/src/lib.rs @@ -1,10 +1,10 @@ pub mod graph; pub mod provider; -pub use provider::GraphOperations; -pub use provider::MemoryProvider; pub use graph::{ ClusterEntry, Edge, EdgeKind, GRAPH_VERSION, GraphMetadata, MemoryGraph, TagEntry, }; +pub use provider::GraphOperations; +pub use provider::MemoryProvider; use std::time::Instant; diff --git a/crates/jcode-tui-mermaid/src/lib.rs b/crates/jcode-tui-mermaid/src/lib.rs index c07bd725a8..d424fc9cce 100644 --- a/crates/jcode-tui-mermaid/src/lib.rs +++ b/crates/jcode-tui-mermaid/src/lib.rs @@ -239,14 +239,12 @@ pub use content_render::{ image_widget_placeholder_markdown, parse_image_placeholder, result_to_content, result_to_lines, write_video_export_marker, }; +pub use inline_image::{inline_image_dims, inline_image_id, materialize_inline_image}; pub use runtime::{ error_lines_for, get_cached_png, get_font_size, image_protocol_available, init_picker, is_video_export_mode, protocol_type, register_external_image, register_inline_image, set_video_export_mode, }; -pub use inline_image::{ - inline_image_dims, inline_image_id, materialize_inline_image, -}; pub use viewport_render::{ invalidate_render_state, render_image_widget_viewport, render_image_widget_viewport_precise, }; diff --git a/crates/jcode-tui-mermaid/src/mermaid_inline.rs b/crates/jcode-tui-mermaid/src/mermaid_inline.rs index bfd323ee8d..9bb1eebc90 100644 --- a/crates/jcode-tui-mermaid/src/mermaid_inline.rs +++ b/crates/jcode-tui-mermaid/src/mermaid_inline.rs @@ -120,13 +120,7 @@ pub fn materialize_inline_image(media_type: &str, data_b64: &str) -> Option<(u64 .ok()?; let image = image::load_from_memory(&bytes).ok()?; let (width, height) = image.dimensions(); - dims_cache_put( - id, - InlineDims { - width, - height, - }, - ); + dims_cache_put(id, InlineDims { width, height }); let ext = inline_image_extension(media_type); if let Ok(mut cache) = RENDER_CACHE.lock() { @@ -204,10 +198,7 @@ pub(crate) fn dimensions_from_header(data: &[u8]) -> Option<(u32, u32)> { } let marker = data[i + 1]; // SOF0 (baseline) / SOF1 / SOF2 (progressive) etc. - if (0xC0..=0xCF).contains(&marker) - && marker != 0xC4 - && marker != 0xC8 - && marker != 0xCC + if (0xC0..=0xCF).contains(&marker) && marker != 0xC4 && marker != 0xC8 && marker != 0xCC { let height = u16::from_be_bytes([data[i + 5], data[i + 6]]) as u32; let width = u16::from_be_bytes([data[i + 7], data[i + 8]]) as u32; @@ -247,8 +238,10 @@ pub(crate) fn dimensions_from_header(data: &[u8]) -> Option<(u32, u32)> { if data.len() > 30 && &data[0..4] == b"RIFF" && &data[8..12] == b"WEBP" { let fourcc = &data[12..16]; if fourcc == b"VP8X" && data.len() > 30 { - let w = 1 + (u32::from(data[24]) | (u32::from(data[25]) << 8) | (u32::from(data[26]) << 16)); - let h = 1 + (u32::from(data[27]) | (u32::from(data[28]) << 8) | (u32::from(data[29]) << 16)); + let w = 1 + + (u32::from(data[24]) | (u32::from(data[25]) << 8) | (u32::from(data[26]) << 16)); + let h = 1 + + (u32::from(data[27]) | (u32::from(data[28]) << 8) | (u32::from(data[29]) << 16)); return Some((w, h)); } if fourcc == b"VP8 " && data.len() > 30 { diff --git a/crates/jcode-tui/src/tui/app/navigation.rs b/crates/jcode-tui/src/tui/app/navigation.rs index 01cf0ce232..d3da434ee4 100644 --- a/crates/jcode-tui/src/tui/app/navigation.rs +++ b/crates/jcode-tui/src/tui/app/navigation.rs @@ -1437,11 +1437,7 @@ impl App { // If the upward scroll bottomed out against the top of the currently // loaded content, fold the unsatisfied intent into the prefetch as // overshoot so the newly loaded history scrolls into view smoothly. - let overshoot = if self.scroll_offset == 0 { - amount - } else { - 0 - }; + let overshoot = if self.scroll_offset == 0 { amount } else { 0 }; self.maybe_queue_compacted_history_load_with_overshoot(overshoot); before != (self.scroll_offset, self.auto_scroll_paused) } diff --git a/crates/jcode-tui/src/tui/app/remote/key_handling.rs b/crates/jcode-tui/src/tui/app/remote/key_handling.rs index 7cc760bd5f..4b7581064f 100644 --- a/crates/jcode-tui/src/tui/app/remote/key_handling.rs +++ b/crates/jcode-tui/src/tui/app/remote/key_handling.rs @@ -1771,7 +1771,9 @@ async fn handle_remote_key_internal( return Ok(()); } - if trimmed == "/commit" || trimmed == "/commit-push" || trimmed == "/commit-and-push" + if trimmed == "/commit" + || trimmed == "/commit-push" + || trimmed == "/commit-and-push" { let is_push = trimmed != "/commit"; let prompt = if is_push { diff --git a/crates/jcode-tui/src/tui/app/turn.rs b/crates/jcode-tui/src/tui/app/turn.rs index 3c10a7f5a9..fd9fd2d722 100644 --- a/crates/jcode-tui/src/tui/app/turn.rs +++ b/crates/jcode-tui/src/tui/app/turn.rs @@ -252,758 +252,758 @@ impl App { redraw_interval = interval(redraw_period); } tokio::select! { - // Cheap single-cell spinner refresh between full redraws. This - // keeps the thinking/connecting spinner feeling responsive - // (especially in low-resource tiers where full redraws run at - // the ~1 Hz passive-liveness rate) by patching just the status - // cell. Only active while there is no streaming text to reveal. - _ = status_spinner_interval.tick(), if super::run_shell::status_spinner_only_symbol(self).is_some() => { - if !status_spinner_renderer.draw_status_spinner_only(self, terminal)? { + // Cheap single-cell spinner refresh between full redraws. This + // keeps the thinking/connecting spinner feeling responsive + // (especially in low-resource tiers where full redraws run at + // the ~1 Hz passive-liveness rate) by patching just the status + // cell. Only active while there is no streaming text to reveal. + _ = status_spinner_interval.tick(), if super::run_shell::status_spinner_only_symbol(self).is_some() => { + if !status_spinner_renderer.draw_status_spinner_only(self, terminal)? { + status_spinner_renderer.draw_full(self, terminal)?; + super::run_shell::reset_status_spinner_interval(&mut status_spinner_interval, self); + } + } + // Redraw periodically + _ = redraw_interval.tick() => { + if let Some(chunk) = self.stream_buffer.flush_smooth_frame() { + self.append_streaming_text(&chunk); + } + // Poll for background compaction completion during streaming + self.poll_compaction_completion(); status_spinner_renderer.draw_full(self, terminal)?; super::run_shell::reset_status_spinner_interval(&mut status_spinner_interval, self); } - } - // Redraw periodically - _ = redraw_interval.tick() => { - if let Some(chunk) = self.stream_buffer.flush_smooth_frame() { - self.append_streaming_text(&chunk); - } - // Poll for background compaction completion during streaming - self.poll_compaction_completion(); - status_spinner_renderer.draw_full(self, terminal)?; - super::run_shell::reset_status_spinner_interval(&mut status_spinner_interval, self); - } - bus_event = async { - match bus_receiver.as_mut() { - Some(rx) => rx.recv().await, - None => futures::future::pending::>().await, - } - } => { - if super::local::handle_bus_event(self, bus_event) { - status_spinner_renderer.draw_full(self, terminal)?; + bus_event = async { + match bus_receiver.as_mut() { + Some(rx) => rx.recv().await, + None => futures::future::pending::>().await, + } + } => { + if super::local::handle_bus_event(self, bus_event) { + status_spinner_renderer.draw_full(self, terminal)?; + } } - } - // Handle keyboard input - event = event_stream.next() => { - match event { - Some(Ok(Event::Key(key))) => { - self.update_copy_badge_key_event(key); - if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) { - let scroll_only = super::input::is_scroll_only_key(self, key.code, key.modifiers); - let _ = self.handle_key_press_event(key); - // Check for cancel request - if self.cancel_requested { - self.cancel_requested = false; - self.interleave_message = None; - self.pending_soft_interrupts.clear(); - self.pending_soft_interrupt_requests.clear(); - // Save partial assistant response before clearing - if let Some(tool) = current_tool.take() { - tool_calls.push(tool); - } - if !text_content.is_empty() || !tool_calls.is_empty() { - let mut content_blocks = Vec::new(); - if !text_content.is_empty() { - content_blocks.push(ContentBlock::Text { - text: format!("{}\n\n[generation interrupted by user]", text_content), - cache_control: None, - }); - } - crate::message::push_reasoning_blocks( - &mut content_blocks, - &provider_name, - &reasoning_content, - Some(&reasoning_signature), - store_reasoning_content, - ); - if store_reasoning_content { - content_blocks.extend(openai_reasoning_items.iter().cloned()); - } - for tc in &tool_calls { - content_blocks.push(ContentBlock::ToolUse { - id: tc.id.clone(), - name: tc.name.clone(), - input: tc.input.clone(), thought_signature: None, }); - } - if !content_blocks.is_empty() { - let content_clone = content_blocks.clone(); - self.add_provider_message(Message { - role: Role::Assistant, - content: content_blocks, - timestamp: Some(chrono::Utc::now()), - tool_duration_ms: None, - }); - self.session.add_message(Role::Assistant, content_clone); - let _ = self.session.save(); - } - // Flush buffer and show partial response - if let Some(chunk) = self.stream_buffer.flush() { - self.append_streaming_text(&chunk); - } - if !self.streaming.streaming_text.is_empty() { - let content = self.take_streaming_text(); - let content = self.collapse_reasoning_for_commit(content); - if !content.trim().is_empty() { - self.push_display_message(DisplayMessage { - role: "assistant".to_string(), - content, - tool_calls: tool_calls.iter().map(|t| t.name.clone()).collect(), - duration_secs: self.display_turn_duration_secs(), - title: None, - tool_data: None, - }); - } - } - } - self.clear_streaming_render_state(); - self.stream_buffer.clear(); - self.streaming_tool_calls.clear(); - self.schedule_queued_dispatch_after_interrupt(); - self.push_display_message(DisplayMessage::system("Interrupted")); - return Ok(()); - } - // Check for interleave request (Shift+Enter) - if let Some(interleave_msg) = self.interleave_message.take() { - // Save partial assistant response if any - if !text_content.is_empty() || !tool_calls.is_empty() { - // Complete any pending tool + // Handle keyboard input + event = event_stream.next() => { + match event { + Some(Ok(Event::Key(key))) => { + self.update_copy_badge_key_event(key); + if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) { + let scroll_only = super::input::is_scroll_only_key(self, key.code, key.modifiers); + let _ = self.handle_key_press_event(key); + // Check for cancel request + if self.cancel_requested { + self.cancel_requested = false; + self.interleave_message = None; + self.pending_soft_interrupts.clear(); + self.pending_soft_interrupt_requests.clear(); + // Save partial assistant response before clearing if let Some(tool) = current_tool.take() { tool_calls.push(tool); } - // Build content blocks for partial response - let mut content_blocks = Vec::new(); - if !text_content.is_empty() { - content_blocks.push(ContentBlock::Text { - text: text_content.clone(), - cache_control: None, - }); - } - crate::message::push_reasoning_blocks( - &mut content_blocks, - &provider_name, - &reasoning_content, - Some(&reasoning_signature), - store_reasoning_content, - ); - if store_reasoning_content { - content_blocks.extend(openai_reasoning_items.iter().cloned()); - } - for tc in &tool_calls { - content_blocks.push(ContentBlock::ToolUse { - id: tc.id.clone(), - name: tc.name.clone(), - input: tc.input.clone(), thought_signature: None, }); - } - // Add partial assistant response to messages - if !content_blocks.is_empty() { - self.add_provider_message(Message { - role: Role::Assistant, - content: content_blocks, - timestamp: Some(chrono::Utc::now()), - tool_duration_ms: None, - }); + if !text_content.is_empty() || !tool_calls.is_empty() { + let mut content_blocks = Vec::new(); + if !text_content.is_empty() { + content_blocks.push(ContentBlock::Text { + text: format!("{}\n\n[generation interrupted by user]", text_content), + cache_control: None, + }); + } + crate::message::push_reasoning_blocks( + &mut content_blocks, + &provider_name, + &reasoning_content, + Some(&reasoning_signature), + store_reasoning_content, + ); + if store_reasoning_content { + content_blocks.extend(openai_reasoning_items.iter().cloned()); + } + for tc in &tool_calls { + content_blocks.push(ContentBlock::ToolUse { + id: tc.id.clone(), + name: tc.name.clone(), + input: tc.input.clone(), thought_signature: None, }); + } + if !content_blocks.is_empty() { + let content_clone = content_blocks.clone(); + self.add_provider_message(Message { + role: Role::Assistant, + content: content_blocks, + timestamp: Some(chrono::Utc::now()), + tool_duration_ms: None, + }); + self.session.add_message(Role::Assistant, content_clone); + let _ = self.session.save(); + } + // Flush buffer and show partial response + if let Some(chunk) = self.stream_buffer.flush() { + self.append_streaming_text(&chunk); + } + if !self.streaming.streaming_text.is_empty() { + let content = self.take_streaming_text(); + let content = self.collapse_reasoning_for_commit(content); + if !content.trim().is_empty() { + self.push_display_message(DisplayMessage { + role: "assistant".to_string(), + content, + tool_calls: tool_calls.iter().map(|t| t.name.clone()).collect(), + duration_secs: self.display_turn_duration_secs(), + title: None, + tool_data: None, + }); + } + } } - // Add display message for partial response - if !self.streaming.streaming_text.is_empty() { - let content = self.take_streaming_text(); - let content = self.collapse_reasoning_for_commit(content); - if !content.trim().is_empty() { - self.push_display_message(DisplayMessage { - role: "assistant".to_string(), - content, - tool_calls: tool_calls.iter().map(|t| t.name.clone()).collect(), - duration_secs: None, - title: None, - tool_data: None, - }); + self.clear_streaming_render_state(); + self.stream_buffer.clear(); + self.streaming_tool_calls.clear(); + self.schedule_queued_dispatch_after_interrupt(); + self.push_display_message(DisplayMessage::system("Interrupted")); + return Ok(()); + } + // Check for interleave request (Shift+Enter) + if let Some(interleave_msg) = self.interleave_message.take() { + // Save partial assistant response if any + if !text_content.is_empty() || !tool_calls.is_empty() { + // Complete any pending tool + if let Some(tool) = current_tool.take() { + tool_calls.push(tool); + } + // Build content blocks for partial response + let mut content_blocks = Vec::new(); + if !text_content.is_empty() { + content_blocks.push(ContentBlock::Text { + text: text_content.clone(), + cache_control: None, + }); + } + crate::message::push_reasoning_blocks( + &mut content_blocks, + &provider_name, + &reasoning_content, + Some(&reasoning_signature), + store_reasoning_content, + ); + if store_reasoning_content { + content_blocks.extend(openai_reasoning_items.iter().cloned()); + } + for tc in &tool_calls { + content_blocks.push(ContentBlock::ToolUse { + id: tc.id.clone(), + name: tc.name.clone(), + input: tc.input.clone(), thought_signature: None, }); + } + // Add partial assistant response to messages + if !content_blocks.is_empty() { + self.add_provider_message(Message { + role: Role::Assistant, + content: content_blocks, + timestamp: Some(chrono::Utc::now()), + tool_duration_ms: None, + }); + } + // Add display message for partial response + if !self.streaming.streaming_text.is_empty() { + let content = self.take_streaming_text(); + let content = self.collapse_reasoning_for_commit(content); + if !content.trim().is_empty() { + self.push_display_message(DisplayMessage { + role: "assistant".to_string(), + content, + tool_calls: tool_calls.iter().map(|t| t.name.clone()).collect(), + duration_secs: None, + title: None, + tool_data: None, + }); + } } } + // Add user's interleaved message + self.add_provider_message(Message::user(&interleave_msg)); + self.push_display_message(DisplayMessage { + role: "user".to_string(), + content: interleave_msg, + tool_calls: vec![], + duration_secs: None, + title: None, + tool_data: None, + }); + // Clear streaming state and continue with new turn + self.clear_streaming_render_state(); + self.streaming_tool_calls.clear(); + self.stream_buffer = StreamBuffer::new(); + reasoning_content.clear(); + interleaved = true; + // Continue to next iteration of outer loop (new API call) + break; } - // Add user's interleaved message - self.add_provider_message(Message::user(&interleave_msg)); - self.push_display_message(DisplayMessage { - role: "user".to_string(), - content: interleave_msg, - tool_calls: vec![], - duration_secs: None, - title: None, - tool_data: None, - }); - // Clear streaming state and continue with new turn - self.clear_streaming_render_state(); - self.streaming_tool_calls.clear(); - self.stream_buffer = StreamBuffer::new(); - reasoning_content.clear(); - interleaved = true; - // Continue to next iteration of outer loop (new API call) - break; - } - if !scroll_only { - status_spinner_renderer.draw_full(self, terminal)?; + if !scroll_only { + status_spinner_renderer.draw_full(self, terminal)?; + } } } - } - Some(Ok(Event::Paste(text))) => { - self.handle_paste(text); - status_spinner_renderer.draw_full(self, terminal)?; - } - Some(Ok(Event::Mouse(mouse))) => { - let scroll_only = self.handle_mouse_event(mouse); - if !scroll_only { + Some(Ok(Event::Paste(text))) => { + self.handle_paste(text); status_spinner_renderer.draw_full(self, terminal)?; } - } - Some(Ok(Event::Resize(_, _))) => { - if self.should_redraw_after_resize() { - status_spinner_renderer.draw_full(self, terminal)?; + Some(Ok(Event::Mouse(mouse))) => { + let scroll_only = self.handle_mouse_event(mouse); + if !scroll_only { + status_spinner_renderer.draw_full(self, terminal)?; + } } + Some(Ok(Event::Resize(_, _))) => { + if self.should_redraw_after_resize() { + status_spinner_renderer.draw_full(self, terminal)?; + } + } + _ => {} } - _ => {} } - } - // Handle stream events - stream_event = stream.next() => { - match stream_event { - Some(Ok(event)) => { - // Track activity for status display - self.last_stream_activity = Some(Instant::now()); + // Handle stream events + stream_event = stream.next() => { + match stream_event { + Some(Ok(event)) => { + // Track activity for status display + self.last_stream_activity = Some(Instant::now()); - if first_event { - first_event = false; - } - match event { - StreamEvent::TextDelta(text) => { - self.status = ProcessingStatus::Streaming; - text_content.push_str(&text); - self.resume_streaming_tps(); - // Real output token: close any open reasoning region first so - // the answer renders as normal (non-quoted) text. - if self.reasoning_streaming && !text.trim().is_empty() { - self.close_reasoning_region(None); - } - if let Some(chunk) = self.stream_buffer.push(&text) { - self.append_streaming_text(&chunk); - self.broadcast_debug(crate::tui::backend::DebugEvent::TextDelta { - text: chunk.clone() - }); - if eager_stream_redraw { - status_spinner_renderer.draw_full(self, terminal)?; - } - } + if first_event { + first_event = false; } - StreamEvent::ToolUseStart { id, name } => { - // Tool input JSON is still provider-generated output and is - // included in provider output-token usage. Keep the TPS timer - // running until the tool call has finished streaming; actual - // tool execution is excluded below at ToolUseEnd. - self.resume_streaming_tps(); - self.clear_active_experimental_feature_notice(); - self.broadcast_debug(crate::tui::backend::DebugEvent::ToolStart { - id: id.clone(), - name: name.clone(), - }); - // Close any open reasoning region before committing the - // assistant message so the blockquote is well-formed. - if self.reasoning_streaming { - self.close_reasoning_region(None); - } - self.commit_pending_streaming_assistant_message(); - // Update status to show tool in progress - self.status = ProcessingStatus::RunningTool(name.clone()); - if matches!(name.as_str(), "memory") { - crate::memory::set_state( - crate::tui::info_widget::MemoryState::Embedding, - ); - } - self.streaming_tool_calls.push(ToolCall { - id: id.clone(), - name: name.clone(), - input: serde_json::Value::Null, - intent: None, thought_signature: None, }); - current_tool = Some(ToolCall { - id, - name, - input: serde_json::Value::Null, - intent: None, thought_signature: None, }); - current_tool_input.clear(); - if eager_stream_redraw { - status_spinner_renderer.draw_full(self, terminal)?; - } - } - StreamEvent::ToolInputDelta(delta) => { - self.broadcast_debug(crate::tui::backend::DebugEvent::ToolInput { - delta: delta.clone() - }); - current_tool_input.push_str(&delta); - } - StreamEvent::ToolUseEnd => { - // Provider output generation for this tool call is complete, - // but final usage often arrives after MessageEnd. Keep - // collecting output-token deltas while excluding tool runtime. - self.pause_streaming_tps(true); - if let Some(mut tool) = current_tool.take() { - tool.input = crate::message::ToolCall::parse_streamed_input_to_object( - ¤t_tool_input, - ); - tool.refresh_intent_from_input(); - if let Some(key) = Self::experimental_feature_key_for_tool(&tool) { - self.note_experimental_feature_use(key); + match event { + StreamEvent::TextDelta(text) => { + self.status = ProcessingStatus::Streaming; + text_content.push_str(&text); + self.resume_streaming_tps(); + // Real output token: close any open reasoning region first so + // the answer renders as normal (non-quoted) text. + if self.reasoning_streaming && !text.trim().is_empty() { + self.close_reasoning_region(None); } - if let Some(streaming_tool) = self - .streaming_tool_calls - .iter_mut() - .find(|tc| tc.id == tool.id) - { - streaming_tool.input = tool.input.clone(); - streaming_tool.intent = tool.intent.clone(); + if let Some(chunk) = self.stream_buffer.push(&text) { + self.append_streaming_text(&chunk); + self.broadcast_debug(crate::tui::backend::DebugEvent::TextDelta { + text: chunk.clone() + }); + if eager_stream_redraw { + status_spinner_renderer.draw_full(self, terminal)?; + } } - self.broadcast_debug(crate::tui::backend::DebugEvent::ToolExec { - id: tool.id.clone(), - name: tool.name.clone(), + } + StreamEvent::ToolUseStart { id, name } => { + // Tool input JSON is still provider-generated output and is + // included in provider output-token usage. Keep the TPS timer + // running until the tool call has finished streaming; actual + // tool execution is excluded below at ToolUseEnd. + self.resume_streaming_tps(); + self.clear_active_experimental_feature_notice(); + self.broadcast_debug(crate::tui::backend::DebugEvent::ToolStart { + id: id.clone(), + name: name.clone(), }); + // Close any open reasoning region before committing the + // assistant message so the blockquote is well-formed. + if self.reasoning_streaming { + self.close_reasoning_region(None); + } self.commit_pending_streaming_assistant_message(); - - // Add tool call as its own display message - self.push_display_message(DisplayMessage { - role: "tool".to_string(), - content: tool.name.clone(), - tool_calls: vec![], - duration_secs: None, - title: None, - tool_data: Some(tool.clone()), - }); - - tool_calls.push(tool); + // Update status to show tool in progress + self.status = ProcessingStatus::RunningTool(name.clone()); + if matches!(name.as_str(), "memory") { + crate::memory::set_state( + crate::tui::info_widget::MemoryState::Embedding, + ); + } + self.streaming_tool_calls.push(ToolCall { + id: id.clone(), + name: name.clone(), + input: serde_json::Value::Null, + intent: None, thought_signature: None, }); + current_tool = Some(ToolCall { + id, + name, + input: serde_json::Value::Null, + intent: None, thought_signature: None, }); current_tool_input.clear(); if eager_stream_redraw { status_spinner_renderer.draw_full(self, terminal)?; } } - } - StreamEvent::ToolUseSignature(signature) => { - // Attach Gemini 3 thought signature to the - // most recent tool call so it can be - // persisted and replayed on later turns. - if !signature.is_empty() { - if let Some(tool) = tool_calls.last_mut() { - tool.thought_signature = Some(signature.clone()); - } - if let Some(streaming_tool) = - self.streaming_tool_calls.last_mut() - { - streaming_tool.thought_signature = Some(signature); - } - } - } - StreamEvent::TokenUsage { - input_tokens, - output_tokens, - cache_read_input_tokens, - cache_creation_input_tokens, - } => { - let mut usage_changed = false; - if let Some(input) = input_tokens { - self.streaming.streaming_input_tokens = input; - usage_changed = true; - } - if let Some(output) = output_tokens { - self.streaming.streaming_output_tokens = output; - self.accumulate_streaming_output_tokens( - output, - &mut call_output_tokens_seen, - ); - } - if cache_read_input_tokens.is_some() { - self.streaming.streaming_cache_read_tokens = cache_read_input_tokens; - usage_changed = true; - } - if cache_creation_input_tokens.is_some() { - self.streaming.streaming_cache_creation_tokens = - cache_creation_input_tokens; - usage_changed = true; + StreamEvent::ToolInputDelta(delta) => { + self.broadcast_debug(crate::tui::backend::DebugEvent::ToolInput { + delta: delta.clone() + }); + current_tool_input.push_str(&delta); } - if usage_changed { - self.update_compaction_usage_from_stream(); - if let Some(context_tokens) = self.current_stream_context_tokens() { - self.check_context_warning(context_tokens); + StreamEvent::ToolUseEnd => { + // Provider output generation for this tool call is complete, + // but final usage often arrives after MessageEnd. Keep + // collecting output-token deltas while excluding tool runtime. + self.pause_streaming_tps(true); + if let Some(mut tool) = current_tool.take() { + tool.input = crate::message::ToolCall::parse_streamed_input_to_object( + ¤t_tool_input, + ); + tool.refresh_intent_from_input(); + if let Some(key) = Self::experimental_feature_key_for_tool(&tool) { + self.note_experimental_feature_use(key); + } + if let Some(streaming_tool) = self + .streaming_tool_calls + .iter_mut() + .find(|tc| tc.id == tool.id) + { + streaming_tool.input = tool.input.clone(); + streaming_tool.intent = tool.intent.clone(); + } + self.broadcast_debug(crate::tui::backend::DebugEvent::ToolExec { + id: tool.id.clone(), + name: tool.name.clone(), + }); + self.commit_pending_streaming_assistant_message(); + + // Add tool call as its own display message + self.push_display_message(DisplayMessage { + role: "tool".to_string(), + content: tool.name.clone(), + tool_calls: vec![], + duration_secs: None, + title: None, + tool_data: Some(tool.clone()), + }); + + tool_calls.push(tool); + current_tool_input.clear(); + if eager_stream_redraw { + status_spinner_renderer.draw_full(self, terminal)?; + } } } - self.broadcast_debug(crate::tui::backend::DebugEvent::TokenUsage { - input_tokens: self.streaming.streaming_input_tokens, - output_tokens: self.streaming.streaming_output_tokens, - cache_read_input_tokens: self.streaming.streaming_cache_read_tokens, - cache_creation_input_tokens: self - .streaming.streaming_cache_creation_tokens, - }); - } - StreamEvent::ConnectionType { connection } => { - self.connection_type = Some(connection); - self.update_terminal_title(); - } - StreamEvent::ConnectionPhase { phase } => { - self.status = if matches!(phase, crate::message::ConnectionPhase::Streaming) { - ProcessingStatus::Streaming - } else { - ProcessingStatus::Connecting(phase) - }; - if eager_stream_redraw { - status_spinner_renderer.draw_full(self, terminal)?; - } - } - StreamEvent::StatusDetail { detail } => { - self.status_detail = Some(detail); - if eager_stream_redraw { - status_spinner_renderer.draw_full(self, terminal)?; + StreamEvent::ToolUseSignature(signature) => { + // Attach Gemini 3 thought signature to the + // most recent tool call so it can be + // persisted and replayed on later turns. + if !signature.is_empty() { + if let Some(tool) = tool_calls.last_mut() { + tool.thought_signature = Some(signature.clone()); + } + if let Some(streaming_tool) = + self.streaming_tool_calls.last_mut() + { + streaming_tool.thought_signature = Some(signature); + } + } } - } - StreamEvent::MessageEnd { .. } => { - self.pause_streaming_tps(true); - self.stream_message_ended = true; - saw_message_end = true; - if eager_stream_redraw { - status_spinner_renderer.draw_full(self, terminal)?; + StreamEvent::TokenUsage { + input_tokens, + output_tokens, + cache_read_input_tokens, + cache_creation_input_tokens, + } => { + let mut usage_changed = false; + if let Some(input) = input_tokens { + self.streaming.streaming_input_tokens = input; + usage_changed = true; + } + if let Some(output) = output_tokens { + self.streaming.streaming_output_tokens = output; + self.accumulate_streaming_output_tokens( + output, + &mut call_output_tokens_seen, + ); + } + if cache_read_input_tokens.is_some() { + self.streaming.streaming_cache_read_tokens = cache_read_input_tokens; + usage_changed = true; + } + if cache_creation_input_tokens.is_some() { + self.streaming.streaming_cache_creation_tokens = + cache_creation_input_tokens; + usage_changed = true; + } + if usage_changed { + self.update_compaction_usage_from_stream(); + if let Some(context_tokens) = self.current_stream_context_tokens() { + self.check_context_warning(context_tokens); + } + } + self.broadcast_debug(crate::tui::backend::DebugEvent::TokenUsage { + input_tokens: self.streaming.streaming_input_tokens, + output_tokens: self.streaming.streaming_output_tokens, + cache_read_input_tokens: self.streaming.streaming_cache_read_tokens, + cache_creation_input_tokens: self + .streaming.streaming_cache_creation_tokens, + }); } - } - StreamEvent::SessionId(sid) => { - self.provider_session_id = Some(sid); - if saw_message_end { - break; + StreamEvent::ConnectionType { connection } => { + self.connection_type = Some(connection); + self.update_terminal_title(); } - } - StreamEvent::Error { message, .. } => { - let no_partial_output = text_content.is_empty() - && tool_calls.is_empty() - && current_tool.is_none() - && self.streaming.streaming_text.is_empty() - && !saw_message_end; - if no_partial_output - && let Some(reason) = crate::network_retry::classify_message(&message) - { - let plan = crate::network_retry::wait_plan(); - self.push_display_message(DisplayMessage::system(format!( - "Stream interrupted, likely because {reason}. Waiting to retry: {}.", - plan.listener_summary - ))); - self.status = ProcessingStatus::WaitingForNetwork { - listener: plan.listener_summary.clone(), + StreamEvent::ConnectionPhase { phase } => { + self.status = if matches!(phase, crate::message::ConnectionPhase::Streaming) { + ProcessingStatus::Streaming + } else { + ProcessingStatus::Connecting(phase) }; - status_spinner_renderer.draw_full(self, terminal)?; - crate::network_retry::wait_until_probably_online().await; - self.push_display_message(DisplayMessage::system( - "Network connectivity looks restored; retrying request.".to_string(), - )); - continue 'turn_loop; + if eager_stream_redraw { + status_spinner_renderer.draw_full(self, terminal)?; + } } - return Err(anyhow::anyhow!("Stream error: {}", message)); - } - StreamEvent::ThinkingStart => { - let start = Instant::now(); - self.resume_streaming_tps(); - self.thinking_start = Some(start); - self.thinking_buffer.clear(); - self.thinking_prefix_emitted = false; - // Always show Thinking in status bar - self.status = ProcessingStatus::Thinking(start); - self.broadcast_debug(crate::tui::backend::DebugEvent::ThinkingStart); - if eager_stream_redraw { - status_spinner_renderer.draw_full(self, terminal)?; + StreamEvent::StatusDetail { detail } => { + self.status_detail = Some(detail); + if eager_stream_redraw { + status_spinner_renderer.draw_full(self, terminal)?; + } } - } - StreamEvent::ThinkingSignatureDelta(signature) => { - if store_reasoning_content { - reasoning_signature.push_str(&signature); + StreamEvent::MessageEnd { .. } => { + self.pause_streaming_tps(true); + self.stream_message_ended = true; + saw_message_end = true; + if eager_stream_redraw { + status_spinner_renderer.draw_full(self, terminal)?; + } } - } - StreamEvent::ThinkingDelta(thinking_text) => { - self.resume_streaming_tps(); - // Reflect active reasoning in the status line even when the - // provider streams reasoning deltas without an explicit - // ThinkingStart (e.g. OpenRouter, Bedrock) or when the - // reasoning text itself is hidden by config. - let thinking_start = - *self.thinking_start.get_or_insert_with(Instant::now); - let entered_thinking = - !matches!(self.status, ProcessingStatus::Thinking(_)); - if entered_thinking { - self.status = ProcessingStatus::Thinking(thinking_start); + StreamEvent::SessionId(sid) => { + self.provider_session_id = Some(sid); + if saw_message_end { + break; + } } - // Buffer thinking content for status/debug accounting. - self.thinking_buffer.push_str(&thinking_text); - // Flush any pending real output before reasoning text. - if let Some(chunk) = self.stream_buffer.flush() { - self.append_streaming_text(&chunk); + StreamEvent::Error { message, .. } => { + let no_partial_output = text_content.is_empty() + && tool_calls.is_empty() + && current_tool.is_none() + && self.streaming.streaming_text.is_empty() + && !saw_message_end; + if no_partial_output + && let Some(reason) = crate::network_retry::classify_message(&message) + { + let plan = crate::network_retry::wait_plan(); + self.push_display_message(DisplayMessage::system(format!( + "Stream interrupted, likely because {reason}. Waiting to retry: {}.", + plan.listener_summary + ))); + self.status = ProcessingStatus::WaitingForNetwork { + listener: plan.listener_summary.clone(), + }; + status_spinner_renderer.draw_full(self, terminal)?; + crate::network_retry::wait_until_probably_online().await; + self.push_display_message(DisplayMessage::system( + "Network connectivity looks restored; retrying request.".to_string(), + )); + continue 'turn_loop; + } + return Err(anyhow::anyhow!("Stream error: {}", message)); } - // Only render thinking content if enabled in config. - if config().display.reasoning_enabled() { - self.open_reasoning_region(); - self.append_reasoning_text(&thinking_text); + StreamEvent::ThinkingStart => { + let start = Instant::now(); + self.resume_streaming_tps(); + self.thinking_start = Some(start); + self.thinking_buffer.clear(); + self.thinking_prefix_emitted = false; + // Always show Thinking in status bar + self.status = ProcessingStatus::Thinking(start); + self.broadcast_debug(crate::tui::backend::DebugEvent::ThinkingStart); + if eager_stream_redraw { + status_spinner_renderer.draw_full(self, terminal)?; + } } - // Always capture reasoning text so it can be - // persisted as a history-only trace, regardless - // of provider replay support. - reasoning_content.push_str(&thinking_text); - // When reasoning text is hidden, the status flip to - // "thinking…" is the only visible signal, so repaint - // promptly on the first delta. - if entered_thinking && eager_stream_redraw { - status_spinner_renderer.draw_full(self, terminal)?; + StreamEvent::ThinkingSignatureDelta(signature) => { + if store_reasoning_content { + reasoning_signature.push_str(&signature); + } } - } - StreamEvent::ThinkingEnd => { - self.pause_streaming_tps(true); - self.thinking_start = None; - self.thinking_buffer.clear(); - self.broadcast_debug(crate::tui::backend::DebugEvent::ThinkingEnd); - } - StreamEvent::ThinkingDone { duration_secs: _ } => { - // Flush any pending buffered text first - if let Some(chunk) = self.stream_buffer.flush() { - self.append_streaming_text(&chunk); + StreamEvent::ThinkingDelta(thinking_text) => { + self.resume_streaming_tps(); + // Reflect active reasoning in the status line even when the + // provider streams reasoning deltas without an explicit + // ThinkingStart (e.g. OpenRouter, Bedrock) or when the + // reasoning text itself is hidden by config. + let thinking_start = + *self.thinking_start.get_or_insert_with(Instant::now); + let entered_thinking = + !matches!(self.status, ProcessingStatus::Thinking(_)); + if entered_thinking { + self.status = ProcessingStatus::Thinking(thinking_start); + } + // Buffer thinking content for status/debug accounting. + self.thinking_buffer.push_str(&thinking_text); + // Flush any pending real output before reasoning text. + if let Some(chunk) = self.stream_buffer.flush() { + self.append_streaming_text(&chunk); + } + // Only render thinking content if enabled in config. + if config().display.reasoning_enabled() { + self.open_reasoning_region(); + self.append_reasoning_text(&thinking_text); + } + // Always capture reasoning text so it can be + // persisted as a history-only trace, regardless + // of provider replay support. + reasoning_content.push_str(&thinking_text); + // When reasoning text is hidden, the status flip to + // "thinking…" is the only visible signal, so repaint + // promptly on the first delta. + if entered_thinking && eager_stream_redraw { + status_spinner_renderer.draw_full(self, terminal)?; + } } - if config().display.reasoning_enabled() { - self.close_reasoning_region(None); + StreamEvent::ThinkingEnd => { + self.pause_streaming_tps(true); + self.thinking_start = None; + self.thinking_buffer.clear(); + self.broadcast_debug(crate::tui::backend::DebugEvent::ThinkingEnd); } - self.thinking_prefix_emitted = false; - self.thinking_buffer.clear(); - } - StreamEvent::OpenAIReasoning { - id, - summary, - encrypted_content, - status, - } => { - if store_reasoning_content { - openai_reasoning_items.push(ContentBlock::OpenAIReasoning { - id, - summary, - encrypted_content, - status, - }); + StreamEvent::ThinkingDone { duration_secs: _ } => { + // Flush any pending buffered text first + if let Some(chunk) = self.stream_buffer.flush() { + self.append_streaming_text(&chunk); + } + if config().display.reasoning_enabled() { + self.close_reasoning_region(None); + } + self.thinking_prefix_emitted = false; + self.thinking_buffer.clear(); } - } - StreamEvent::Compaction { - trigger, - pre_tokens, - openai_encrypted_content, - } => { - if let Some(encrypted_content) = openai_encrypted_content { - openai_native_compaction - .get_or_insert_with(|| { - (encrypted_content, self.local_transcript_message_count()) + StreamEvent::OpenAIReasoning { + id, + summary, + encrypted_content, + status, + } => { + if store_reasoning_content { + openai_reasoning_items.push(ContentBlock::OpenAIReasoning { + id, + summary, + encrypted_content, + status, }); + } } - // Flush any pending buffered text first - if let Some(chunk) = self.stream_buffer.flush() { - self.append_streaming_text(&chunk); + StreamEvent::Compaction { + trigger, + pre_tokens, + openai_encrypted_content, + } => { + if let Some(encrypted_content) = openai_encrypted_content { + openai_native_compaction + .get_or_insert_with(|| { + (encrypted_content, self.local_transcript_message_count()) + }); + } + // Flush any pending buffered text first + if let Some(chunk) = self.stream_buffer.flush() { + self.append_streaming_text(&chunk); + } + let tokens_str = pre_tokens + .map(|t| format!(" (was {} tokens)", t)) + .unwrap_or_default(); + let compact_msg = format!( + "📦 **Compaction complete** - context summarized ({}){}\n\n", + trigger, tokens_str + ); + self.append_streaming_text(&compact_msg); + self.context_warning_shown = false; } - let tokens_str = pre_tokens - .map(|t| format!(" (was {} tokens)", t)) - .unwrap_or_default(); - let compact_msg = format!( - "📦 **Compaction complete** - context summarized ({}){}\n\n", - trigger, tokens_str - ); - self.append_streaming_text(&compact_msg); - self.context_warning_shown = false; - } - StreamEvent::UpstreamProvider { provider } => { - // Store the upstream provider (e.g., Fireworks, Together) - self.upstream_provider = Some(provider); - } - StreamEvent::ToolResult { tool_use_id, content, is_error } => { - // SDK already executed this tool - self.tool_result_ids.insert(tool_use_id.clone()); - // Find the tool name from our tracking - let tool_name = self.streaming_tool_calls - .iter() - .find(|tc| tc.id == tool_use_id) - .map(|tc| tc.name.clone()) - .unwrap_or_default(); + StreamEvent::UpstreamProvider { provider } => { + // Store the upstream provider (e.g., Fireworks, Together) + self.upstream_provider = Some(provider); + } + StreamEvent::ToolResult { tool_use_id, content, is_error } => { + // SDK already executed this tool + self.tool_result_ids.insert(tool_use_id.clone()); + // Find the tool name from our tracking + let tool_name = self.streaming_tool_calls + .iter() + .find(|tc| tc.id == tool_use_id) + .map(|tc| tc.name.clone()) + .unwrap_or_default(); - self.broadcast_debug(crate::tui::backend::DebugEvent::ToolDone { - id: tool_use_id.clone(), - name: tool_name.clone(), - output: content.clone(), - is_error, - }); + self.broadcast_debug(crate::tui::backend::DebugEvent::ToolDone { + id: tool_use_id.clone(), + name: tool_name.clone(), + output: content.clone(), + is_error, + }); - // Update the tool's DisplayMessage with the output (if it exists) - if let Some(dm) = self.display_messages.iter_mut().rev().find(|dm| { - dm.tool_data.as_ref().map(|td| &td.id) == Some(&tool_use_id) - }) { - dm.content = content.clone(); - self.bump_display_messages_version(); - } + // Update the tool's DisplayMessage with the output (if it exists) + if let Some(dm) = self.display_messages.iter_mut().rev().find(|dm| { + dm.tool_data.as_ref().map(|td| &td.id) == Some(&tool_use_id) + }) { + dm.content = content.clone(); + self.bump_display_messages_version(); + } - // Clear this tool from streaming_tool_calls - self.streaming_tool_calls.retain(|tc| tc.id != tool_use_id); + // Clear this tool from streaming_tool_calls + self.streaming_tool_calls.retain(|tc| tc.id != tool_use_id); - // Reset status back to Streaming - self.status = ProcessingStatus::Streaming; + // Reset status back to Streaming + self.status = ProcessingStatus::Streaming; - sdk_tool_results.insert(tool_use_id, (content, is_error)); - } - StreamEvent::GeneratedImage { - id, - path, - metadata_path, - output_format, - revised_prompt, - } => { - self.pause_streaming_tps(false); - self.commit_pending_streaming_assistant_message(); - let input = crate::message::generated_image_tool_input( - &path, - metadata_path.as_deref(), - &output_format, - revised_prompt.as_deref(), - ); - let tool_call = ToolCall { - id: id.clone(), - name: crate::message::GENERATED_IMAGE_TOOL_NAME.to_string(), - input, - intent: Some("OpenAI native image generation".to_string()), thought_signature: None, }; - let summary = crate::message::generated_image_summary( - &path, - metadata_path.as_deref(), - &output_format, - revised_prompt.as_deref(), - ); - self.push_display_message(DisplayMessage { - role: "tool".to_string(), - content: summary, - tool_calls: vec![], - duration_secs: None, - title: Some("Generated image".to_string()), - tool_data: Some(tool_call), - }); - match crate::tui::write_generated_image_side_panel_page( - &self.session.id, - &id, - &path, - metadata_path.as_deref(), - &output_format, - revised_prompt.as_deref(), - ) { - Ok(snapshot) => self.set_side_panel_snapshot(snapshot), - Err(err) => crate::logging::warn(&format!( - "Failed to write generated image side panel page: {}", - err - )), + sdk_tool_results.insert(tool_use_id, (content, is_error)); } - if provider.supports_image_input() { - if let Some(blocks) = crate::message::generated_image_visual_context_blocks( + StreamEvent::GeneratedImage { + id, + path, + metadata_path, + output_format, + revised_prompt, + } => { + self.pause_streaming_tps(false); + self.commit_pending_streaming_assistant_message(); + let input = crate::message::generated_image_tool_input( + &path, + metadata_path.as_deref(), + &output_format, + revised_prompt.as_deref(), + ); + let tool_call = ToolCall { + id: id.clone(), + name: crate::message::GENERATED_IMAGE_TOOL_NAME.to_string(), + input, + intent: Some("OpenAI native image generation".to_string()), thought_signature: None, }; + let summary = crate::message::generated_image_summary( + &path, + metadata_path.as_deref(), + &output_format, + revised_prompt.as_deref(), + ); + self.push_display_message(DisplayMessage { + role: "tool".to_string(), + content: summary, + tool_calls: vec![], + duration_secs: None, + title: Some("Generated image".to_string()), + tool_data: Some(tool_call), + }); + match crate::tui::write_generated_image_side_panel_page( + &self.session.id, + &id, &path, metadata_path.as_deref(), &output_format, revised_prompt.as_deref(), ) { - generated_image_contexts.push(blocks); - } else { - crate::logging::warn(&format!( - "Generated image was not attached as visual context: {}", - path - )); + Ok(snapshot) => self.set_side_panel_snapshot(snapshot), + Err(err) => crate::logging::warn(&format!( + "Failed to write generated image side panel page: {}", + err + )), + } + if provider.supports_image_input() { + if let Some(blocks) = crate::message::generated_image_visual_context_blocks( + &path, + metadata_path.as_deref(), + &output_format, + revised_prompt.as_deref(), + ) { + generated_image_contexts.push(blocks); + } else { + crate::logging::warn(&format!( + "Generated image was not attached as visual context: {}", + path + )); + } + } + self.status = ProcessingStatus::Streaming; + if eager_stream_redraw { + status_spinner_renderer.draw_full(self, terminal)?; } } - self.status = ProcessingStatus::Streaming; - if eager_stream_redraw { - status_spinner_renderer.draw_full(self, terminal)?; + StreamEvent::NativeToolCall { + request_id, + tool_name, + input, + } => { + // Execute native tool and send result back to SDK bridge + let ctx = crate::tool::ToolContext { + session_id: self.session_id().to_string(), + message_id: self.session_id().to_string(), + tool_call_id: request_id.clone(), + working_dir: self.session.working_dir.as_deref().map(PathBuf::from), + stdin_request_tx: None, + graceful_shutdown_signal: None, + execution_mode: crate::tool::ToolExecutionMode::AgentTurn, + best_of_n_run_id: None, + best_of_n_candidate_id: None, + }; + let tool_result = self + .registry + .execute( + &tool_name, + crate::message::ToolCall::normalize_input_to_object(input), + ctx, + ) + .await; + crate::telemetry::record_tool_call(); + if tool_result.is_err() { + crate::telemetry::record_tool_failure(); + } + let native_result = match tool_result { + Ok(output) => crate::provider::NativeToolResult::success(request_id, output.output), + Err(e) => crate::provider::NativeToolResult::error(request_id, e.to_string()), + }; + if let Some(sender) = self.provider.native_result_sender() { + let _ = sender.send(native_result).await; + } } } - StreamEvent::NativeToolCall { - request_id, - tool_name, - input, - } => { - // Execute native tool and send result back to SDK bridge - let ctx = crate::tool::ToolContext { - session_id: self.session_id().to_string(), - message_id: self.session_id().to_string(), - tool_call_id: request_id.clone(), - working_dir: self.session.working_dir.as_deref().map(PathBuf::from), - stdin_request_tx: None, - graceful_shutdown_signal: None, - execution_mode: crate::tool::ToolExecutionMode::AgentTurn, - best_of_n_run_id: None, - best_of_n_candidate_id: None, - }; - let tool_result = self - .registry - .execute( - &tool_name, - crate::message::ToolCall::normalize_input_to_object(input), - ctx, - ) - .await; - crate::telemetry::record_tool_call(); - if tool_result.is_err() { - crate::telemetry::record_tool_failure(); - } - let native_result = match tool_result { - Ok(output) => crate::provider::NativeToolResult::success(request_id, output.output), - Err(e) => crate::provider::NativeToolResult::error(request_id, e.to_string()), + } + Some(Err(e)) => { + let no_partial_output = text_content.is_empty() + && tool_calls.is_empty() + && current_tool.is_none() + && self.streaming.streaming_text.is_empty() + && !saw_message_end; + if no_partial_output + && let Some(reason) = crate::network_retry::classify_network_interruption(e.as_ref()) + { + let plan = crate::network_retry::wait_plan(); + self.push_display_message(DisplayMessage::system(format!( + "Stream interrupted, likely because {reason}. Waiting to retry: {}.", + plan.listener_summary + ))); + self.status = ProcessingStatus::WaitingForNetwork { + listener: plan.listener_summary.clone(), }; - if let Some(sender) = self.provider.native_result_sender() { - let _ = sender.send(native_result).await; - } + status_spinner_renderer.draw_full(self, terminal)?; + crate::network_retry::wait_until_probably_online().await; + self.push_display_message(DisplayMessage::system( + "Network connectivity looks restored; retrying request.".to_string(), + )); + continue 'turn_loop; } + return Err(e); } - } - Some(Err(e)) => { - let no_partial_output = text_content.is_empty() - && tool_calls.is_empty() - && current_tool.is_none() - && self.streaming.streaming_text.is_empty() - && !saw_message_end; - if no_partial_output - && let Some(reason) = crate::network_retry::classify_network_interruption(e.as_ref()) - { - let plan = crate::network_retry::wait_plan(); - self.push_display_message(DisplayMessage::system(format!( - "Stream interrupted, likely because {reason}. Waiting to retry: {}.", - plan.listener_summary - ))); - self.status = ProcessingStatus::WaitingForNetwork { - listener: plan.listener_summary.clone(), - }; - status_spinner_renderer.draw_full(self, terminal)?; - crate::network_retry::wait_until_probably_online().await; - self.push_display_message(DisplayMessage::system( - "Network connectivity looks restored; retrying request.".to_string(), - )); - continue 'turn_loop; - } - return Err(e); - } - None => { - let no_partial_output = text_content.is_empty() - && tool_calls.is_empty() - && current_tool.is_none() - && self.streaming.streaming_text.is_empty() - && !saw_message_end; - if no_partial_output { - let plan = crate::network_retry::wait_plan(); - self.push_display_message(DisplayMessage::system(format!( - "Stream ended before the model response completed; this may be a network disconnect. Waiting to retry: {}.", - plan.listener_summary - ))); - self.status = ProcessingStatus::WaitingForNetwork { - listener: plan.listener_summary.clone(), - }; - status_spinner_renderer.draw_full(self, terminal)?; - crate::network_retry::wait_until_probably_online().await; - self.push_display_message(DisplayMessage::system( - "Network connectivity looks restored; retrying request.".to_string(), - )); - continue 'turn_loop; + None => { + let no_partial_output = text_content.is_empty() + && tool_calls.is_empty() + && current_tool.is_none() + && self.streaming.streaming_text.is_empty() + && !saw_message_end; + if no_partial_output { + let plan = crate::network_retry::wait_plan(); + self.push_display_message(DisplayMessage::system(format!( + "Stream ended before the model response completed; this may be a network disconnect. Waiting to retry: {}.", + plan.listener_summary + ))); + self.status = ProcessingStatus::WaitingForNetwork { + listener: plan.listener_summary.clone(), + }; + status_spinner_renderer.draw_full(self, terminal)?; + crate::network_retry::wait_until_probably_online().await; + self.push_display_message(DisplayMessage::system( + "Network connectivity looks restored; retrying request.".to_string(), + )); + continue 'turn_loop; + } + break; } - break; } } } - } } // If we interleaved a message, skip post-processing and go straight to new API call @@ -1204,8 +1204,8 @@ impl App { stdin_request_tx: None, graceful_shutdown_signal: None, execution_mode: crate::tool::ToolExecutionMode::AgentTurn, - best_of_n_run_id: None, - best_of_n_candidate_id: None, + best_of_n_run_id: None, + best_of_n_candidate_id: None, }; Bus::global().publish(BusEvent::ToolUpdated(ToolEvent { diff --git a/crates/jcode-tui/src/tui/info_widget_stability.rs b/crates/jcode-tui/src/tui/info_widget_stability.rs index ed9588c747..ab2b9ebc4e 100644 --- a/crates/jcode-tui/src/tui/info_widget_stability.rs +++ b/crates/jcode-tui/src/tui/info_widget_stability.rs @@ -314,7 +314,10 @@ pub fn analyze_frames_with_scroll( .map(|w| w.appearances + w.disappearances) .sum(); report.total_travel = widgets.iter().map(|w| w.x_travel + w.y_travel).sum(); - report.total_content_travel = widgets.iter().map(|w| w.x_travel + w.content_y_travel).sum(); + report.total_content_travel = widgets + .iter() + .map(|w| w.x_travel + w.content_y_travel) + .sum(); report.total_recycles = widgets.iter().map(|w| w.recycles).sum(); report.total_size_churn = widgets.iter().map(|w| w.width_churn + w.height_churn).sum(); report.worst_widget = widgets diff --git a/crates/jcode-tui/src/tui/info_widget_stability_tests.rs b/crates/jcode-tui/src/tui/info_widget_stability_tests.rs index b70bc3a5f9..a0cc6b0f19 100644 --- a/crates/jcode-tui/src/tui/info_widget_stability_tests.rs +++ b/crates/jcode-tui/src/tui/info_widget_stability_tests.rs @@ -227,7 +227,8 @@ fn content_anchoring_reduces_content_relative_travel() { .map(|i| if i % period == 0 { 95 } else { 28 }) .collect(); let screen = measure_scroll_mode(&content, 100, 24, &sample_data(), SimMode::Anchored); - let stuck = measure_scroll_mode(&content, 100, 24, &sample_data(), SimMode::ContentAnchored); + let stuck = + measure_scroll_mode(&content, 100, 24, &sample_data(), SimMode::ContentAnchored); assert!( stuck.widgets.iter().any(|w| w.frames_present > 0), "period {period}: expected a widget to be placed" @@ -277,8 +278,14 @@ fn demo_content_anchor() { "{:<20} | screen-anchored: travel/100={:>6.1} content-travel/100={:>6.1} flicker/100={:>5.1} keepVis={:>3.0}% \ | content-anchored: travel/100={:>6.1} content-travel/100={:>6.1} flicker/100={:>5.1} keepVis={:>3.0}%", name, - s.travel_per_100_lines, s.content_travel_per_100_lines, s.flicker_per_100_lines, s.mean_kind_visibility * 100.0, - c.travel_per_100_lines, c.content_travel_per_100_lines, c.flicker_per_100_lines, c.mean_kind_visibility * 100.0, + s.travel_per_100_lines, + s.content_travel_per_100_lines, + s.flicker_per_100_lines, + s.mean_kind_visibility * 100.0, + c.travel_per_100_lines, + c.content_travel_per_100_lines, + c.flicker_per_100_lines, + c.mean_kind_visibility * 100.0, ); } @@ -289,15 +296,21 @@ fn demo_content_anchor() { row("flat narrow", &vec![20; 300]); row( "long line every 7", - &(0..300).map(|i| if i % 7 == 0 { 95 } else { 28 }).collect::>(), + &(0..300) + .map(|i| if i % 7 == 0 { 95 } else { 28 }) + .collect::>(), ); row( "long line every 14", - &(0..300).map(|i| if i % 14 == 0 { 95 } else { 28 }).collect::>(), + &(0..300) + .map(|i| if i % 14 == 0 { 95 } else { 28 }) + .collect::>(), ); row( "code-like (ragged)", - &(0..300).map(|i| 20 + ((i * 37) % 70) as u16).collect::>(), + &(0..300) + .map(|i| 20 + ((i * 37) % 70) as u16) + .collect::>(), ); } diff --git a/crates/jcode-tui/src/tui/mermaid.rs b/crates/jcode-tui/src/tui/mermaid.rs index 10a958f493..da406b303b 100644 --- a/crates/jcode-tui/src/tui/mermaid.rs +++ b/crates/jcode-tui/src/tui/mermaid.rs @@ -12,12 +12,12 @@ pub use jcode_tui_mermaid::{ diagram_placeholder_lines, error_lines_for, error_to_lines, estimate_image_height, evict_old_cache, get_active_diagrams, get_cached_path, get_cached_png, get_font_size, image_protocol_available, image_widget_placeholder_markdown, init_picker, inline_image_dims, - inline_image_id, materialize_inline_image, - invalidate_render_state, is_mermaid_lang, is_video_export_mode, normalize_aspect_ratio, - parse_image_placeholder, preferred_aspect_ratio_bucket, protocol_type, register_active_diagram, - register_external_image, register_inline_image, render_image_widget, render_image_widget_fit, - render_image_widget_scale, render_image_widget_viewport, render_image_widget_viewport_precise, - render_mermaid, render_mermaid_deferred, render_mermaid_deferred_with_registration, + inline_image_id, invalidate_render_state, is_mermaid_lang, is_video_export_mode, + materialize_inline_image, normalize_aspect_ratio, parse_image_placeholder, + preferred_aspect_ratio_bucket, protocol_type, register_active_diagram, register_external_image, + register_inline_image, render_image_widget, render_image_widget_fit, render_image_widget_scale, + render_image_widget_viewport, render_image_widget_viewport_precise, render_mermaid, + render_mermaid_deferred, render_mermaid_deferred_with_registration, render_mermaid_deferred_with_stream_scope, render_mermaid_sized, render_mermaid_untracked, reset_debug_stats, restore_active_diagrams, result_to_content, result_to_lines, set_log_hooks, set_memory_snapshot_hook, set_render_completed_hook, set_streaming_preview_diagram, diff --git a/crates/jcode-tui/src/tui/mod.rs b/crates/jcode-tui/src/tui/mod.rs index c25b564bcc..73bf01f0f6 100644 --- a/crates/jcode-tui/src/tui/mod.rs +++ b/crates/jcode-tui/src/tui/mod.rs @@ -146,7 +146,12 @@ pub trait TuiState { image.media_type.hash(&mut hasher); image.data.len().hash(&mut hasher); // A short prefix is enough to distinguish distinct payloads cheaply. - image.data.as_bytes().iter().take(64).for_each(|b| b.hash(&mut hasher)); + image + .data + .as_bytes() + .iter() + .take(64) + .for_each(|b| b.hash(&mut hasher)); } (images.len(), hasher.finish()) } diff --git a/crates/jcode-tui/src/tui/ui.rs b/crates/jcode-tui/src/tui/ui.rs index 7c80e8814a..7fc042d0a6 100644 --- a/crates/jcode-tui/src/tui/ui.rs +++ b/crates/jcode-tui/src/tui/ui.rs @@ -76,12 +76,12 @@ mod file_diff_ui; mod frame_metrics; #[path = "ui_header.rs"] mod header; +#[path = "ui_inline_image.rs"] +pub(crate) mod inline_image_ui; #[path = "ui_inline_interactive.rs"] mod inline_interactive_ui; #[path = "ui_inline.rs"] mod inline_ui; -#[path = "ui_inline_image.rs"] -pub(crate) mod inline_image_ui; #[path = "ui_input.rs"] pub(crate) mod input_ui; #[path = "ui_memory_estimates.rs"] diff --git a/crates/jcode-tui/src/tui/ui_inline_image.rs b/crates/jcode-tui/src/tui/ui_inline_image.rs index b865233db8..b76f4fb4a5 100644 --- a/crates/jcode-tui/src/tui/ui_inline_image.rs +++ b/crates/jcode-tui/src/tui/ui_inline_image.rs @@ -93,11 +93,7 @@ pub(crate) fn register_payload(id: u64, media_type: &str, data_b64: &str) { /// Ensure the image with `id` is materialized (decoded + cached) so it can be /// drawn. Returns true on success. Cheap and idempotent on repeat. pub(crate) fn materialize_visible(id: u64) -> bool { - if let Some((media_type, data_b64)) = PAYLOAD_REGISTRY - .lock() - .ok() - .and_then(|reg| reg.get(id)) - { + if let Some((media_type, data_b64)) = PAYLOAD_REGISTRY.lock().ok().and_then(|reg| reg.get(id)) { return mermaid::materialize_inline_image(&media_type, &data_b64).is_some(); } false @@ -109,8 +105,7 @@ pub(crate) fn materialize_visible(id: u64) -> bool { pub(crate) fn resolve_items(images: &[crate::session::RenderedImage]) -> Vec { let mut items = Vec::new(); for image in images { - let Some((id, width, height)) = - mermaid::inline_image_dims(&image.media_type, &image.data) + let Some((id, width, height)) = mermaid::inline_image_dims(&image.media_type, &image.data) else { continue; }; @@ -190,10 +185,7 @@ pub(crate) fn build_section( format!("{} {}", item.label, dims) }; lines.push(Line::from(vec![ - Span::styled( - " 🖼 ", - Style::default().add_modifier(Modifier::DIM), - ), + Span::styled(" 🖼 ", Style::default().add_modifier(Modifier::DIM)), Span::styled(label, Style::default().add_modifier(Modifier::DIM)), ])); @@ -214,7 +206,10 @@ pub(crate) fn build_section( } let line_count = lines.len(); - let plain: Vec = lines.iter().map(jcode_tui_render::line_plain_text).collect(); + let plain: Vec = lines + .iter() + .map(jcode_tui_render::line_plain_text) + .collect(); PreparedMessages { wrapped_lines: lines, @@ -300,7 +295,10 @@ mod tests { } // A dim label line precedes the first region. let label_line = jcode_tui_render::line_plain_text(§ion.wrapped_lines[1]); - assert!(label_line.contains("test.png"), "label missing: {label_line:?}"); + assert!( + label_line.contains("test.png"), + "label missing: {label_line:?}" + ); } #[test] diff --git a/crates/jcode-tui/src/tui/ui_viewport.rs b/crates/jcode-tui/src/tui/ui_viewport.rs index 02e9346051..d95be6e88c 100644 --- a/crates/jcode-tui/src/tui/ui_viewport.rs +++ b/crates/jcode-tui/src/tui/ui_viewport.rs @@ -291,7 +291,11 @@ pub(super) fn draw_messages( // is seamless (no jump to the new absolute top). let anchored_scroll = app .pending_history_anchor_lines_from_bottom() - .map(|lines_from_bottom| total_lines.saturating_sub(lines_from_bottom).min(max_scroll)); + .map(|lines_from_bottom| { + total_lines + .saturating_sub(lines_from_bottom) + .min(max_scroll) + }); let user_scroll = app.scroll_offset().min(max_scroll); let scroll = if let Some(anchored) = anchored_scroll { anchored