fix: resolve rebase conflicts, clean TUI alignment, and sync with upstream
- Split multi-line user input and system messages on newlines so each visual line gets its own ConversationLine (same pattern push_output already used). This fixes text misalignment when Enter is pressed. - Don't enable Wrap on the conversation Paragraph — the FIFO viewport counts Line items to fill the pane, so soft-wrapping would throw off the row count and misalign content. - Restore non-TUI files from upstream/main to clear conflict artifacts. - Fix clippy warnings in lsp_client, lsp_process/parse, and trident. - Add HandlerSwap and TeamToggle match arms in main.rs ReadOutcome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2971728abb
commit
28155a331f
|
|
@ -29,6 +29,12 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
|
|
@ -118,15 +124,15 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
|||
|
||||
[[package]]
|
||||
name = "aspect-core"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70188b9bf884266a6c7117e30af44f38229bc5ac56916bd16512b3e49f90fe20"
|
||||
checksum = "4e94aecae4a10eea63983b608beb48998578925d0660796140d69d8e8276f215"
|
||||
|
||||
[[package]]
|
||||
name = "aspect-macros"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "176e7db9b6a7bb4f117b8d97054d2d5a7bdc43b95c19c15c751fb8dcb9bc8a5c"
|
||||
checksum = "5d56334ff035ccdeed6b6c159b9089ae1baba29f93eb6fa8bd89312e9fa3f136"
|
||||
dependencies = [
|
||||
"aspect-core",
|
||||
"proc-macro2",
|
||||
|
|
@ -136,9 +142,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "aspect-std"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba7d130884fda30ec0acabcadc8f4711267d1cc21edc8f85283e91a011d699fa"
|
||||
checksum = "059c997171f851dcd36d5eb14cf9ec831884ecafa2b753629379ff4b71971008"
|
||||
dependencies = [
|
||||
"aspect-core",
|
||||
"log",
|
||||
|
|
@ -355,12 +361,27 @@ version = "1.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.58"
|
||||
|
|
@ -521,6 +542,20 @@ dependencies = [
|
|||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compat-harness"
|
||||
version = "0.1.3"
|
||||
|
|
@ -591,7 +626,7 @@ dependencies = [
|
|||
"clap",
|
||||
"criterion-plot",
|
||||
"is-terminal",
|
||||
"itertools",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
|
|
@ -612,7 +647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -687,8 +722,18 @@ version = "0.20.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.20.11",
|
||||
"darling_macro 0.20.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"darling_macro 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -705,13 +750,37 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.20.11",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
|
@ -740,7 +809,7 @@ version = "0.20.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.20.11",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
|
|
@ -868,6 +937,12 @@ version = "1.0.7"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
|
|
@ -981,7 +1056,7 @@ version = "0.2.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1075,6 +1150,17 @@ dependencies = [
|
|||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
|
|
@ -1375,6 +1461,28 @@ dependencies = [
|
|||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indoc"
|
||||
version = "2.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instability"
|
||||
version = "0.3.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"indoc",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.12.0"
|
||||
|
|
@ -1417,6 +1525,15 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
|
|
@ -1491,6 +1608,15 @@ version = "0.4.29"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
|
|
@ -1657,6 +1783,12 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
|
|
@ -1794,7 +1926,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"itertools 0.13.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
|
|
@ -1998,6 +2130,27 @@ dependencies = [
|
|||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"crossterm",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"lru",
|
||||
"paste",
|
||||
"strum",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
|
|
@ -2192,9 +2345,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
|
||||
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"rustls-pki-types",
|
||||
|
|
@ -2223,9 +2376,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
version = "0.103.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
|
|
@ -2249,6 +2402,7 @@ dependencies = [
|
|||
"mock-anthropic-service",
|
||||
"plugins",
|
||||
"pulldown-cmark",
|
||||
"ratatui",
|
||||
"runtime",
|
||||
"rustyline",
|
||||
"serde",
|
||||
|
|
@ -2256,6 +2410,7 @@ dependencies = [
|
|||
"syntect",
|
||||
"tokio",
|
||||
"tools",
|
||||
"tui-textarea",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2275,7 +2430,7 @@ dependencies = [
|
|||
"nix",
|
||||
"radix_trie",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"unicode-width 0.2.0",
|
||||
"utf8parse",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
|
@ -2506,12 +2661,40 @@ version = "1.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
|
@ -2956,6 +3139,17 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tui-textarea"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a5318dd619ed73c52a9417ad19046724effc1287fb75cdcc4eca1d6ac1acbae"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"ratatui",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
|
|
@ -2981,10 +3175,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
name = "unicode-truncate"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
|
|
|
|||
|
|
@ -135,10 +135,6 @@ impl ApiError {
|
|||
/// Return the `Retry-After` delay if this error came from a 429 response
|
||||
/// that included a `retry-after` header. Callers should prefer this value
|
||||
/// over the computed backoff delay when it exists.
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
#[must_use]
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
pub fn retry_after(&self) -> Option<Duration> {
|
||||
match self {
|
||||
Self::Api { retry_after, .. } => *retry_after,
|
||||
|
|
|
|||
|
|
@ -132,20 +132,20 @@ pub fn build_http_client() -> Result<reqwest::Client, ApiError> {
|
|||
#[must_use]
|
||||
pub fn build_http_client_or_default() -> reqwest::Client {
|
||||
build_http_client_with_opts(&ProxyConfig::from_env(), &TimeoutConfig::from_env())
|
||||
<<<<<<< HEAD
|
||||
.unwrap_or_else(|_| {
|
||||
reqwest::Client::builder()
|
||||
.user_agent("clawd-rust-tools/0.1")
|
||||
.build()
|
||||
.expect("default client with user_agent should always succeed")
|
||||
})
|
||||
=======
|
||||
.unwrap_or_else(|_| reqwest::Client::new())
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
}
|
||||
|
||||
/// Build a `reqwest::Client` from an explicit [`ProxyConfig`]. Used by tests
|
||||
/// and by callers that want to override process-level environment lookups.
|
||||
///
|
||||
/// When `config.proxy_url` is set it overrides the per-scheme `http_proxy`
|
||||
/// and `https_proxy` fields and is registered as both an HTTP and HTTPS
|
||||
/// proxy so a single value can route every outbound request.
|
||||
pub fn build_http_client_with(config: &ProxyConfig) -> Result<reqwest::Client, ApiError> {
|
||||
build_http_client_with_opts(config, &TimeoutConfig::from_env())
|
||||
}
|
||||
|
|
@ -158,10 +158,7 @@ pub fn build_http_client_with_opts(
|
|||
) -> Result<reqwest::Client, ApiError> {
|
||||
let mut builder = reqwest::Client::builder()
|
||||
.no_proxy()
|
||||
<<<<<<< HEAD
|
||||
.user_agent("clawd-rust-tools/0.1")
|
||||
=======
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
.connect_timeout(timeout.connect_timeout)
|
||||
.timeout(timeout.request_timeout);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,14 +12,8 @@ pub use client::{
|
|||
};
|
||||
pub use error::ApiError;
|
||||
pub use http_client::{
|
||||
<<<<<<< HEAD
|
||||
build_http_client, build_http_client_or_default, build_http_client_with,
|
||||
build_http_client_with_opts, ProxyConfig, TimeoutConfig,
|
||||
=======
|
||||
TimeoutConfig,
|
||||
build_http_client, build_http_client_or_default, build_http_client_with,
|
||||
build_http_client_with_opts, ProxyConfig,
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
};
|
||||
pub use prompt_cache::{
|
||||
CacheBreakEvent, PromptCache, PromptCacheConfig, PromptCachePaths, PromptCacheRecord,
|
||||
|
|
@ -34,12 +28,8 @@ pub use providers::openai_compat::{
|
|||
};
|
||||
pub use providers::{
|
||||
detect_provider_kind, max_tokens_for_model, max_tokens_for_model_with_override,
|
||||
<<<<<<< HEAD
|
||||
model_family_identity_for, model_family_identity_for_kind, provider_diagnostics_for_model,
|
||||
resolve_model_alias, ProviderDiagnostics, ProviderKind,
|
||||
=======
|
||||
model_token_limit, ModelTokenLimit, resolve_model_alias, ProviderKind,
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
};
|
||||
pub use sse::{parse_frame, SseParser};
|
||||
pub use types::{
|
||||
|
|
|
|||
|
|
@ -467,12 +467,8 @@ impl AnthropicClient {
|
|||
break;
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
let delay = if let Some(retry_after) = last_error.as_ref().and_then(|e| e.retry_after())
|
||||
{
|
||||
=======
|
||||
let delay = if let Some(retry_after) = last_error.as_ref().and_then(|e| e.retry_after()) {
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
retry_after
|
||||
} else {
|
||||
self.jittered_backoff_for_attempt(attempts)?
|
||||
|
|
@ -760,7 +756,11 @@ fn now_unix_timestamp() -> u64 {
|
|||
}
|
||||
|
||||
fn read_env_non_empty(key: &str) -> Result<Option<String>, ApiError> {
|
||||
super::read_env_or_config(key)
|
||||
match std::env::var(key) {
|
||||
Ok(value) if !value.is_empty() => Ok(Some(value)),
|
||||
Ok(_) | Err(std::env::VarError::NotPresent) => Ok(super::dotenv_value(key)),
|
||||
Err(error) => Err(ApiError::from(error)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -781,10 +781,7 @@ fn read_auth_token() -> Option<String> {
|
|||
|
||||
#[must_use]
|
||||
pub fn read_base_url() -> String {
|
||||
super::read_env_or_config("ANTHROPIC_BASE_URL")
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
|
||||
std::env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string())
|
||||
}
|
||||
|
||||
fn request_id_from_headers(headers: &reqwest::header::HeaderMap) -> Option<String> {
|
||||
|
|
@ -892,7 +889,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
|||
let request_id = request_id_from_headers(&headers);
|
||||
let body = response.text().await.unwrap_or_else(|_| String::new());
|
||||
let parsed_error = serde_json::from_str::<AnthropicErrorEnvelope>(&body).ok();
|
||||
let retryable = is_retryable_status(status) || is_retryable_400(status, &body);
|
||||
let retryable = is_retryable_status(status);
|
||||
let retry_after = parse_retry_after(&headers, status);
|
||||
|
||||
Err(ApiError::Api {
|
||||
|
|
@ -911,14 +908,10 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
|||
})
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
fn parse_retry_after(
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
status: reqwest::StatusCode,
|
||||
) -> Option<std::time::Duration> {
|
||||
=======
|
||||
fn parse_retry_after(headers: &reqwest::header::HeaderMap, status: reqwest::StatusCode) -> Option<std::time::Duration> {
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
if status != reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -943,15 +936,10 @@ fn is_retryable_400(status: reqwest::StatusCode, body: &str) -> bool {
|
|||
return false;
|
||||
}
|
||||
let lowered = body.to_ascii_lowercase();
|
||||
// Gateway/proxy flakes that return 400 with transient error bodies
|
||||
lowered.contains("no parseable body")
|
||||
|| lowered.contains("connection reset")
|
||||
|| lowered.contains("broken pipe")
|
||||
|| lowered.contains("empty reply from server")
|
||||
// Anthropic sometimes returns 400 invalid_request_error when their
|
||||
// backend flakes — the body contains "no parseable body" in the
|
||||
// message field of the JSON error envelope.
|
||||
|| (lowered.contains("invalid_request_error") && lowered.contains("no parseable body"))
|
||||
}
|
||||
|
||||
/// Anthropic API keys (`sk-ant-*`) are accepted over the `x-api-key` header
|
||||
|
|
|
|||
|
|
@ -391,65 +391,9 @@ pub fn detect_provider_kind(model: &str) -> ProviderKind {
|
|||
if std::env::var_os("OPENAI_BASE_URL").is_some() {
|
||||
return ProviderKind::OpenAi;
|
||||
}
|
||||
// Fallback: check stored provider config from setup wizard.
|
||||
if let Some(kind) = stored_provider_kind() {
|
||||
return kind;
|
||||
}
|
||||
ProviderKind::Anthropic
|
||||
}
|
||||
|
||||
/// Look up a stored provider config value by env var name.
|
||||
/// Returns the stored API key or base URL when the env var matches the
|
||||
/// configured provider kind, enabling the setup wizard to persist credentials
|
||||
/// that work without shell env vars.
|
||||
pub fn provider_config_value(key: &str) -> Option<String> {
|
||||
let cwd = std::env::current_dir().ok()?;
|
||||
let config = runtime::ConfigLoader::default_for(&cwd).load().ok()?;
|
||||
let provider = config.provider();
|
||||
let kind = provider.kind()?;
|
||||
match (key, kind) {
|
||||
("ANTHROPIC_API_KEY" | "ANTHROPIC_AUTH_TOKEN", "anthropic")
|
||||
| ("XAI_API_KEY", "xai")
|
||||
| ("OPENAI_API_KEY", "openai")
|
||||
| ("DASHSCOPE_API_KEY", "dashscope") => provider.api_key().map(ToOwned::to_owned),
|
||||
("ANTHROPIC_BASE_URL", "anthropic")
|
||||
| ("XAI_BASE_URL", "xai")
|
||||
| ("OPENAI_BASE_URL", "openai")
|
||||
| ("DASHSCOPE_BASE_URL", "dashscope") => provider.base_url().map(ToOwned::to_owned),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read an env var with a 3-tier fallback: process env -> .env file -> stored config.
|
||||
/// Environment variables always take priority over stored settings.
|
||||
pub fn read_env_or_config(key: &str) -> Result<Option<String>, ApiError> {
|
||||
match std::env::var(key) {
|
||||
Ok(value) if !value.is_empty() => return Ok(Some(value)),
|
||||
Ok(_) | Err(std::env::VarError::NotPresent) => {}
|
||||
Err(error) => return Err(ApiError::from(error)),
|
||||
}
|
||||
if let Some(value) = dotenv_value(key) {
|
||||
return Ok(Some(value));
|
||||
}
|
||||
if let Some(value) = provider_config_value(key) {
|
||||
return Ok(Some(value));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Return the stored `ProviderKind` from config, if set.
|
||||
fn stored_provider_kind() -> Option<ProviderKind> {
|
||||
let cwd = std::env::current_dir().ok()?;
|
||||
let config = runtime::ConfigLoader::default_for(&cwd).load().ok()?;
|
||||
let kind = config.provider().kind()?;
|
||||
match kind {
|
||||
"anthropic" => Some(ProviderKind::Anthropic),
|
||||
"xai" => Some(ProviderKind::Xai),
|
||||
"openai" => Some(ProviderKind::OpenAi),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn model_family_identity_for_kind(kind: ProviderKind) -> runtime::ModelFamilyIdentity {
|
||||
match kind {
|
||||
|
|
@ -722,7 +666,6 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
|
|||
max_output_tokens: 16_384,
|
||||
context_window_tokens: 256_000,
|
||||
}),
|
||||
<<<<<<< HEAD
|
||||
"qwen-max" => Some(ModelTokenLimit {
|
||||
max_output_tokens: 8_192,
|
||||
context_window_tokens: 131_072,
|
||||
|
|
@ -732,23 +675,6 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
|
|||
context_window_tokens: 131_072,
|
||||
}),
|
||||
_ => None,
|
||||
=======
|
||||
// Qwen models via DashScope / OpenAI-compat
|
||||
"qwen3.6-35b-fast" | "qwen3-235b-a22b" | "qwen-max" | "qwen-plus" | "qwen-turbo" | "qwen-qwq" => Some(ModelTokenLimit {
|
||||
max_output_tokens: 16_384,
|
||||
context_window_tokens: 131_072,
|
||||
}),
|
||||
"glm-5.1-fast" => Some(ModelTokenLimit {
|
||||
max_output_tokens: 16_384,
|
||||
context_window_tokens: 200_000,
|
||||
}),
|
||||
// Generic fallback for any model: assume 128K context, 8K output
|
||||
// This prevents the "unknown model → no limit check → context overflow" bug
|
||||
_ => Some(ModelTokenLimit {
|
||||
max_output_tokens: 8_192,
|
||||
context_window_tokens: 131_072,
|
||||
}),
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1697,7 +1697,11 @@ fn parse_sse_frame(
|
|||
}
|
||||
|
||||
fn read_env_non_empty(key: &str) -> Result<Option<String>, ApiError> {
|
||||
super::read_env_or_config(key)
|
||||
match std::env::var(key) {
|
||||
Ok(value) if !value.is_empty() => Ok(Some(value)),
|
||||
Ok(_) | Err(std::env::VarError::NotPresent) => Ok(super::dotenv_value(key)),
|
||||
Err(error) => Err(ApiError::from(error)),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
|
@ -1710,10 +1714,7 @@ pub fn has_api_key(key: &str) -> bool {
|
|||
|
||||
#[must_use]
|
||||
pub fn read_base_url(config: OpenAiCompatConfig) -> String {
|
||||
super::read_env_or_config(config.base_url_env)
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_else(|| config.default_base_url.to_string())
|
||||
std::env::var(config.base_url_env).unwrap_or_else(|_| config.default_base_url.to_string())
|
||||
}
|
||||
|
||||
fn chat_completions_endpoint(base_url: &str) -> String {
|
||||
|
|
@ -1743,7 +1744,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
|||
let request_id = request_id_from_headers(&headers);
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
let parsed_error = serde_json::from_str::<ErrorEnvelope>(&body).ok();
|
||||
let retryable = is_retryable_status(status) || is_retryable_400(status, &body);
|
||||
let retryable = is_retryable_status(status);
|
||||
let retry_after = parse_retry_after(&headers, status);
|
||||
|
||||
let suggested_action = suggested_action_for_status(status);
|
||||
|
|
@ -1764,14 +1765,10 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
|
|||
})
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
fn parse_retry_after(
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
status: reqwest::StatusCode,
|
||||
) -> Option<std::time::Duration> {
|
||||
=======
|
||||
fn parse_retry_after(headers: &reqwest::header::HeaderMap, status: reqwest::StatusCode) -> Option<std::time::Duration> {
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
if status != reqwest::StatusCode::TOO_MANY_REQUESTS {
|
||||
return None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1042,13 +1042,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
|
|||
argument_hint: None,
|
||||
resume_supported: true,
|
||||
},
|
||||
SlashCommandSpec {
|
||||
name: "lsp",
|
||||
aliases: &[],
|
||||
summary: "Show or manage LSP server status",
|
||||
argument_hint: Some("[start|stop|restart <language>]"),
|
||||
resume_supported: true,
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -1195,13 +1188,6 @@ pub enum SlashCommand {
|
|||
History {
|
||||
count: Option<String>,
|
||||
},
|
||||
Lsp {
|
||||
action: Option<String>,
|
||||
target: Option<String>,
|
||||
},
|
||||
Team {
|
||||
action: Option<String>,
|
||||
},
|
||||
Unknown(String),
|
||||
Team {
|
||||
action: Option<String>,
|
||||
|
|
@ -1292,11 +1278,6 @@ impl SlashCommand {
|
|||
Self::Theme { .. } => "/theme",
|
||||
Self::Voice { .. } => "/voice",
|
||||
Self::Usage { .. } => "/usage",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
Self::Team { .. } => "/team",
|
||||
Self::Setup => "/setup",
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
Self::Rename { .. } => "/rename",
|
||||
Self::Copy { .. } => "/copy",
|
||||
Self::Hooks { .. } => "/hooks",
|
||||
|
|
@ -1309,15 +1290,7 @@ impl SlashCommand {
|
|||
Self::Tag { .. } => "/tag",
|
||||
Self::OutputStyle { .. } => "/output-style",
|
||||
Self::AddDir { .. } => "/add-dir",
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
Self::Team { .. } => "/team",
|
||||
=======
|
||||
Self::Lsp { .. } => "/lsp",
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
Self::Lsp { .. } => "/lsp",
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
Self::Sandbox => "/sandbox",
|
||||
Self::Mcp { .. } => "/mcp",
|
||||
Self::Export { .. } => "/export",
|
||||
|
|
@ -1521,11 +1494,6 @@ pub fn validate_slash_command_input(
|
|||
"theme" => SlashCommand::Theme { name: remainder },
|
||||
"voice" => SlashCommand::Voice { mode: remainder },
|
||||
"usage" => SlashCommand::Usage { scope: remainder },
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"team" => SlashCommand::Team { action: remainder },
|
||||
"setup" => SlashCommand::Setup,
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
"rename" => SlashCommand::Rename { name: remainder },
|
||||
"copy" => SlashCommand::Copy { target: remainder },
|
||||
"hooks" => SlashCommand::Hooks { args: remainder },
|
||||
|
|
@ -1538,10 +1506,6 @@ pub fn validate_slash_command_input(
|
|||
"tag" => SlashCommand::Tag { label: remainder },
|
||||
"output-style" => SlashCommand::OutputStyle { style: remainder },
|
||||
"add-dir" => SlashCommand::AddDir { path: remainder },
|
||||
"lsp" => SlashCommand::Lsp {
|
||||
action: args.first().map(|s| (*s).to_string()),
|
||||
target: args.get(1).map(|s| (*s).to_string()),
|
||||
},
|
||||
"history" => SlashCommand::History {
|
||||
count: optional_single_arg(command, &args, "[count]")?,
|
||||
},
|
||||
|
|
@ -5429,27 +5393,9 @@ pub fn handle_slash_command(
|
|||
| SlashCommand::OutputStyle { .. }
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::History { .. }
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
| SlashCommand::Team { .. }
|
||||
=======
|
||||
| SlashCommand::Lsp { .. }
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
| SlashCommand::Setup
|
||||
=======
|
||||
| SlashCommand::Lsp { .. }
|
||||
<<<<<<< HEAD
|
||||
| SlashCommand::Setup
|
||||
| SlashCommand::Unknown(_) => None,
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> 0b227b62 (fix: resolve cherry-pick conflicts and remove non-LSP artifacts)
|
||||
| SlashCommand::Unknown(_) => None,
|
||||
=======
|
||||
| SlashCommand::Team { .. }
|
||||
| SlashCommand::Setup | SlashCommand::Unknown(_) => None,
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6065,20 +6011,8 @@ mod tests {
|
|||
assert!(help.contains("aliases: /skill"));
|
||||
assert!(!help.contains("/login"));
|
||||
assert!(!help.contains("/logout"));
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
assert!(help.contains("/setup"));
|
||||
assert_eq!(slash_command_specs().len(), 140);
|
||||
=======
|
||||
assert_eq!(slash_command_specs().len(), 141);
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
assert_eq!(slash_command_specs().len(), 141);
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
assert_eq!(slash_command_specs().len(), 140);
|
||||
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
|
||||
assert!(resume_supported_slash_commands().len() >= 39);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,6 @@ pub fn compact_session(session: &Session, config: CompactionConfig) -> Compactio
|
|||
.first()
|
||||
.and_then(extract_existing_compacted_summary);
|
||||
let compacted_prefix_len = usize::from(existing_summary.is_some());
|
||||
<<<<<<< HEAD
|
||||
// When preserve_recent_messages is 0, the caller wants maximum compaction
|
||||
// (no recent messages preserved). Without this guard, saturating_sub(0)
|
||||
// returns messages.len(), which later indexes past the end of the array
|
||||
|
|
@ -120,12 +119,6 @@ pub fn compact_session(session: &Session, config: CompactionConfig) -> Compactio
|
|||
.messages
|
||||
.len()
|
||||
.saturating_sub(config.preserve_recent_messages)
|
||||
=======
|
||||
let raw_keep_from = if config.preserve_recent_messages == 0 {
|
||||
session.messages.len()
|
||||
} else {
|
||||
session.messages.len().saturating_sub(config.preserve_recent_messages)
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
};
|
||||
// Ensure we do not split a tool-use / tool-result pair at the compaction
|
||||
// boundary. If the first preserved message is a user message whose first
|
||||
|
|
|
|||
|
|
@ -125,22 +125,6 @@ pub struct RuntimePluginConfig {
|
|||
max_output_tokens: Option<u32>,
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
/// Per-language LSP server configuration supplied by the user in settings.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LspServerConfig {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
/// API timeout and retry configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ApiTimeoutConfig {
|
||||
|
|
@ -160,66 +144,27 @@ impl Default for ApiTimeoutConfig {
|
|||
max_retries: 8,
|
||||
}
|
||||
}
|
||||
=======
|
||||
/// Per-language LSP server configuration supplied by the user in settings.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LspServerConfig {
|
||||
pub command: String,
|
||||
pub args: Vec<String>,
|
||||
pub enabled: bool,
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
/// API timeout and retry configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ApiTimeoutConfig {
|
||||
/// Connect timeout in seconds. Defaults to 30.
|
||||
pub connect_timeout_secs: u64,
|
||||
/// Request timeout in seconds. Defaults to 300 (5 minutes).
|
||||
pub request_timeout_secs: u64,
|
||||
/// Maximum retry attempts on transient failures. Defaults to 8.
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
impl Default for ApiTimeoutConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
connect_timeout_secs: 30,
|
||||
request_timeout_secs: 300,
|
||||
max_retries: 8,
|
||||
}
|
||||
}
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
}
|
||||
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
/// Structured feature configuration consumed by runtime subsystems.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct RuntimeFeatureConfig {
|
||||
hooks: RuntimeHookConfig,
|
||||
plugins: RuntimePluginConfig,
|
||||
mcp: McpConfigCollection,
|
||||
oauth: Option<OAuthConfig>,
|
||||
model: Option<String>,
|
||||
lsp_auto_start: bool,
|
||||
aliases: BTreeMap<String, String>,
|
||||
permission_mode: Option<ResolvedPermissionMode>,
|
||||
permission_rules: RuntimePermissionRuleConfig,
|
||||
sandbox: SandboxConfig,
|
||||
provider_fallbacks: ProviderFallbackConfig,
|
||||
trusted_roots: Vec<String>,
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
api_timeout: ApiTimeoutConfig,
|
||||
rules_import: RulesImportConfig,
|
||||
provider: RuntimeProviderConfig,
|
||||
lsp: BTreeMap<String, LspServerConfig>,
|
||||
api_timeout: ApiTimeoutConfig,
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/// Controls which external AI coding framework rules are imported into the system prompt.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum RulesImportConfig {
|
||||
|
|
@ -241,26 +186,6 @@ impl RulesImportConfig {
|
|||
Self::List(frameworks) => frameworks
|
||||
.iter()
|
||||
.any(|candidate| candidate.eq_ignore_ascii_case(framework)),
|
||||
=======
|
||||
impl Default for RuntimeFeatureConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hooks: RuntimeHookConfig::default(),
|
||||
plugins: RuntimePluginConfig::default(),
|
||||
mcp: McpConfigCollection::default(),
|
||||
oauth: None,
|
||||
model: None,
|
||||
lsp_auto_start: true,
|
||||
aliases: BTreeMap::new(),
|
||||
permission_mode: None,
|
||||
permission_rules: RuntimePermissionRuleConfig::default(),
|
||||
sandbox: SandboxConfig::default(),
|
||||
provider_fallbacks: ProviderFallbackConfig::default(),
|
||||
trusted_roots: Vec::new(),
|
||||
provider: RuntimeProviderConfig::default(),
|
||||
lsp: BTreeMap::new(),
|
||||
api_timeout: ApiTimeoutConfig::default(),
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -270,34 +195,6 @@ impl Default for RuntimeFeatureConfig {
|
|||
/// Represents the `provider` section in `~/.claw/settings.json`, used as a
|
||||
/// fallback when environment variables are absent (3-tier resolution:
|
||||
/// env var > .env file > stored config).
|
||||
=======
|
||||
provider: RuntimeProviderConfig,
|
||||
lsp: BTreeMap<String, LspServerConfig>,
|
||||
}
|
||||
|
||||
impl Default for RuntimeFeatureConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
hooks: RuntimeHookConfig::default(),
|
||||
plugins: RuntimePluginConfig::default(),
|
||||
mcp: McpConfigCollection::default(),
|
||||
oauth: None,
|
||||
model: None,
|
||||
lsp_auto_start: true,
|
||||
aliases: BTreeMap::new(),
|
||||
permission_mode: None,
|
||||
permission_rules: RuntimePermissionRuleConfig::default(),
|
||||
sandbox: SandboxConfig::default(),
|
||||
provider_fallbacks: ProviderFallbackConfig::default(),
|
||||
trusted_roots: Vec::new(),
|
||||
provider: RuntimeProviderConfig::default(),
|
||||
lsp: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stored provider configuration from the setup wizard.
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct RuntimeProviderConfig {
|
||||
kind: Option<String>,
|
||||
|
|
@ -326,37 +223,6 @@ impl RuntimeProviderConfig {
|
|||
pub fn model(&self) -> Option<&str> {
|
||||
self.model.as_deref()
|
||||
}
|
||||
=======
|
||||
rules_import: RulesImportConfig,
|
||||
}
|
||||
|
||||
/// Controls which external AI coding framework rules are auto-imported
|
||||
/// into the system prompt.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum RulesImportConfig {
|
||||
/// Auto-import from all supported frameworks (Cursor, Copilot, Windsurf, Aider)
|
||||
Auto,
|
||||
/// No auto-import — only .claw/rules/ and CLAUDE.md files are loaded
|
||||
None,
|
||||
/// Import only from the listed frameworks
|
||||
List(Vec<String>),
|
||||
#[default]
|
||||
/// Default: auto-import all detected frameworks
|
||||
Default,
|
||||
}
|
||||
|
||||
impl RulesImportConfig {
|
||||
pub fn should_import(&self, framework: &str) -> bool {
|
||||
match self {
|
||||
Self::Auto | Self::Default => true,
|
||||
Self::None => false,
|
||||
Self::List(frameworks) => frameworks.iter().any(|f| f.eq_ignore_ascii_case(framework)),
|
||||
}
|
||||
}
|
||||
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||
=======
|
||||
subagent_model: Option<String>,
|
||||
>>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation)
|
||||
}
|
||||
|
||||
/// Ordered chain of fallback model identifiers used when the primary
|
||||
|
|
@ -630,7 +496,6 @@ impl ConfigLoader {
|
|||
build_runtime_config(merged, loaded_entries, mcp)
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/// Like [`load`] but also returns the list of validation warnings collected during
|
||||
/// loading, without emitting them to stderr. Callers that want to surface warnings
|
||||
/// through a structured channel (e.g. the JSON config envelope) should use this.
|
||||
|
|
@ -787,50 +652,6 @@ impl ConfigLoader {
|
|||
load_error.get_or_insert_with(|| error.to_string());
|
||||
None
|
||||
}
|
||||
=======
|
||||
let feature_config = RuntimeFeatureConfig {
|
||||
hooks: parse_optional_hooks_config(&merged_value)?,
|
||||
plugins: parse_optional_plugin_config(&merged_value)?,
|
||||
mcp: McpConfigCollection {
|
||||
servers: mcp_servers,
|
||||
},
|
||||
oauth: parse_optional_oauth_config(&merged_value, "merged settings.oauth")?,
|
||||
model: parse_optional_model(&merged_value),
|
||||
aliases: parse_optional_aliases(&merged_value)?,
|
||||
permission_mode: parse_optional_permission_mode(&merged_value)?,
|
||||
permission_rules: parse_optional_permission_rules(&merged_value)?,
|
||||
sandbox: parse_optional_sandbox_config(&merged_value)?,
|
||||
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
|
||||
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
provider: parse_optional_provider_config(&merged_value)?,
|
||||
lsp: parse_optional_lsp_config(&merged_value)?,
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
lsp_auto_start: merged_value
|
||||
.as_object()
|
||||
.and_then(|o| o.get("lspAutoStart"))
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(true),
|
||||
<<<<<<< HEAD
|
||||
api_timeout: parse_optional_api_timeout_config(&merged_value)?,
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
=======
|
||||
rules_import: parse_optional_rules_import(&merged_value)?,
|
||||
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||
=======
|
||||
subagent_model: parse_optional_subagent_model(&merged_value),
|
||||
>>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation)
|
||||
};
|
||||
|
||||
ConfigInspection {
|
||||
|
|
@ -1089,23 +910,16 @@ impl RuntimeConfig {
|
|||
&self.feature_config.trusted_roots
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
#[must_use]
|
||||
<<<<<<< HEAD
|
||||
pub fn rules_import(&self) -> &RulesImportConfig {
|
||||
&self.feature_config.rules_import
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
pub fn provider(&self) -> &RuntimeProviderConfig {
|
||||
&self.feature_config.provider
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
/// Merge config-level default trusted roots with per-call roots.
|
||||
///
|
||||
/// Config roots are defaults and are kept first; per-call roots extend the
|
||||
|
|
@ -1115,40 +929,7 @@ impl RuntimeConfig {
|
|||
#[must_use]
|
||||
pub fn trusted_roots_with_overrides(&self, per_call_roots: &[String]) -> Vec<String> {
|
||||
merge_trusted_roots(self.trusted_roots(), per_call_roots)
|
||||
=======
|
||||
#[must_use]
|
||||
pub fn lsp(&self) -> &BTreeMap<String, LspServerConfig> {
|
||||
&self.feature_config.lsp
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
#[must_use]
|
||||
pub fn lsp(&self) -> &BTreeMap<String, LspServerConfig> {
|
||||
&self.feature_config.lsp
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn lsp_auto_start(&self) -> bool {
|
||||
self.feature_config.lsp_auto_start
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
pub fn rules_import(&self) -> &RulesImportConfig {
|
||||
&self.feature_config.rules_import
|
||||
}
|
||||
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||
=======
|
||||
=======
|
||||
|
||||
>>>>>>> 6b0af2bc (refactor: remove SubAgent tool, make Agent use subagentModel config)
|
||||
#[must_use]
|
||||
pub fn subagent_model(&self) -> Option<&str> {
|
||||
self.feature_config.subagent_model.as_deref()
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation)
|
||||
=======
|
||||
>>>>>>> 6b0af2bc (refactor: remove SubAgent tool, make Agent use subagentModel config)
|
||||
}
|
||||
|
||||
impl RuntimeFeatureConfig {
|
||||
|
|
@ -1227,12 +1008,10 @@ impl RuntimeFeatureConfig {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
<<<<<<< HEAD
|
||||
pub fn rules_import(&self) -> &RulesImportConfig {
|
||||
&self.rules_import
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/// Merge this config's default trusted roots with per-call roots.
|
||||
#[must_use]
|
||||
pub fn trusted_roots_with_overrides(&self, per_call_roots: &[String]) -> Vec<String> {
|
||||
|
|
@ -1248,29 +1027,6 @@ fn merge_trusted_roots(config_roots: &[String], per_call_roots: &[String]) -> Ve
|
|||
}
|
||||
}
|
||||
merged
|
||||
=======
|
||||
=======
|
||||
pub fn provider(&self) -> &RuntimeProviderConfig {
|
||||
&self.provider
|
||||
}
|
||||
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
#[must_use]
|
||||
pub fn lsp(&self) -> &BTreeMap<String, LspServerConfig> {
|
||||
&self.lsp
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
|
||||
#[must_use]
|
||||
pub fn lsp_auto_start(&self) -> bool {
|
||||
self.lsp_auto_start
|
||||
}
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
}
|
||||
|
||||
impl ProviderFallbackConfig {
|
||||
|
|
@ -1961,19 +1717,6 @@ fn parse_optional_model(root: &JsonValue) -> Option<String> {
|
|||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn parse_optional_subagent_model(value: &JsonValue) -> Option<String> {
|
||||
value
|
||||
.as_object()
|
||||
.and_then(|object| {
|
||||
object
|
||||
.get("subagentModel")
|
||||
.or_else(|| object.get("subagent_model"))
|
||||
})
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
fn parse_optional_aliases(root: &JsonValue) -> Result<BTreeMap<String, String>, ConfigError> {
|
||||
let Some(object) = root.as_object() else {
|
||||
return Ok(BTreeMap::new());
|
||||
|
|
@ -2357,15 +2100,8 @@ fn parse_optional_api_timeout_config(root: &JsonValue) -> Result<ApiTimeoutConfi
|
|||
return Ok(ApiTimeoutConfig::default());
|
||||
};
|
||||
let context = "merged settings.apiTimeout";
|
||||
<<<<<<< HEAD
|
||||
let connect_timeout_secs = optional_u64(obj, "connectTimeout", context)?.unwrap_or(30);
|
||||
let request_timeout_secs = optional_u64(obj, "requestTimeout", context)?.unwrap_or(300);
|
||||
=======
|
||||
let connect_timeout_secs = optional_u64(obj, "connectTimeout", context)?
|
||||
.unwrap_or(30);
|
||||
let request_timeout_secs = optional_u64(obj, "requestTimeout", context)?
|
||||
.unwrap_or(300);
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
let max_retries = optional_u64(obj, "maxRetries", context)?
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(8);
|
||||
|
|
@ -2386,7 +2122,6 @@ fn parse_optional_trusted_roots(root: &JsonValue) -> Result<Vec<String>, ConfigE
|
|||
)
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, ConfigError> {
|
||||
let Some(object) = root.as_object() else {
|
||||
return Ok(RulesImportConfig::default());
|
||||
|
|
@ -2437,36 +2172,6 @@ fn parse_optional_provider_config(root: &JsonValue) -> Result<RuntimeProviderCon
|
|||
})
|
||||
}
|
||||
|
||||
=======
|
||||
|
||||
fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, ConfigError> {
|
||||
let Some(object) = root.as_object() else {
|
||||
return Ok(RulesImportConfig::Default);
|
||||
};
|
||||
let Some(value) = object.get("rulesImport") else {
|
||||
return Ok(RulesImportConfig::Default);
|
||||
};
|
||||
match value {
|
||||
JsonValue::String(s) => match s.as_str() {
|
||||
"auto" => Ok(RulesImportConfig::Auto),
|
||||
"none" => Ok(RulesImportConfig::None),
|
||||
other => Err(ConfigError::Parse(format!(
|
||||
r#"merged settings.rulesImport: expected "auto", "none", or an array, got "{other}""#
|
||||
))),
|
||||
},
|
||||
JsonValue::Array(arr) => {
|
||||
let frameworks: Vec<String> = arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(str::to_owned))
|
||||
.collect();
|
||||
Ok(RulesImportConfig::List(frameworks))
|
||||
}
|
||||
_ => Err(ConfigError::Parse(format!(
|
||||
r#"merged settings.rulesImport: expected "auto", "none", or an array"#
|
||||
))),
|
||||
}
|
||||
}
|
||||
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
|
||||
match value {
|
||||
"off" => Ok(FilesystemIsolationMode::Off),
|
||||
|
|
@ -2503,7 +2208,6 @@ fn parse_optional_oauth_config(
|
|||
}))
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
/// #92: expand `${VAR}` environment variable references and `~/` home directory
|
||||
/// prefix in a config string value. Returns the expanded string.
|
||||
fn expand_config_value(value: &str) -> String {
|
||||
|
|
@ -2541,53 +2245,6 @@ fn expand_config_value(value: &str) -> String {
|
|||
}
|
||||
}
|
||||
result
|
||||
=======
|
||||
fn parse_optional_provider_config(root: &JsonValue) -> Result<RuntimeProviderConfig, ConfigError> {
|
||||
let Some(provider_value) = root.as_object().and_then(|object| object.get("provider")) else {
|
||||
return Ok(RuntimeProviderConfig::default());
|
||||
};
|
||||
let Some(object) = provider_value.as_object() else {
|
||||
return Ok(RuntimeProviderConfig::default());
|
||||
};
|
||||
let kind = optional_string(object, "kind", "provider")?.map(str::to_string);
|
||||
let api_key = optional_string(object, "apiKey", "provider")?.map(str::to_string);
|
||||
let base_url = optional_string(object, "baseUrl", "provider")?.map(str::to_string);
|
||||
let model = optional_string(object, "model", "provider")?.map(str::to_string);
|
||||
Ok(RuntimeProviderConfig {
|
||||
kind,
|
||||
api_key,
|
||||
base_url,
|
||||
model,
|
||||
})
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
}
|
||||
|
||||
fn parse_optional_lsp_config(
|
||||
root: &JsonValue,
|
||||
) -> Result<BTreeMap<String, LspServerConfig>, ConfigError> {
|
||||
let Some(lsp_value) = root.as_object().and_then(|object| object.get("lsp")) else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
let lsp_object = expect_object(lsp_value, "merged settings.lsp")?;
|
||||
let mut result = BTreeMap::new();
|
||||
for (language, value) in lsp_object {
|
||||
let entry = expect_object(value, &format!("merged settings.lsp.{language}"))?;
|
||||
let command = expect_string(entry, "command", &format!("merged settings.lsp.{language}"))?
|
||||
.to_string();
|
||||
let args = optional_string_array(entry, "args", &format!("merged settings.lsp.{language}"))?
|
||||
.unwrap_or_default();
|
||||
let enabled = optional_bool(entry, "enabled", &format!("merged settings.lsp.{language}"))?
|
||||
.unwrap_or(true);
|
||||
result.insert(
|
||||
language.clone(),
|
||||
LspServerConfig {
|
||||
command,
|
||||
args,
|
||||
enabled,
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn parse_mcp_server_config(
|
||||
|
|
|
|||
|
|
@ -209,44 +209,16 @@ const TOP_LEVEL_FIELDS: &[FieldSpec] = &[
|
|||
expected: FieldType::StringArray,
|
||||
},
|
||||
FieldSpec {
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
name: "provider",
|
||||
expected: FieldType::Object,
|
||||
},
|
||||
FieldSpec {
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
name: "rulesImport",
|
||||
expected: FieldType::RulesImport,
|
||||
},
|
||||
FieldSpec {
|
||||
name: "subagentModel",
|
||||
expected: FieldType::String,
|
||||
=======
|
||||
name: "lsp",
|
||||
expected: FieldType::Object,
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
},
|
||||
FieldSpec {
|
||||
name: "lspAutoStart",
|
||||
expected: FieldType::Bool,
|
||||
=======
|
||||
name: "lsp",
|
||||
expected: FieldType::Object,
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
},
|
||||
FieldSpec {
|
||||
name: "lspAutoStart",
|
||||
expected: FieldType::Bool,
|
||||
=======
|
||||
name: "rulesImport",
|
||||
expected: FieldType::String,
|
||||
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||
=======
|
||||
name: "subagentModel",
|
||||
expected: FieldType::String,
|
||||
>>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation)
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -365,19 +337,22 @@ const OAUTH_FIELDS: &[FieldSpec] = &[
|
|||
},
|
||||
];
|
||||
|
||||
|
||||
const LSP_FIELDS: &[FieldSpec] = &[
|
||||
const PROVIDER_FIELDS: &[FieldSpec] = &[
|
||||
FieldSpec {
|
||||
name: "command",
|
||||
name: "kind",
|
||||
expected: FieldType::String,
|
||||
},
|
||||
FieldSpec {
|
||||
name: "args",
|
||||
expected: FieldType::StringArray,
|
||||
name: "apiKey",
|
||||
expected: FieldType::String,
|
||||
},
|
||||
FieldSpec {
|
||||
name: "enabled",
|
||||
expected: FieldType::Bool,
|
||||
name: "baseUrl",
|
||||
expected: FieldType::String,
|
||||
},
|
||||
FieldSpec {
|
||||
name: "model",
|
||||
expected: FieldType::String,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -581,56 +556,6 @@ pub fn validate_config_file(
|
|||
));
|
||||
}
|
||||
|
||||
// Validate lsp map: each value must be an object with LSP_FIELDS.
|
||||
if let Some(lsp) = object.get("lsp").and_then(JsonValue::as_object) {
|
||||
for (server_name, server_value) in lsp {
|
||||
if let Some(server_obj) = server_value.as_object() {
|
||||
result.merge(validate_object_keys(
|
||||
server_obj,
|
||||
LSP_FIELDS,
|
||||
&format!("lsp.{server_name}"),
|
||||
source,
|
||||
&path_display,
|
||||
));
|
||||
} else {
|
||||
result.errors.push(ConfigDiagnostic {
|
||||
path: path_display.clone(),
|
||||
field: format!("lsp.{server_name}"),
|
||||
line: find_key_line(source, server_name),
|
||||
kind: DiagnosticKind::WrongType {
|
||||
expected: "an object",
|
||||
got: json_type_label(server_value),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate lsp map: each value must be an object with LSP_FIELDS.
|
||||
if let Some(lsp) = object.get("lsp").and_then(JsonValue::as_object) {
|
||||
for (server_name, server_value) in lsp {
|
||||
if let Some(server_obj) = server_value.as_object() {
|
||||
result.merge(validate_object_keys(
|
||||
server_obj,
|
||||
LSP_FIELDS,
|
||||
&format!("lsp.{server_name}"),
|
||||
source,
|
||||
&path_display,
|
||||
));
|
||||
} else {
|
||||
result.errors.push(ConfigDiagnostic {
|
||||
path: path_display.clone(),
|
||||
field: format!("lsp.{server_name}"),
|
||||
line: find_key_line(source, server_name),
|
||||
kind: DiagnosticKind::WrongType {
|
||||
expected: "an object",
|
||||
got: json_type_label(server_value),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
|
@ -1090,122 +1015,4 @@ mod tests {
|
|||
r#"/test/settings.json: field "permissionMode" is deprecated (line 3). Use "permissions.defaultMode" instead"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_lsp_config_valid() {
|
||||
// given
|
||||
let source = r#"{"lsp": {"rust": {"command": "rust-analyzer", "args": [], "enabled": true}, "python": {"command": "pyright-langserver", "args": ["--stdio"], "enabled": false}}}"#;
|
||||
let parsed = JsonValue::parse(source).expect("valid json");
|
||||
let object = parsed.as_object().expect("object");
|
||||
|
||||
// when
|
||||
let result = validate_config_file(object, source, &test_path());
|
||||
|
||||
// then
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_lsp_config_unknown_field() {
|
||||
// given
|
||||
let source = r#"{"lsp": {"rust": {"command": "rust-analyzer", "port": 8080}}}"#;
|
||||
let parsed = JsonValue::parse(source).expect("valid json");
|
||||
let object = parsed.as_object().expect("object");
|
||||
|
||||
// when
|
||||
let result = validate_config_file(object, source, &test_path());
|
||||
|
||||
// then
|
||||
assert_eq!(result.errors.len(), 1);
|
||||
assert_eq!(result.errors[0].field, "lsp.rust.port");
|
||||
assert!(matches!(
|
||||
result.errors[0].kind,
|
||||
DiagnosticKind::UnknownKey { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_lsp_config_wrong_type_for_command() {
|
||||
// given
|
||||
let source = r#"{"lsp": {"rust": {"command": 123}}}"#;
|
||||
let parsed = JsonValue::parse(source).expect("valid json");
|
||||
let object = parsed.as_object().expect("object");
|
||||
|
||||
// when
|
||||
let result = validate_config_file(object, source, &test_path());
|
||||
|
||||
// then
|
||||
assert_eq!(result.errors.len(), 1);
|
||||
assert_eq!(result.errors[0].field, "lsp.rust.command");
|
||||
assert!(matches!(
|
||||
result.errors[0].kind,
|
||||
DiagnosticKind::WrongType {
|
||||
expected: "a string",
|
||||
got: "a number"
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_lsp_config_wrong_type_for_args() {
|
||||
// given
|
||||
let source = r#"{"lsp": {"rust": {"command": "rust-analyzer", "args": "wrong"}}}"#;
|
||||
let parsed = JsonValue::parse(source).expect("valid json");
|
||||
let object = parsed.as_object().expect("object");
|
||||
|
||||
// when
|
||||
let result = validate_config_file(object, source, &test_path());
|
||||
|
||||
// then
|
||||
assert_eq!(result.errors.len(), 1);
|
||||
assert_eq!(result.errors[0].field, "lsp.rust.args");
|
||||
assert!(matches!(
|
||||
result.errors[0].kind,
|
||||
DiagnosticKind::WrongType { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_lsp_config_wrong_type_for_enabled() {
|
||||
// given
|
||||
let source = r#"{"lsp": {"rust": {"command": "rust-analyzer", "enabled": "yes"}}}"#;
|
||||
let parsed = JsonValue::parse(source).expect("valid json");
|
||||
let object = parsed.as_object().expect("object");
|
||||
|
||||
// when
|
||||
let result = validate_config_file(object, source, &test_path());
|
||||
|
||||
// then
|
||||
assert_eq!(result.errors.len(), 1);
|
||||
assert_eq!(result.errors[0].field, "lsp.rust.enabled");
|
||||
assert!(matches!(
|
||||
result.errors[0].kind,
|
||||
DiagnosticKind::WrongType {
|
||||
expected: "a boolean",
|
||||
got: "a string"
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_lsp_server_must_be_object() {
|
||||
// given
|
||||
let source = r#"{"lsp": {"rust": "not-an-object"}}"#;
|
||||
let parsed = JsonValue::parse(source).expect("valid json");
|
||||
let object = parsed.as_object().expect("object");
|
||||
|
||||
// when
|
||||
let result = validate_config_file(object, source, &test_path());
|
||||
|
||||
// then
|
||||
assert_eq!(result.errors.len(), 1);
|
||||
assert_eq!(result.errors[0].field, "lsp.rust");
|
||||
assert!(matches!(
|
||||
result.errors[0].kind,
|
||||
DiagnosticKind::WrongType {
|
||||
expected: "an object",
|
||||
got: "a string"
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ pub mod bash_validation;
|
|||
mod bootstrap;
|
||||
pub mod branch_lock;
|
||||
mod compact;
|
||||
pub mod trident;
|
||||
mod config;
|
||||
pub mod config_validate;
|
||||
mod conversation;
|
||||
|
|
@ -69,10 +68,6 @@ pub use compact::{
|
|||
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
|
||||
};
|
||||
pub use config::{
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
clear_user_provider_settings, default_config_home, save_user_provider_settings,
|
||||
suppress_config_warnings_for_json_mode, ApiTimeoutConfig, ConfigEntry, ConfigError,
|
||||
ConfigFileReport, ConfigFileStatus, ConfigInspection, ConfigLoader, ConfigSource,
|
||||
|
|
@ -82,49 +77,15 @@ pub use config::{
|
|||
RulesImportConfig, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookCommand, RuntimeHookConfig,
|
||||
RuntimeInvalidHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig,
|
||||
RuntimeProviderConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||
=======
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
clear_user_provider_settings, save_user_provider_settings, ConfigEntry, ConfigError,
|
||||
=======
|
||||
ConfigEntry, ConfigError,
|
||||
>>>>>>> 0b227b62 (fix: resolve cherry-pick conflicts and remove non-LSP artifacts)
|
||||
ConfigLoader, ConfigSource, LspServerConfig, McpConfigCollection, McpManagedProxyServerConfig,
|
||||
McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig,
|
||||
McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
|
||||
<<<<<<< HEAD
|
||||
ApiTimeoutConfig, ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection,
|
||||
=======
|
||||
ApiTimeoutConfig, clear_user_provider_settings, save_user_provider_settings, ConfigEntry,
|
||||
ConfigError, ConfigLoader, ConfigSource, LspServerConfig, McpConfigCollection,
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
|
||||
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
ProviderFallbackConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig,
|
||||
RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, RuntimeProviderConfig,
|
||||
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
ProviderFallbackConfig, ResolvedPermissionMode, RulesImportConfig, RuntimeConfig,
|
||||
RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig,
|
||||
ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME,
|
||||
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||
};
|
||||
pub use config_validate::{
|
||||
check_unsupported_format, format_diagnostics, validate_config_file, ConfigDiagnostic,
|
||||
DiagnosticKind, ValidationResult,
|
||||
};
|
||||
pub use lsp_discovery::{
|
||||
command_exists_on_path, discover_available_servers, find_server_for_file,
|
||||
known_lsp_servers, LspServerDescriptor,
|
||||
};
|
||||
pub use conversation::{
|
||||
auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent,
|
||||
ConversationRuntime, PromptCacheEvent, RuntimeError, StaticToolExecutor, ToolCall, ToolError,
|
||||
ToolExecutor, ToolResult, TurnProgressReporter, TurnSummary,
|
||||
ConversationRuntime, PromptCacheEvent, RuntimeError, StaticToolExecutor, ToolError,
|
||||
ToolExecutor, TurnSummary,
|
||||
};
|
||||
pub use file_ops::{
|
||||
edit_file, edit_file_in_workspace, glob_search, glob_search_in_workspace, grep_search,
|
||||
|
|
|
|||
|
|
@ -68,15 +68,14 @@ impl super::LspRegistry {
|
|||
{
|
||||
let inner = self.inner.lock().expect("lsp registry lock poisoned");
|
||||
if let Some(entry) = inner.servers.get(&language) {
|
||||
if entry.state.status == LspServerStatus::Disconnected
|
||||
|| entry.state.status == LspServerStatus::Error
|
||||
if (entry.state.status == LspServerStatus::Disconnected
|
||||
|| entry.state.status == LspServerStatus::Error)
|
||||
&& entry.process.is_none()
|
||||
{
|
||||
if entry.process.is_none() {
|
||||
return Err(format!(
|
||||
"LSP server for '{}' is not connected (status: {})",
|
||||
language, entry.state.status
|
||||
));
|
||||
}
|
||||
return Err(format!(
|
||||
"LSP server for '{}' is not connected (status: {})",
|
||||
language, entry.state.status
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,23 +267,11 @@ pub fn known_lsp_servers() -> Vec<LspServerDescriptor> {
|
|||
/// Check whether a command exists on the user's PATH by attempting to run it
|
||||
/// with `--version`. Returns `true` if the command could be spawned
|
||||
/// successfully, `false` otherwise.
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
///
|
||||
/// Some LSP servers (like rust-analyzer via rustup) exit non-zero on --version
|
||||
/// but are still functional. We treat "spawned successfully" as found, regardless
|
||||
/// of the exit code. Only a failure to spawn (command not found) returns false.
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
|
||||
#[must_use]
|
||||
pub fn command_exists_on_path(command: &str) -> bool {
|
||||
Command::new(command)
|
||||
.arg("--version")
|
||||
.output()
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
|
|
@ -307,16 +295,6 @@ fn rustup_component_works(component: &str) -> bool {
|
|||
.args(["run", "stable", component, "--version"])
|
||||
.output()
|
||||
.is_ok_and(|o| o.status.success())
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
.is_ok()
|
||||
>>>>>>> 73cc8277 (feat: auto-LSP integration with didOpen/didChange and diagnostic enrichment)
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
}
|
||||
|
||||
/// Detect the current platform/distro for install suggestion filtering.
|
||||
|
|
@ -471,35 +449,11 @@ pub fn format_install_prompt(actions: &[LspInstallAction]) -> String {
|
|||
}
|
||||
|
||||
/// Discover LSP servers that are actually installed on the current system.
|
||||
<<<<<<< HEAD
|
||||
///
|
||||
/// Iterates over the known server table and returns only those whose command
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
/// is found on `PATH` **and** is actually functional. For `rust-analyzer`,
|
||||
/// rustup ships a stub proxy that always exists on PATH but prints
|
||||
/// "Unknown binary" when the component isn't installed. We detect that
|
||||
/// case and either rewrite to `rustup run stable rust-analyzer` (when the
|
||||
/// component is installed) or skip the server entirely (when it's not).
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
/// is found on `PATH`.
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
=======
|
||||
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
|
||||
#[must_use]
|
||||
pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
|
||||
KNOWN_LSP_SERVERS_TABLE
|
||||
.iter()
|
||||
.filter(|desc| command_exists_on_path(desc.command))
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
.filter_map(|desc| {
|
||||
let mut server = desc.to_descriptor();
|
||||
if desc.command == "rust-analyzer" && is_rustup_proxy("rust-analyzer") {
|
||||
|
|
@ -516,12 +470,6 @@ pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
|
|||
}
|
||||
Some(server)
|
||||
})
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
.map(StaticLspServerDescriptor::to_descriptor)
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ pub(super) fn parse_code_actions(value: &JsonValue) -> Vec<crate::lsp_client::Ls
|
|||
let title = item.get("title")?.as_str()?.to_owned();
|
||||
let kind = item.get("kind").and_then(JsonValue::as_str).map(str::to_owned);
|
||||
let is_preferred = item.get("isPreferred").and_then(JsonValue::as_bool).unwrap_or(false);
|
||||
let edit = item.get("edit").and_then(|e| parse_workspace_edit(e));
|
||||
let edit = item.get("edit").and_then(parse_workspace_edit);
|
||||
let command = item.get("command").and_then(parse_command);
|
||||
Some(crate::lsp_client::LspCodeAction { title, kind, is_preferred, edit, command })
|
||||
}).collect()
|
||||
|
|
@ -372,12 +372,12 @@ pub(super) fn parse_signature_help(value: &JsonValue) -> Option<crate::lsp_clien
|
|||
.and_then(|d| d.get("value").and_then(JsonValue::as_str).or_else(|| d.as_str()))
|
||||
.map(str::to_owned);
|
||||
let parameters = sig.get("parameters").and_then(JsonValue::as_array)
|
||||
.map(|arr| arr.iter().filter_map(|p| {
|
||||
.map(|arr| arr.iter().map(|p| {
|
||||
let plabel = p.get("label").and_then(|l| l.as_str().or_else(|| l.get("value").and_then(JsonValue::as_str))).unwrap_or("").to_owned();
|
||||
let pdoc = p.get("documentation")
|
||||
.and_then(|d| d.get("value").and_then(JsonValue::as_str).or_else(|| d.as_str()))
|
||||
.map(str::to_owned);
|
||||
Some(crate::lsp_client::LspParameterInfo { label: plabel, documentation: pdoc })
|
||||
crate::lsp_client::LspParameterInfo { label: plabel, documentation: pdoc }
|
||||
}).collect())
|
||||
.unwrap_or_default();
|
||||
let active_parameter = sig.get("activeParameter").and_then(JsonValue::as_u64).map(|v| v as u32);
|
||||
|
|
|
|||
|
|
@ -295,7 +295,6 @@ fn discover_instruction_files(
|
|||
|
||||
let mut files = Vec::new();
|
||||
for dir in directories {
|
||||
// Single-file instruction files (existing)
|
||||
for candidate in [
|
||||
dir.join("CLAUDE.md"),
|
||||
dir.join("CLAW.md"),
|
||||
|
|
@ -307,23 +306,13 @@ fn discover_instruction_files(
|
|||
] {
|
||||
push_context_file(&mut files, candidate)?;
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
push_rules_dir(&mut files, dir.join(".claw").join("rules"))?;
|
||||
push_rules_dir(&mut files, dir.join(".claw").join("rules.local"))?;
|
||||
push_framework_imports(&mut files, &dir, rules_import)?
|
||||
=======
|
||||
// .claw/rules/ directory: all .md files loaded in sorted order
|
||||
push_rules_dir(&mut files, dir.join(".claw").join("rules"))?;
|
||||
// .claw/rules.local/ directory: personal/local rules (gitignored)
|
||||
push_rules_dir(&mut files, dir.join(".claw").join("rules.local"))?;
|
||||
// Auto-import from other frameworks (Cursor, Copilot, Windsurf, Aider)
|
||||
push_framework_imports(&mut files, &dir)?;
|
||||
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||
}
|
||||
Ok(dedupe_instruction_files(files))
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
fn instruction_discovery_dirs(cwd: &Path) -> Vec<PathBuf> {
|
||||
let boundary = nearest_git_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
|
||||
let mut directories = Vec::new();
|
||||
|
|
@ -346,94 +335,6 @@ fn nearest_git_root(cwd: &Path) -> Option<PathBuf> {
|
|||
return Some(dir.to_path_buf());
|
||||
}
|
||||
cursor = dir.parent();
|
||||
=======
|
||||
/// Load all .md files from a rules directory, sorted alphabetically.
|
||||
fn push_rules_dir(files: &mut Vec<ContextFile>, dir: PathBuf) -> std::io::Result<()> {
|
||||
let entries = match fs::read_dir(&dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let mut paths: Vec<PathBuf> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| {
|
||||
p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
|
||||
|| p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("txt"))
|
||||
|| p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("mdc"))
|
||||
})
|
||||
.collect();
|
||||
paths.sort();
|
||||
for path in paths {
|
||||
push_context_file(files, path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Detect and import rules from other AI coding frameworks so that
|
||||
/// users switching to claw-code don't have to duplicate their rules.
|
||||
///
|
||||
/// Supported frameworks:
|
||||
/// - Cursor: .cursorrules, .cursor/rules/
|
||||
/// - GitHub Copilot: .github/copilot-instructions.md
|
||||
/// - Windsurf: .windsurfrules, .windsurfrules/
|
||||
/// - Aider: .aider.conf.yml instructions block
|
||||
/// - Pi (Plandex): .plandex/plan.md, .plandex/instructions.md
|
||||
/// - OpenCode: opencode.json instructions field
|
||||
/// - CrushCode / Crush: .crush/rules/, .crush/CLAUDE.md
|
||||
fn push_framework_imports(files: &mut Vec<ContextFile>, dir: &Path) -> std::io::Result<()> {
|
||||
// Cursor
|
||||
push_context_file(files, dir.join(".cursorrules"))?;
|
||||
push_rules_dir(files, dir.join(".cursor").join("rules"))?;
|
||||
// GitHub Copilot
|
||||
push_context_file(files, dir.join(".github").join("copilot-instructions.md"))?;
|
||||
// Windsurf
|
||||
push_context_file(files, dir.join(".windsurfrules"))?;
|
||||
push_rules_dir(files, dir.join(".windsurfrules"))?;
|
||||
// Aider — reads the instruction lines from .aider.conf.yml
|
||||
if let Some(aider_instructions) = read_aider_instructions(dir) {
|
||||
files.push(ContextFile {
|
||||
path: dir.join(".aider.conf.yml").join("instructions"),
|
||||
content: aider_instructions,
|
||||
});
|
||||
}
|
||||
// Pi (Plandex)
|
||||
push_context_file(files, dir.join(".plandex").join("instructions.md"))?;
|
||||
push_context_file(files, dir.join(".plandex").join("plan.md"))?;
|
||||
// OpenCode — reads instructions from opencode.json config
|
||||
if let Some(opencode_instructions) = read_opencode_instructions(dir) {
|
||||
files.push(ContextFile {
|
||||
path: dir.join("opencode.json").join("instructions"),
|
||||
content: opencode_instructions,
|
||||
});
|
||||
}
|
||||
// CrushCode / Crush
|
||||
push_context_file(files, dir.join(".crush").join("CLAUDE.md"))?;
|
||||
push_rules_dir(files, dir.join(".crush").join("rules"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract instructions from an opencode.json config file.
|
||||
/// OpenCode stores rules in a top-level "instructions" field.
|
||||
fn read_opencode_instructions(dir: &Path) -> Option<String> {
|
||||
let content = fs::read_to_string(dir.join("opencode.json")).ok()?;
|
||||
let parsed: serde_json::Value = serde_json::from_str(&content).ok()?;
|
||||
parsed.get("instructions")?.as_str().map(str::to_owned)
|
||||
}
|
||||
|
||||
/// Extract instruction lines from an .aider.conf.yml file.
|
||||
/// Aider stores instructions like: `instructions: ...` or multiline block.
|
||||
fn read_aider_instructions(dir: &Path) -> Option<String> {
|
||||
let content = fs::read_to_string(dir.join(".aider.conf.yml")).ok()?;
|
||||
for line in content.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(val) = trimmed.strip_prefix("instructions:") {
|
||||
let instruction = val.trim();
|
||||
if !instruction.is_empty() {
|
||||
return Some(instruction.to_owned());
|
||||
}
|
||||
}
|
||||
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
|
||||
}
|
||||
None
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,11 +298,7 @@ fn unshare_user_namespace_works() -> bool {
|
|||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
<<<<<<< HEAD
|
||||
.is_ok_and(|status| status.success())
|
||||
=======
|
||||
.is_ok_and(|s| s.success())
|
||||
>>>>>>> 0b227b62 (fix: resolve cherry-pick conflicts and remove non-LSP artifacts)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ impl Default for TridentConfig {
|
|||
}
|
||||
|
||||
/// Statistics from a Trident compaction run.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TridentStats {
|
||||
pub superseded_count: usize,
|
||||
pub collapsed_chains: usize,
|
||||
|
|
@ -41,21 +41,6 @@ pub struct TridentStats {
|
|||
pub final_message_count: usize,
|
||||
}
|
||||
|
||||
impl Default for TridentStats {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
superseded_count: 0,
|
||||
collapsed_chains: 0,
|
||||
messages_collapsed: 0,
|
||||
clusters_found: 0,
|
||||
messages_clustered: 0,
|
||||
tokens_saved_estimate: 0,
|
||||
original_message_count: 0,
|
||||
final_message_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TridentStats {
|
||||
pub fn format_report(&self) -> String {
|
||||
let compression = if self.final_message_count > 0 {
|
||||
|
|
@ -78,14 +63,10 @@ impl TridentStats {
|
|||
self.messages_clustered, self.clusters_found
|
||||
),
|
||||
format!(" Original: {} messages", self.original_message_count),
|
||||
<<<<<<< HEAD
|
||||
format!(
|
||||
" Final: {} messages ({:.1}x compression)",
|
||||
self.final_message_count, compression
|
||||
),
|
||||
=======
|
||||
format!(" Final: {} messages ({:.1}x compression)", self.final_message_count, compression),
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
];
|
||||
if self.tokens_saved_estimate > 0 {
|
||||
lines.push(format!(
|
||||
|
|
@ -128,12 +109,8 @@ pub fn trident_compact_session(
|
|||
}
|
||||
|
||||
if trident_config.collapse_enabled {
|
||||
<<<<<<< HEAD
|
||||
let (collapsed, chains, collapsed_count) =
|
||||
stage2_collapse(&messages, trident_config.collapse_threshold);
|
||||
=======
|
||||
let (collapsed, chains, collapsed_count) = stage2_collapse(&messages, trident_config.collapse_threshold);
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
stats.collapsed_chains = chains;
|
||||
stats.messages_collapsed = collapsed_count;
|
||||
messages = collapsed;
|
||||
|
|
@ -190,24 +167,17 @@ fn stage1_supersede(messages: &[ConversationMessage]) -> (Vec<ConversationMessag
|
|||
for (i, msg) in messages.iter().enumerate() {
|
||||
for block in &msg.blocks {
|
||||
if let Some((path, op_type)) = extract_file_operation(block) {
|
||||
<<<<<<< HEAD
|
||||
file_ops
|
||||
.entry(path)
|
||||
.or_default()
|
||||
.push(FileOperation { index: i, op_type });
|
||||
=======
|
||||
file_ops.entry(path).or_default().push(FileOperation {
|
||||
index: i,
|
||||
op_type,
|
||||
});
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut obsolete_indices: BTreeSet<usize> = BTreeSet::new();
|
||||
|
||||
for (_path, ops) in &file_ops {
|
||||
for ops in file_ops.values() {
|
||||
if ops.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -220,9 +190,7 @@ fn stage1_supersede(messages: &[ConversationMessage]) -> (Vec<ConversationMessag
|
|||
|
||||
if let Some(last_write) = last_write_idx {
|
||||
for op in ops {
|
||||
if op.op_type == FileOp::Read && op.index < last_write {
|
||||
obsolete_indices.insert(op.index);
|
||||
} else if (op.op_type == FileOp::Write || op.op_type == FileOp::Edit)
|
||||
if (op.op_type == FileOp::Read || op.op_type == FileOp::Write || op.op_type == FileOp::Edit)
|
||||
&& op.index < last_write
|
||||
{
|
||||
obsolete_indices.insert(op.index);
|
||||
|
|
@ -254,13 +222,9 @@ fn extract_file_operation(block: &ContentBlock) -> Option<(String, FileOp)> {
|
|||
};
|
||||
Some((path, op_type))
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
ContentBlock::ToolResult {
|
||||
tool_name, output, ..
|
||||
} => {
|
||||
=======
|
||||
ContentBlock::ToolResult { tool_name, output, .. } => {
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
let path = extract_path_from_tool_output(tool_name, output)?;
|
||||
let op_type = match tool_name.as_str() {
|
||||
"read_file" | "Read" => FileOp::Read,
|
||||
|
|
@ -271,23 +235,15 @@ fn extract_file_operation(block: &ContentBlock) -> Option<(String, FileOp)> {
|
|||
Some((path, op_type))
|
||||
}
|
||||
ContentBlock::Text { .. } => None,
|
||||
<<<<<<< HEAD
|
||||
ContentBlock::Thinking { .. } => None,
|
||||
=======
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option<String> {
|
||||
<<<<<<< HEAD
|
||||
if !matches!(
|
||||
tool_name,
|
||||
"read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit"
|
||||
) {
|
||||
=======
|
||||
if !matches!(tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit")
|
||||
{
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
return None;
|
||||
}
|
||||
serde_json::from_str::<serde_json::Value>(input)
|
||||
|
|
@ -301,15 +257,10 @@ fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option<String>
|
|||
}
|
||||
|
||||
fn extract_path_from_tool_output(tool_name: &str, output: &str) -> Option<String> {
|
||||
<<<<<<< HEAD
|
||||
if !matches!(
|
||||
tool_name,
|
||||
"read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit"
|
||||
) {
|
||||
=======
|
||||
if !matches!(tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit")
|
||||
{
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
return None;
|
||||
}
|
||||
serde_json::from_str::<serde_json::Value>(output)
|
||||
|
|
@ -357,7 +308,7 @@ fn stage2_collapse(
|
|||
usage: None,
|
||||
});
|
||||
} else {
|
||||
result.extend(buffer.drain(..));
|
||||
result.append(&mut buffer);
|
||||
}
|
||||
buffer.clear();
|
||||
result.push(msg.clone());
|
||||
|
|
@ -383,7 +334,6 @@ fn stage2_collapse(
|
|||
}
|
||||
|
||||
fn is_chatty_message(msg: &ConversationMessage) -> bool {
|
||||
<<<<<<< HEAD
|
||||
let total_chars: usize = msg
|
||||
.blocks
|
||||
.iter()
|
||||
|
|
@ -403,16 +353,6 @@ fn is_chatty_message(msg: &ConversationMessage) -> bool {
|
|||
.blocks
|
||||
.iter()
|
||||
.any(|b| matches!(b, ContentBlock::ToolResult { .. }));
|
||||
=======
|
||||
let total_chars: usize = msg.blocks.iter().map(|b| match b {
|
||||
ContentBlock::Text { text } => text.len(),
|
||||
ContentBlock::ToolUse { input, .. } => input.len(),
|
||||
ContentBlock::ToolResult { output, .. } => output.len(),
|
||||
}).sum();
|
||||
|
||||
let has_tool_use = msg.blocks.iter().any(|b| matches!(b, ContentBlock::ToolUse { .. }));
|
||||
let has_tool_result = msg.blocks.iter().any(|b| matches!(b, ContentBlock::ToolResult { .. }));
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
|
||||
if has_tool_use || has_tool_result {
|
||||
return false;
|
||||
|
|
@ -519,7 +459,7 @@ fn stage3_cluster(
|
|||
}
|
||||
|
||||
let total_clustered: usize = cluster_assignments.len();
|
||||
let clusters_found = cluster_id as usize;
|
||||
let clusters_found = cluster_id;
|
||||
|
||||
let mut result: Vec<ConversationMessage> = Vec::new();
|
||||
let mut cluster_buffers: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
|
||||
|
|
@ -528,24 +468,12 @@ fn stage3_cluster(
|
|||
cluster_buffers.entry(cid).or_default().push(*msg_idx);
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
for (i, msg) in messages.iter().enumerate() {
|
||||
if let Some(&cid) = cluster_assignments.get(&i) {
|
||||
if let Some(buffer) = cluster_buffers.get_mut(&cid) {
|
||||
if buffer[0] == i {
|
||||
<<<<<<< HEAD
|
||||
let cluster_messages: Vec<&ConversationMessage> =
|
||||
buffer.iter().filter_map(|&idx| messages.get(idx)).collect();
|
||||
=======
|
||||
let cluster_messages: Vec<&ConversationMessage> = buffer
|
||||
.iter()
|
||||
.filter_map(|&idx| messages.get(idx))
|
||||
.collect();
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
let summary = generate_cluster_summary(&cluster_messages);
|
||||
result.push(ConversationMessage {
|
||||
role: MessageRole::System,
|
||||
|
|
@ -591,13 +519,9 @@ fn fingerprint_message(index: usize, msg: &ConversationMessage) -> Option<Messag
|
|||
}
|
||||
text_length += input.len();
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
ContentBlock::ToolResult {
|
||||
tool_name, output, ..
|
||||
} => {
|
||||
=======
|
||||
ContentBlock::ToolResult { tool_name, output, .. } => {
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
tool_names.insert(tool_name.clone());
|
||||
if let Some(path) = extract_path_from_tool_output(tool_name, output) {
|
||||
file_paths.insert(path);
|
||||
|
|
@ -607,12 +531,9 @@ fn fingerprint_message(index: usize, msg: &ConversationMessage) -> Option<Messag
|
|||
ContentBlock::Text { text } => {
|
||||
text_length += text.len();
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
ContentBlock::Thinking { thinking, .. } => {
|
||||
text_length += thinking.len();
|
||||
}
|
||||
=======
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -676,23 +597,16 @@ fn generate_cluster_summary(messages: &[&ConversationMessage]) -> String {
|
|||
file_paths.insert(path);
|
||||
}
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
ContentBlock::ToolResult {
|
||||
tool_name, output, ..
|
||||
} => {
|
||||
=======
|
||||
ContentBlock::ToolResult { tool_name, output, .. } => {
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
tool_names.insert(tool_name.clone());
|
||||
if let Some(path) = extract_path_from_tool_output(tool_name, output) {
|
||||
file_paths.insert(path);
|
||||
}
|
||||
}
|
||||
ContentBlock::Text { .. } => {}
|
||||
<<<<<<< HEAD
|
||||
ContentBlock::Thinking { .. } => {}
|
||||
=======
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -728,10 +642,7 @@ fn estimate_message_tokens(message: &ConversationMessage) -> usize {
|
|||
ContentBlock::ToolResult {
|
||||
tool_name, output, ..
|
||||
} => (tool_name.len() + output.len()) / 4 + 1,
|
||||
<<<<<<< HEAD
|
||||
ContentBlock::Thinking { thinking, .. } => thinking.len() / 4 + 1,
|
||||
=======
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
|
@ -759,31 +670,23 @@ mod tests {
|
|||
name: "read_file".to_string(),
|
||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||
}]),
|
||||
<<<<<<< HEAD
|
||||
ConversationMessage::tool_result(
|
||||
"1",
|
||||
"read_file",
|
||||
r#"{"path":"src/main.rs","content":"old"}"#,
|
||||
false,
|
||||
),
|
||||
=======
|
||||
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"old"}"#, false),
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
||||
id: "2".to_string(),
|
||||
name: "edit_file".to_string(),
|
||||
input: r#"{"path":"src/main.rs","old":"old","new":"new"}"#.to_string(),
|
||||
}]),
|
||||
<<<<<<< HEAD
|
||||
ConversationMessage::tool_result(
|
||||
"2",
|
||||
"edit_file",
|
||||
r#"{"path":"src/main.rs","ok":true}"#,
|
||||
false,
|
||||
),
|
||||
=======
|
||||
ConversationMessage::tool_result("2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false),
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
];
|
||||
|
||||
let (kept, superseded) = stage1_supersede(&messages);
|
||||
|
|
@ -799,16 +702,12 @@ mod tests {
|
|||
name: "read_file".to_string(),
|
||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||
}]),
|
||||
<<<<<<< HEAD
|
||||
ConversationMessage::tool_result(
|
||||
"1",
|
||||
"read_file",
|
||||
r#"{"path":"src/main.rs","content":"data"}"#,
|
||||
false,
|
||||
),
|
||||
=======
|
||||
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"data"}"#, false),
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
];
|
||||
|
||||
let (kept, superseded) = stage1_supersede(&messages);
|
||||
|
|
@ -825,7 +724,6 @@ mod tests {
|
|||
text: format!("got {i}"),
|
||||
}]));
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
messages.push(ConversationMessage::assistant(vec![
|
||||
ContentBlock::ToolUse {
|
||||
id: "t".to_string(),
|
||||
|
|
@ -833,13 +731,6 @@ mod tests {
|
|||
input: r#"{"command":"ls"}"#.to_string(),
|
||||
},
|
||||
]));
|
||||
=======
|
||||
messages.push(ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
||||
id: "t".to_string(),
|
||||
name: "bash".to_string(),
|
||||
input: r#"{"command":"ls"}"#.to_string(),
|
||||
}]));
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
|
||||
let (result, chains, collapsed) = stage2_collapse(&messages, 4);
|
||||
assert!(chains > 0, "should collapse at least one chain");
|
||||
|
|
@ -851,7 +742,6 @@ mod tests {
|
|||
fn stage3_clusters_similar_messages() {
|
||||
let mut messages = vec![];
|
||||
for i in 0..5 {
|
||||
<<<<<<< HEAD
|
||||
messages.push(ConversationMessage::assistant(vec![
|
||||
ContentBlock::ToolUse {
|
||||
id: format!("read_{i}"),
|
||||
|
|
@ -859,13 +749,6 @@ mod tests {
|
|||
input: format!(r#"{{"path":"src/{i}.rs"}}"#),
|
||||
},
|
||||
]));
|
||||
=======
|
||||
messages.push(ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
||||
id: format!("read_{i}"),
|
||||
name: "read_file".to_string(),
|
||||
input: format!(r#"{{"path":"src/{i}.rs"}}"#),
|
||||
}]));
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
messages.push(ConversationMessage::tool_result(
|
||||
&format!("read_{i}"),
|
||||
"read_file",
|
||||
|
|
@ -874,12 +757,7 @@ mod tests {
|
|||
));
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
let (result, clusters, clustered) = stage3_cluster(&messages, 3, 0.4);
|
||||
=======
|
||||
let (result, clusters, clustered) =
|
||||
stage3_cluster(&messages, 3, 0.4);
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
assert!(clusters > 0, "should find at least one cluster");
|
||||
assert!(clustered > 0);
|
||||
assert!(result.len() < messages.len());
|
||||
|
|
@ -895,31 +773,23 @@ mod tests {
|
|||
name: "read_file".to_string(),
|
||||
input: r#"{"path":"src/main.rs"}"#.to_string(),
|
||||
}]),
|
||||
<<<<<<< HEAD
|
||||
ConversationMessage::tool_result(
|
||||
"1",
|
||||
"read_file",
|
||||
r#"{"path":"src/main.rs","content":"fn main() { buggy }"}"#,
|
||||
false,
|
||||
),
|
||||
=======
|
||||
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"fn main() { buggy }"}"#, false),
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
ConversationMessage::assistant(vec![ContentBlock::ToolUse {
|
||||
id: "2".to_string(),
|
||||
name: "edit_file".to_string(),
|
||||
input: r#"{"path":"src/main.rs","old":"buggy","new":"fixed"}"#.to_string(),
|
||||
}]),
|
||||
<<<<<<< HEAD
|
||||
ConversationMessage::tool_result(
|
||||
"2",
|
||||
"edit_file",
|
||||
r#"{"path":"src/main.rs","ok":true}"#,
|
||||
false,
|
||||
),
|
||||
=======
|
||||
ConversationMessage::tool_result("2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false),
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
ConversationMessage::assistant(vec![ContentBlock::Text {
|
||||
text: "Fixed the bug in main.rs".to_string(),
|
||||
}]),
|
||||
|
|
@ -935,14 +805,10 @@ mod tests {
|
|||
&trident_config,
|
||||
);
|
||||
|
||||
<<<<<<< HEAD
|
||||
assert!(
|
||||
result.removed_message_count > 0
|
||||
|| result.compacted_session.messages.len() < session.messages.len()
|
||||
);
|
||||
=======
|
||||
assert!(result.removed_message_count > 0 || result.compacted_session.messages.len() < session.messages.len());
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
)]
|
||||
mod init;
|
||||
mod input;
|
||||
mod tui;
|
||||
mod render;
|
||||
mod setup_wizard;
|
||||
|
||||
|
|
@ -6926,30 +6925,10 @@ fn run_resume_command(
|
|||
| SlashCommand::Ide { .. }
|
||||
| SlashCommand::Tag { .. }
|
||||
| SlashCommand::OutputStyle { .. }
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
| SlashCommand::AddDir { .. }
|
||||
<<<<<<< HEAD
|
||||
| SlashCommand::Team { .. }
|
||||
=======
|
||||
| SlashCommand::Lsp { .. }
|
||||
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
| SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::Lsp { .. }
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
| SlashCommand::Setup => Err("unsupported resumed slash command".into()),
|
||||
=======
|
||||
| SlashCommand::AddDir { .. }
|
||||
| SlashCommand::Lsp { .. } => Err("unsupported resumed slash command".into()),
|
||||
>>>>>>> 0b227b62 (fix: resolve cherry-pick conflicts and remove non-LSP artifacts)
|
||||
}
|
||||
=======
|
||||
| SlashCommand::Team { .. }
|
||||
| SlashCommand::Setup => Err("unsupported resumed slash command".into()), }
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
}
|
||||
|
||||
/// Detect if the current working directory is "broad" (home directory or
|
||||
|
|
@ -7073,349 +7052,17 @@ fn run_repl(
|
|||
base_commit: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
allow_broad_cwd: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if io::stdout().is_terminal() {
|
||||
return run_repl_tui(model, allowed_tools, permission_mode, base_commit, reasoning_effort, allow_broad_cwd);
|
||||
}
|
||||
run_repl_classic(model, allowed_tools, permission_mode, base_commit, reasoning_effort, allow_broad_cwd)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn run_repl_tui(
|
||||
model: String,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
permission_mode: PermissionMode,
|
||||
base_commit: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
allow_broad_cwd: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
enforce_broad_cwd_policy(allow_broad_cwd, CliOutputFormat::Text)?;
|
||||
run_stale_base_preflight(base_commit.as_deref());
|
||||
let resolved_model = resolve_repl_model(model.clone());
|
||||
let reasoning_effort_clone = reasoning_effort.clone();
|
||||
|
||||
let mut cli = LiveCli::new(resolved_model.clone(), true, allowed_tools.clone(), permission_mode)?;
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let lsp_auto = runtime::ConfigLoader::default_for(&cwd)
|
||||
.load()
|
||||
.map(|c| c.lsp_auto_start())
|
||||
.unwrap_or(true);
|
||||
cli.lsp_auto_start = lsp_auto;
|
||||
cli.set_reasoning_effort(reasoning_effort_clone);
|
||||
|
||||
// Build shared dashboard state
|
||||
let dashboard_state = tui::SharedDashboardState::new(std::sync::RwLock::new({
|
||||
let mut ds = tui::DashboardState::new();
|
||||
ds.model = cli.model.clone();
|
||||
ds.permission_mode = permission_mode.as_str().to_string();
|
||||
ds.session_id = Some(cli.session.id.clone());
|
||||
if let Ok(config) = runtime::ConfigLoader::default_for(&cwd).load() {
|
||||
if let Some(base_url) = config.provider().base_url() {
|
||||
ds.provider_url = base_url.to_string();
|
||||
}
|
||||
}
|
||||
ds
|
||||
}));
|
||||
|
||||
// Discover and register LSP servers (before TUI takes over the terminal)
|
||||
let lsp_servers = runtime::lsp_discovery::discover_available_servers();
|
||||
if !lsp_servers.is_empty() {
|
||||
for server in &lsp_servers {
|
||||
tools::global_lsp_registry().register_with_descriptor(
|
||||
&server.language,
|
||||
runtime::lsp_client::LspServerStatus::Starting,
|
||||
None,
|
||||
vec![],
|
||||
server.clone(),
|
||||
);
|
||||
}
|
||||
if cli.lsp_auto_start {
|
||||
let registry = tools::global_lsp_registry();
|
||||
for server in &lsp_servers {
|
||||
let _ = registry.start_server(&server.language);
|
||||
}
|
||||
}
|
||||
}
|
||||
update_dashboard_lsp(&dashboard_state);
|
||||
|
||||
// Initialize TUI
|
||||
let mut app = match tui::TuiApp::init(dashboard_state.clone()) {
|
||||
Ok(app) => app,
|
||||
Err(e) => {
|
||||
eprintln!("TUI init failed, falling back to classic mode: {e}");
|
||||
return run_repl_classic(model, allowed_tools, permission_mode, base_commit, reasoning_effort, allow_broad_cwd);
|
||||
}
|
||||
};
|
||||
// TUI banner — styled ASCII art with info lines
|
||||
{
|
||||
let status = status_context(None).ok();
|
||||
let git_branch = status
|
||||
.as_ref()
|
||||
.and_then(|ctx| ctx.git_branch.as_deref())
|
||||
.unwrap_or("unknown");
|
||||
let workspace = status.as_ref().map_or_else(
|
||||
|| "unknown".to_string(),
|
||||
|ctx| ctx.git_summary.headline(),
|
||||
);
|
||||
let banner_lines = vec![
|
||||
tui::BannerLine { text: " ██████╗██╗ █████╗ ██╗ ██╗".to_string(), color: ratatui::style::Color::Red },
|
||||
tui::BannerLine { text: "██╔════╝██║ ██╔══██╗██║ ██║".to_string(), color: ratatui::style::Color::Red },
|
||||
tui::BannerLine { text: "██║ ██║ ███████║██║ █╗ ██║".to_string(), color: ratatui::style::Color::Red },
|
||||
tui::BannerLine { text: "██║ ██║ ██╔══██║██║███╗██║".to_string(), color: ratatui::style::Color::Red },
|
||||
tui::BannerLine { text: "╚██████╗███████╗██║ ██║╚███╔███╔╝".to_string(), color: ratatui::style::Color::Red },
|
||||
tui::BannerLine { text: " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ Code".to_string(), color: ratatui::style::Color::Red },
|
||||
tui::BannerLine { text: String::new(), color: ratatui::style::Color::White },
|
||||
tui::BannerLine { text: format!(" Model {}", cli.model), color: ratatui::style::Color::White },
|
||||
tui::BannerLine { text: format!(" Mode {}", permission_mode.as_str()), color: ratatui::style::Color::White },
|
||||
tui::BannerLine { text: format!(" Branch {}", git_branch), color: ratatui::style::Color::Green },
|
||||
tui::BannerLine { text: format!(" Workspace {}", workspace), color: ratatui::style::Color::White },
|
||||
tui::BannerLine { text: format!(" Session {}", cli.session.id), color: ratatui::style::Color::Gray },
|
||||
tui::BannerLine { text: String::new(), color: ratatui::style::Color::White },
|
||||
];
|
||||
app.push_banner(banner_lines);
|
||||
}
|
||||
|
||||
// Main TUI event loop
|
||||
loop {
|
||||
app.set_slash_completions(cli.repl_completion_candidates().unwrap_or_default());
|
||||
match app.read_line()? {
|
||||
tui::TuiReadOutcome::Pending => continue,
|
||||
tui::TuiReadOutcome::Submit(input) => {
|
||||
let trimmed = input.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if matches!(trimmed.as_str(), "/exit" | "/quit") {
|
||||
let _ = app.restore_terminal();
|
||||
cli.shutdown_lsp_servers();
|
||||
cli.persist_session()?;
|
||||
break;
|
||||
}
|
||||
match SlashCommand::parse(&trimmed) {
|
||||
Ok(Some(command)) => {
|
||||
let cmd_result = cli.handle_repl_command(command);
|
||||
if let Ok(true) = cmd_result {
|
||||
cli.persist_session()?;
|
||||
}
|
||||
if let Err(e) = cmd_result {
|
||||
app.push_system_message(&format!("Command error: {e}"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
app.push_system_message(&error.to_string());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let cwd_check = std::env::current_dir().unwrap_or_default();
|
||||
let prompt = if let Some(p) = try_resolve_bare_skill_prompt(&cwd_check, &trimmed) {
|
||||
p
|
||||
} else {
|
||||
trimmed.clone()
|
||||
};
|
||||
app.push_user_input(&input);
|
||||
cli.record_prompt_history(&trimmed);
|
||||
update_dashboard(&dashboard_state, &cli);
|
||||
app.set_status("Thinking...");
|
||||
|
||||
// Run turn in-place. Output goes to the alternate screen buffer
|
||||
// which ratatui owns - it will be overwritten on next redraw.
|
||||
// This avoids the fragile suspend/resume pattern.
|
||||
let result = cli.run_turn(&prompt);
|
||||
|
||||
// Read the last assistant message from the session for the conversation pane
|
||||
{
|
||||
let messages = &cli.runtime.session().messages;
|
||||
if let Some(msg) = messages.last() {
|
||||
if msg.role == runtime::MessageRole::Assistant {
|
||||
for block in &msg.blocks {
|
||||
if let runtime::ContentBlock::Text { text } = block {
|
||||
app.push_output(text, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
app.set_status("Done");
|
||||
if let Ok(mut ds) = dashboard_state.write() {
|
||||
ds.status_message.clear();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
app.push_system_message(&format!("Error: {e}"));
|
||||
app.set_status("");
|
||||
}
|
||||
}
|
||||
update_dashboard(&dashboard_state, &cli);
|
||||
}
|
||||
tui::TuiReadOutcome::ProviderSwap => {
|
||||
// Provider swap wizard needs interactive terminal
|
||||
let _ = app.restore_terminal();
|
||||
println!();
|
||||
setup_wizard::run_setup_wizard()?;
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let config = runtime::ConfigLoader::default_for(&cwd).load().ok();
|
||||
if let Some(new_model) = config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) {
|
||||
let _ = cli.set_model(Some(new_model));
|
||||
}
|
||||
app.push_system_message("Provider updated - restart for full effect");
|
||||
update_dashboard(&dashboard_state, &cli);
|
||||
}
|
||||
tui::TuiReadOutcome::TeamToggle => {
|
||||
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
|
||||
if current == "1" {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
|
||||
app.push_system_message("[team] Agent teams disabled");
|
||||
} else {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
|
||||
app.push_system_message("[team] Agent teams enabled");
|
||||
}
|
||||
}
|
||||
tui::TuiReadOutcome::Cancel => {
|
||||
// Ctrl+C clears input (handled in TUI), just continue
|
||||
}
|
||||
tui::TuiReadOutcome::Exit => {
|
||||
let _ = app.restore_terminal();
|
||||
cli.shutdown_lsp_servers();
|
||||
cli.persist_session()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_dashboard(state: &tui::SharedDashboardState, cli: &LiveCli) {
|
||||
if let Ok(mut ds) = state.write() {
|
||||
ds.model = cli.model.clone();
|
||||
ds.turn_count = cli.runtime.usage().turns();
|
||||
let usage = cli.runtime.usage().cumulative_usage();
|
||||
ds.input_tokens = usage.input_tokens;
|
||||
ds.output_tokens = usage.output_tokens;
|
||||
ds.cache_read_tokens = usage.cache_read_input_tokens;
|
||||
ds.cache_creation_tokens = usage.cache_creation_input_tokens;
|
||||
ds.cost_usd = usage.estimate_cost_usd().total_cost_usd();
|
||||
ds.session_id = Some(cli.session.id.clone());
|
||||
}
|
||||
update_dashboard_lsp(state);
|
||||
}
|
||||
|
||||
fn update_dashboard_lsp(state: &tui::SharedDashboardState) {
|
||||
if let Ok(mut ds) = state.write() {
|
||||
ds.lsp_servers = tools::global_lsp_registry()
|
||||
.list_servers()
|
||||
.into_iter()
|
||||
.map(|s| tui::LspInfo {
|
||||
language: s.language,
|
||||
status: s.status.to_string(),
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn run_repl_classic(
|
||||
model: String,
|
||||
allowed_tools: Option<AllowedToolSet>,
|
||||
permission_mode: PermissionMode,
|
||||
base_commit: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
allow_broad_cwd: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
enforce_broad_cwd_policy(allow_broad_cwd, CliOutputFormat::Text)?;
|
||||
run_stale_base_preflight(base_commit.as_deref());
|
||||
let resolved_model = resolve_repl_model(model)?;
|
||||
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
||||
|
||||
// Read config for LSP auto-start setting
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let lsp_auto = runtime::ConfigLoader::default_for(&cwd)
|
||||
.load()
|
||||
.map(|c| c.lsp_auto_start())
|
||||
.unwrap_or(true);
|
||||
cli.lsp_auto_start = lsp_auto;
|
||||
cli.set_reasoning_effort(reasoning_effort);
|
||||
let mut editor =
|
||||
input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default());
|
||||
println!("{}", cli.startup_banner());
|
||||
println!("{}", format_connected_line(&cli.model));
|
||||
|
||||
// Validate key config fields and prompt setup wizard if missing
|
||||
{
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
if let Ok(config) = runtime::ConfigLoader::default_for(&cwd).load() {
|
||||
let mut missing: Vec<&str> = Vec::new();
|
||||
if config.provider().api_key().is_none() {
|
||||
missing.push("provider.apiKey");
|
||||
}
|
||||
if config.provider().base_url().is_none() {
|
||||
missing.push("provider.baseUrl");
|
||||
}
|
||||
if config.subagent_model().is_none() {
|
||||
missing.push("subagentModel");
|
||||
}
|
||||
if !missing.is_empty() {
|
||||
eprintln!("
|
||||
[33mWarning: Missing config fields:[0m {}", missing.join(", "));
|
||||
eprintln!(" [2mRun [1mclaw setup[0m[2m or type [1m/setup[0m[2m to configure.[0m
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Discover and register LSP servers
|
||||
let lsp_servers = runtime::lsp_discovery::discover_available_servers();
|
||||
if !lsp_servers.is_empty() {
|
||||
eprintln!("Loading LSP servers...");
|
||||
for server in &lsp_servers {
|
||||
tools::global_lsp_registry().register_with_descriptor(
|
||||
&server.language,
|
||||
runtime::lsp_client::LspServerStatus::Starting,
|
||||
None,
|
||||
vec![],
|
||||
server.clone(),
|
||||
);
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
// Auto-start all discovered servers if enabled
|
||||
if cli.lsp_auto_start {
|
||||
let registry = tools::global_lsp_registry();
|
||||
for server in &lsp_servers {
|
||||
match registry.start_server(&server.language) {
|
||||
Ok(()) => eprintln!(" ✓ {} ({})", server.language, server.command),
|
||||
Err(e) => eprintln!(" ✗ {} — {e}", server.language),
|
||||
}
|
||||
}
|
||||
eprintln!(" Disable with: /lsp toggle or set lspAutoStart=false in settings.json");
|
||||
} else {
|
||||
let names: Vec<&str> = lsp_servers.iter().map(|s| s.language.as_str()).collect();
|
||||
eprintln!(" Available but not started: {}", names.join(", "));
|
||||
eprintln!(" Start with: /lsp start <language> or set lspAutoStart=true in settings.json");
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
}
|
||||
|
||||
// Show install suggestions for missing LSP servers
|
||||
{
|
||||
let availability = runtime::lsp_discovery::check_lsp_availability();
|
||||
let prompt = runtime::lsp_discovery::format_install_prompt(&availability);
|
||||
if !prompt.is_empty() {
|
||||
eprintln!("{prompt}");
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
editor.set_completions(cli.repl_completion_candidates().unwrap_or_default());
|
||||
match editor.read_line()? {
|
||||
|
|
@ -7425,7 +7072,6 @@ fn run_repl_classic(
|
|||
continue;
|
||||
}
|
||||
if matches!(trimmed.as_str(), "/exit" | "/quit") {
|
||||
cli.shutdown_lsp_servers();
|
||||
cli.persist_session()?;
|
||||
break;
|
||||
}
|
||||
|
|
@ -7456,33 +7102,31 @@ fn run_repl_classic(
|
|||
cli.record_prompt_history(&trimmed);
|
||||
cli.run_turn(&trimmed)?;
|
||||
}
|
||||
input::ReadOutcome::Cancel => {}
|
||||
input::ReadOutcome::Exit => {
|
||||
cli.persist_session()?;
|
||||
break;
|
||||
}
|
||||
input::ReadOutcome::ProviderSwap => {
|
||||
// Ctrl+P triggered — launch setup wizard and hot-swap model
|
||||
setup_wizard::run_setup_wizard()?;
|
||||
let _ = setup_wizard::run_setup_wizard();
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let config = runtime::ConfigLoader::default_for(&cwd).load().ok();
|
||||
if let Some(new_model) = config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) {
|
||||
cli.set_model(Some(new_model))?;
|
||||
if let Some(new_model) =
|
||||
config.as_ref().and_then(|c| c.provider().model().map(str::to_string))
|
||||
{
|
||||
let _ = cli.set_model(Some(new_model));
|
||||
}
|
||||
println!("{}", format_connected_line(&cli.model));
|
||||
}
|
||||
input::ReadOutcome::TeamToggle => {
|
||||
// Ctrl+T toggles agent teams mode
|
||||
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
|
||||
if current == "1" {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
|
||||
eprintln!("[team] Agent teams disabled");
|
||||
} else {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
|
||||
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
|
||||
eprintln!("[team] Agent teams enabled");
|
||||
}
|
||||
}
|
||||
input::ReadOutcome::Cancel => {}
|
||||
input::ReadOutcome::Exit => {
|
||||
cli.shutdown_lsp_servers();
|
||||
cli.persist_session()?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -7516,7 +7160,6 @@ struct LiveCli {
|
|||
runtime: BuiltRuntime,
|
||||
session: SessionHandle,
|
||||
prompt_history: Vec<PromptHistoryEntry>,
|
||||
lsp_auto_start: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -8031,7 +7674,6 @@ impl LiveCli {
|
|||
runtime,
|
||||
session,
|
||||
prompt_history: Vec::new(),
|
||||
lsp_auto_start: true,
|
||||
};
|
||||
cli.persist_session()?;
|
||||
Ok(cli)
|
||||
|
|
@ -8188,8 +7830,6 @@ impl LiveCli {
|
|||
// ============================================================================
|
||||
|
||||
let error_str = error.to_string();
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
// Detect context window overflow. Some providers (e.g. OpenAI-compat backends)
|
||||
// return 400 with "no parseable body" instead of a proper context_length_exceeded
|
||||
// error when the request is too large to even parse — treat that as context overflow too.
|
||||
|
|
@ -8252,57 +7892,11 @@ impl LiveCli {
|
|||
);
|
||||
let removed = result.removed_message_count;
|
||||
|
||||
if removed == 0 && round > 0 {
|
||||
// No more messages to compact — further rounds won't help
|
||||
println!(" No further compaction possible.");
|
||||
break;
|
||||
=======
|
||||
let is_context_window = error_str.contains("context_window") || error_str.contains("Context window");
|
||||
=======
|
||||
let is_context_window = error_str.contains("context_window")
|
||||
|| error_str.contains("Context window")
|
||||
|| error_str.contains("no parseable body");
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
|
||||
if is_context_window {
|
||||
// Progressive auto-compact retry loop:
|
||||
// Each round compacts more aggressively (fewer preserved messages)
|
||||
// until the request fits in the model's context window.
|
||||
// Max 4 rounds of compaction before giving up.
|
||||
let max_compact_rounds = 4;
|
||||
let preserve_schedule = [4, 2, 1, 0];
|
||||
|
||||
for round in 0..max_compact_rounds {
|
||||
let preserve = preserve_schedule[round];
|
||||
println!(
|
||||
" Auto-compacting session (round {}/{}, preserving {} recent messages)...",
|
||||
round + 1,
|
||||
max_compact_rounds,
|
||||
preserve
|
||||
);
|
||||
|
||||
// Run Trident pipeline then summary-based compaction
|
||||
let result = runtime::trident::trident_compact_session(
|
||||
runtime.session(),
|
||||
CompactionConfig {
|
||||
preserve_recent_messages: preserve,
|
||||
max_estimated_tokens: 0,
|
||||
},
|
||||
&runtime::trident::TridentConfig::default(),
|
||||
);
|
||||
let removed = result.removed_message_count;
|
||||
|
||||
if removed == 0 && round > 0 {
|
||||
// No more messages to compact — further rounds won't help
|
||||
println!(" No further compaction possible.");
|
||||
break;
|
||||
}
|
||||
|
||||
if removed > 0 {
|
||||
println!("{}", format_compact_report(removed, result.compacted_session.messages.len(), false));
|
||||
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
|
||||
if removed > 0 {
|
||||
println!(
|
||||
|
|
@ -8325,31 +7919,16 @@ impl LiveCli {
|
|||
self.prepare_turn_runtime(true)?;
|
||||
drop(hook_abort_monitor);
|
||||
|
||||
=======
|
||||
|
||||
// Replace self.runtime's session with the compacted version
|
||||
// so prepare_turn_runtime builds from the compacted session
|
||||
*self.runtime.session_mut() = result.compacted_session.clone();
|
||||
|
||||
// Build a new runtime with the compacted session and retry
|
||||
let (mut new_runtime, hook_abort_monitor) = self.prepare_turn_runtime(true)?;
|
||||
drop(hook_abort_monitor);
|
||||
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
let mut rp = CliPermissionPrompter::new(self.permission_mode);
|
||||
match new_runtime.run_turn(input, Some(&mut rp)) {
|
||||
Ok(summary) => {
|
||||
self.replace_runtime(new_runtime)?;
|
||||
spinner.finish(
|
||||
<<<<<<< HEAD
|
||||
if round == 0 {
|
||||
"✨ Done (after auto-compact)"
|
||||
} else {
|
||||
"✨ Done (after aggressive auto-compact)"
|
||||
},
|
||||
=======
|
||||
if round == 0 { "✨ Done (after auto-compact)" } else { "✨ Done (after aggressive auto-compact)" },
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
TerminalRenderer::new().color_theme(),
|
||||
&mut stdout,
|
||||
)?;
|
||||
|
|
@ -8367,7 +7946,6 @@ impl LiveCli {
|
|||
let retry_str = retry_error.to_string();
|
||||
let still_context_window = retry_str.contains("context_window")
|
||||
|| retry_str.contains("Context window")
|
||||
<<<<<<< HEAD
|
||||
|| retry_str.contains("no parseable body")
|
||||
|| retry_str.contains("exceed_context_size")
|
||||
|| retry_str.contains("exceeds the available context size")
|
||||
|
|
@ -8394,21 +7972,11 @@ impl LiveCli {
|
|||
// The compacted session was still too large for the model's context.
|
||||
// Shut down the old runtime, adopt the partially-compacted one,
|
||||
// and loop — the next round will compact more aggressively.
|
||||
=======
|
||||
|| retry_str.contains("no parseable body");
|
||||
|
||||
if still_context_window && round + 1 < max_compact_rounds {
|
||||
// Still too large — compact more aggressively next round
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
runtime.shutdown_plugins()?;
|
||||
runtime = new_runtime;
|
||||
continue;
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
|
||||
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
|
||||
// Not a context window error, or out of rounds
|
||||
return Err(Box::new(retry_error));
|
||||
}
|
||||
|
|
@ -8592,52 +8160,6 @@ impl LiveCli {
|
|||
run_init(CliOutputFormat::Text)?;
|
||||
false
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
SlashCommand::Team { action } => {
|
||||
match action.as_deref().unwrap_or("") {
|
||||
"on" | "enable" => {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
|
||||
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
|
||||
}
|
||||
"off" | "disable" => {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
|
||||
eprintln!("[team] Agent teams disabled");
|
||||
}
|
||||
"status" => {
|
||||
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
|
||||
if current == "1" {
|
||||
eprintln!("[team] Agent teams: ENABLED");
|
||||
} else {
|
||||
eprintln!("[team] Agent teams: DISABLED (use /team on or Ctrl+T to enable)");
|
||||
}
|
||||
}
|
||||
"" => {
|
||||
// Toggle
|
||||
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
|
||||
if current == "1" {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
|
||||
eprintln!("[team] Agent teams disabled");
|
||||
} else {
|
||||
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
|
||||
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
|
||||
}
|
||||
}
|
||||
other => eprintln!("[team] unknown action: {other}. Use: /team [on|off|status]"),
|
||||
}
|
||||
false
|
||||
}
|
||||
SlashCommand::Setup => {
|
||||
setup_wizard::run_setup_wizard()?;
|
||||
// Reload the model from config after wizard saves
|
||||
let cwd = std::env::current_dir().unwrap_or_default();
|
||||
let config = runtime::ConfigLoader::default_for(&cwd).load().ok();
|
||||
if let Some(new_model) = config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) {
|
||||
self.set_model(Some(new_model))?;
|
||||
}
|
||||
false
|
||||
}
|
||||
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
|
||||
SlashCommand::Diff => {
|
||||
Self::print_diff()?;
|
||||
false
|
||||
|
|
@ -8744,10 +8266,6 @@ impl LiveCli {
|
|||
eprintln!("{cmd_name} is not yet implemented in this build.");
|
||||
false
|
||||
}
|
||||
SlashCommand::Lsp { action, target } => {
|
||||
self.handle_lsp_command(action.as_deref(), target.as_deref());
|
||||
false
|
||||
}
|
||||
SlashCommand::Unknown(name) => {
|
||||
eprintln!("{}", format_unknown_slash_command(&name));
|
||||
false
|
||||
|
|
@ -8755,79 +8273,6 @@ impl LiveCli {
|
|||
})
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
fn handle_lsp_command(&mut self, action: Option<&str>, target: Option<&str>) {
|
||||
=======
|
||||
fn handle_lsp_command(&self, action: Option<&str>, target: Option<&str>) {
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
fn handle_lsp_command(&mut self, action: Option<&str>, target: Option<&str>) {
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
let registry = tools::global_lsp_registry();
|
||||
match action {
|
||||
Some("start") => {
|
||||
let lang = target.unwrap_or("unknown");
|
||||
match registry.start_server(lang) {
|
||||
Ok(()) => eprintln!("LSP server '{lang}' started."),
|
||||
Err(e) => eprintln!("Failed to start LSP server '{lang}': {e}"),
|
||||
}
|
||||
}
|
||||
Some("stop") => {
|
||||
let lang = target.unwrap_or("unknown");
|
||||
match registry.stop_server(lang) {
|
||||
Ok(()) => eprintln!("LSP server '{lang}' stopped."),
|
||||
Err(e) => eprintln!("Failed to stop LSP server '{lang}': {e}"),
|
||||
}
|
||||
}
|
||||
Some("restart") => {
|
||||
let lang = target.unwrap_or("unknown");
|
||||
let _ = registry.stop_server(lang);
|
||||
match registry.start_server(lang) {
|
||||
Ok(()) => eprintln!("LSP server '{lang}' restarted."),
|
||||
Err(e) => eprintln!("Failed to restart LSP server '{lang}': {e}"),
|
||||
}
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
Some("toggle") => {
|
||||
self.lsp_auto_start = !self.lsp_auto_start;
|
||||
let state = if self.lsp_auto_start { "on" } else { "off" };
|
||||
eprintln!("LSP auto-start: {state}");
|
||||
}
|
||||
_ => {
|
||||
let servers = registry.list_servers();
|
||||
let auto_state = if self.lsp_auto_start { "on" } else { "off" };
|
||||
eprintln!("LSP auto-start: {auto_state}");
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
_ => {
|
||||
let servers = registry.list_servers();
|
||||
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
|
||||
=======
|
||||
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
|
||||
if servers.is_empty() {
|
||||
eprintln!("No LSP servers registered.");
|
||||
} else {
|
||||
for s in &servers {
|
||||
eprintln!(" {} [{}]", s.language, s.status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown_lsp_servers(&self) {
|
||||
let registry = tools::global_lsp_registry();
|
||||
for server in registry.list_servers() {
|
||||
if server.status == runtime::lsp_client::LspServerStatus::Connected {
|
||||
let _ = registry.stop_server(&server.language);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_session(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
self.runtime.session().save_to_path(&self.session.path)?;
|
||||
Ok(())
|
||||
|
|
@ -14557,153 +14002,6 @@ impl ToolExecutor for CliToolExecutor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_batch(&mut self, calls: Vec<runtime::ToolCall>) -> Vec<runtime::ToolResult> {
|
||||
if calls.len() <= 1 {
|
||||
return calls
|
||||
.into_iter()
|
||||
.map(|call| {
|
||||
let result = self.execute(&call.tool_name, &call.input);
|
||||
runtime::ToolResult {
|
||||
tool_use_id: call.tool_use_id,
|
||||
tool_name: call.tool_name,
|
||||
result,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
/// Tools that are safe to run in parallel because they only read
|
||||
/// state and dispatch through the stateless tool registry.
|
||||
const PARALLEL_SAFE_TOOLS: &[&str] = &[
|
||||
"read_file",
|
||||
"glob_search",
|
||||
"grep_search",
|
||||
"WebFetch",
|
||||
"WebSearch",
|
||||
"ToolSearch",
|
||||
"Skill",
|
||||
"LSP",
|
||||
"Agent",
|
||||
"AgentMessage",
|
||||
"TeamStatus",
|
||||
"TaskClaim",
|
||||
"AgentSuggestion",
|
||||
"ContextRequest", "TaskGet",
|
||||
"TaskList",
|
||||
"TaskOutput",
|
||||
"GitStatus",
|
||||
"GitDiff",
|
||||
"GitLog",
|
||||
"GitShow",
|
||||
"GitBlame",
|
||||
];
|
||||
|
||||
let emit_output = self.emit_output;
|
||||
let mut results: Vec<Option<runtime::ToolResult>> = vec![None; calls.len()];
|
||||
let mut parallel_calls: Vec<(usize, String, String, String)> = Vec::new();
|
||||
let mut sequential_indices: Vec<usize> = Vec::new();
|
||||
|
||||
// Classify calls as parallel-safe or sequential
|
||||
for (i, call) in calls.iter().enumerate() {
|
||||
if self
|
||||
.allowed_tools
|
||||
.as_ref()
|
||||
.is_some_and(|allowed| !allowed.contains(&call.tool_name))
|
||||
{
|
||||
results[i] = Some(runtime::ToolResult {
|
||||
tool_use_id: call.tool_use_id.clone(),
|
||||
tool_name: call.tool_name.clone(),
|
||||
result: Err(ToolError::new(format!(
|
||||
"tool `{}` is not enabled by the current --allowedTools setting",
|
||||
call.tool_name
|
||||
))),
|
||||
});
|
||||
} else if PARALLEL_SAFE_TOOLS.contains(&call.tool_name.as_str())
|
||||
&& !self.tool_registry.has_runtime_tool(&call.tool_name)
|
||||
{
|
||||
parallel_calls.push((
|
||||
i,
|
||||
call.tool_use_id.clone(),
|
||||
call.tool_name.clone(),
|
||||
call.input.clone(),
|
||||
));
|
||||
} else {
|
||||
sequential_indices.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute parallel-safe tools concurrently
|
||||
if !parallel_calls.is_empty() {
|
||||
let registry = self.tool_registry.clone();
|
||||
let parallel_results: Vec<(usize, String, String, Result<String, ToolError>)> =
|
||||
std::thread::scope(|s| {
|
||||
let mut handles = Vec::new();
|
||||
for (idx, tool_use_id, tool_name, input) in ¶llel_calls {
|
||||
let registry = ®istry;
|
||||
let tool_use_id = tool_use_id.clone();
|
||||
let tool_name = tool_name.clone();
|
||||
let input = input.clone();
|
||||
let idx = *idx;
|
||||
handles.push(s.spawn(move || {
|
||||
let value = serde_json::from_str(&input)
|
||||
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")));
|
||||
let result = match value {
|
||||
Ok(v) => registry
|
||||
.execute(&tool_name, &v)
|
||||
.map_err(ToolError::new),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
(idx, tool_use_id, tool_name, result)
|
||||
}));
|
||||
}
|
||||
handles
|
||||
.into_iter()
|
||||
.map(|h| h.join().unwrap_or_else(|_| {
|
||||
(
|
||||
0,
|
||||
String::new(),
|
||||
String::new(),
|
||||
Err(ToolError::new("parallel thread panicked")),
|
||||
)
|
||||
}))
|
||||
.collect()
|
||||
});
|
||||
|
||||
for (idx, tool_use_id, tool_name, result) in parallel_results {
|
||||
if emit_output {
|
||||
let output_str = match &result {
|
||||
Ok(o) => o.clone(),
|
||||
Err(e) => e.to_string(),
|
||||
};
|
||||
let is_error = result.is_err();
|
||||
let markdown = format_tool_result(&tool_name, &output_str, is_error);
|
||||
self.renderer
|
||||
.stream_markdown(&markdown, &mut io::stdout())
|
||||
.map_err(|error| ToolError::new(error.to_string()))
|
||||
.ok();
|
||||
}
|
||||
results[idx] = Some(runtime::ToolResult {
|
||||
tool_use_id,
|
||||
tool_name,
|
||||
result,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Execute sequential tools one at a time
|
||||
for idx in sequential_indices {
|
||||
let call = &calls[idx];
|
||||
let result = self.execute(&call.tool_name, &call.input);
|
||||
results[idx] = Some(runtime::ToolResult {
|
||||
tool_use_id: call.tool_use_id.clone(),
|
||||
tool_name: call.tool_name.clone(),
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
results.into_iter().map(|r| r.unwrap()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn permission_policy(
|
||||
|
|
@ -15230,11 +14528,7 @@ mod tests {
|
|||
retryable: false,
|
||||
suggested_action: None,
|
||||
retry_after: None,
|
||||
<<<<<<< HEAD
|
||||
}),
|
||||
=======
|
||||
}),
|
||||
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
|
||||
};
|
||||
|
||||
let rendered = format_user_visible_api_error("session-issue-32", &error);
|
||||
|
|
|
|||
|
|
@ -233,11 +233,7 @@ fn prompt_fast_model(
|
|||
main_model: Option<&str>,
|
||||
) -> Result<Option<String>, Box<dyn std::error::Error>> {
|
||||
println!();
|
||||
<<<<<<< HEAD
|
||||
println!(" \x1b[1mFast Model (for Agent subtasks)\x1b[0m");
|
||||
=======
|
||||
println!(" [1mFast Model (for Agent subtasks)[0m");
|
||||
>>>>>>> 6b0af2bc (refactor: remove SubAgent tool, make Agent use subagentModel config)
|
||||
println!(" A smaller/cheaper model used by the Agent tool when spawning");
|
||||
println!(" Explore, Plan, or Verification sub-agents. This saves tokens");
|
||||
println!(" by using a fast model for information-gathering tasks.");
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ use std::io::Write;
|
|||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, Clear, ClearType,
|
||||
};
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType};
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
||||
use ratatui::text::{Line, Span};
|
||||
|
|
@ -66,7 +64,9 @@ pub struct AgentInfo {
|
|||
}
|
||||
|
||||
impl Default for DashboardState {
|
||||
fn default() -> Self { Self::new() }
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl DashboardState {
|
||||
|
|
@ -80,7 +80,9 @@ impl DashboardState {
|
|||
.and_then(|o| {
|
||||
if o.status.success() {
|
||||
Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||
} else { None }
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
|
|
@ -171,7 +173,10 @@ impl TuiApp {
|
|||
);
|
||||
input.set_style(Style::default().fg(Color::White));
|
||||
input.set_cursor_style(
|
||||
Style::default().fg(Color::Black).bg(Color::Cyan).add_modifier(Modifier::BOLD)
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
|
||||
let mut me = Self {
|
||||
|
|
@ -259,25 +264,31 @@ impl TuiApp {
|
|||
}
|
||||
|
||||
pub fn push_user_input(&mut self, text: &str) {
|
||||
self.conversation.push(ConversationLine {
|
||||
text: text.to_string(),
|
||||
color: Color::Cyan,
|
||||
bold: true,
|
||||
});
|
||||
for raw_line in text.lines() {
|
||||
self.conversation.push(ConversationLine {
|
||||
text: raw_line.to_string(),
|
||||
color: Color::Cyan,
|
||||
bold: true,
|
||||
});
|
||||
}
|
||||
self.auto_scroll();
|
||||
}
|
||||
|
||||
pub fn push_system_message(&mut self, text: &str) {
|
||||
self.conversation.push(ConversationLine {
|
||||
text: text.to_string(),
|
||||
color: Color::Yellow,
|
||||
bold: false,
|
||||
});
|
||||
for raw_line in text.lines() {
|
||||
self.conversation.push(ConversationLine {
|
||||
text: raw_line.to_string(),
|
||||
color: Color::Yellow,
|
||||
bold: false,
|
||||
});
|
||||
}
|
||||
self.auto_scroll();
|
||||
}
|
||||
|
||||
pub fn push_output(&mut self, text: &str, is_error: bool) {
|
||||
if text.is_empty() { return; }
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
for raw_line in text.lines() {
|
||||
self.conversation.push(ConversationLine {
|
||||
text: raw_line.to_string(),
|
||||
|
|
@ -388,11 +399,19 @@ impl TuiApp {
|
|||
let text = lines.join("\n");
|
||||
self.input.select_all();
|
||||
self.input.cut();
|
||||
if text.trim().is_empty() { return Ok(TuiReadOutcome::Pending); }
|
||||
if text.trim().is_empty() {
|
||||
return Ok(TuiReadOutcome::Pending);
|
||||
}
|
||||
Ok(TuiReadOutcome::Submit(text))
|
||||
}
|
||||
KeyCode::Tab => { self.handle_tab(); Ok(TuiReadOutcome::Pending) }
|
||||
KeyCode::Esc => { self.showing_completions = false; Ok(TuiReadOutcome::Cancel) }
|
||||
KeyCode::Tab => {
|
||||
self.handle_tab();
|
||||
Ok(TuiReadOutcome::Pending)
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
self.showing_completions = false;
|
||||
Ok(TuiReadOutcome::Cancel)
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
self.conversation_scroll = self.conversation_scroll.saturating_add(5);
|
||||
Ok(TuiReadOutcome::Pending)
|
||||
|
|
@ -414,13 +433,17 @@ impl TuiApp {
|
|||
let current_text: String = self.input.lines().join("");
|
||||
if current_text.starts_with('/') {
|
||||
let prefix = ¤t_text;
|
||||
let matches: Vec<&String> = self.slash_completions.iter()
|
||||
let matches: Vec<&String> = self
|
||||
.slash_completions
|
||||
.iter()
|
||||
.filter(|c| c.starts_with(prefix))
|
||||
.collect();
|
||||
if matches.len() == 1 {
|
||||
self.input.select_all();
|
||||
self.input.cut();
|
||||
for ch in matches[0].chars() { self.input.insert_char(ch); }
|
||||
for ch in matches[0].chars() {
|
||||
self.input.insert_char(ch);
|
||||
}
|
||||
self.showing_completions = false;
|
||||
} else if !matches.is_empty() {
|
||||
self.showing_completions = true;
|
||||
|
|
@ -471,8 +494,15 @@ fn draw_frame(
|
|||
.split(size);
|
||||
|
||||
draw_left_pane(
|
||||
f, main[0], conversation, conversation_scroll,
|
||||
input, input_lines, slash_completions, completion_index, showing_completions,
|
||||
f,
|
||||
main[0],
|
||||
conversation,
|
||||
conversation_scroll,
|
||||
input,
|
||||
input_lines,
|
||||
slash_completions,
|
||||
completion_index,
|
||||
showing_completions,
|
||||
);
|
||||
draw_right_pane(f, main[1], dashboard, spinner_frame);
|
||||
}
|
||||
|
|
@ -495,11 +525,16 @@ fn draw_left_pane(
|
|||
.split(area);
|
||||
|
||||
// --- conversation ---
|
||||
let conv_lines: Vec<Line> = conversation.iter().map(|line| {
|
||||
let mut style = Style::default().fg(line.color);
|
||||
if line.bold { style = style.add_modifier(Modifier::BOLD); }
|
||||
Line::from(Span::styled(&line.text, style))
|
||||
}).collect();
|
||||
let conv_lines: Vec<Line> = conversation
|
||||
.iter()
|
||||
.map(|line| {
|
||||
let mut style = Style::default().fg(line.color);
|
||||
if line.bold {
|
||||
style = style.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
Line::from(Span::styled(&line.text, style))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// FIFO viewport: newest content at the bottom, older above.
|
||||
let pane_rows = (left[0].height.saturating_sub(1) as usize).max(1);
|
||||
|
|
@ -510,13 +545,19 @@ fn draw_left_pane(
|
|||
let start = total.saturating_sub(pane_rows + offset);
|
||||
let visible: Vec<Line> = conv_lines.into_iter().skip(start).take(pane_rows).collect();
|
||||
|
||||
let conversation_widget = Paragraph::new(visible)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::default().fg(Color::DarkGray))
|
||||
.title(Span::styled(" Conversation ", Style::default().fg(Color::DarkGray))),
|
||||
);
|
||||
// NOTE: We intentionally do NOT enable Wrap here. The FIFO viewport
|
||||
// counts Line items to fill the pane — if a Line soft-wraps to 2+
|
||||
// visual rows the count is off and text misaligns. Long lines get
|
||||
// clipped rather than wrapped, which keeps scrolling accurate.
|
||||
let conversation_widget = Paragraph::new(visible).block(
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.border_style(Style::default().fg(Color::DarkGray))
|
||||
.title(Span::styled(
|
||||
" Conversation ",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)),
|
||||
);
|
||||
f.render_widget(conversation_widget, left[0]);
|
||||
|
||||
// --- input (real TextArea widget) ---
|
||||
|
|
@ -526,21 +567,28 @@ fn draw_left_pane(
|
|||
// --- completions popup ---
|
||||
if showing_completions {
|
||||
let current_text: String = input_lines.join("");
|
||||
let matches: Vec<&String> = slash_completions.iter()
|
||||
let matches: Vec<&String> = slash_completions
|
||||
.iter()
|
||||
.filter(|c| c.starts_with(current_text.as_str()))
|
||||
.collect();
|
||||
if !matches.is_empty() {
|
||||
let items: Vec<ListItem> = matches.iter().enumerate().map(|(i, m)| {
|
||||
let style = if i == completion_index % matches.len() {
|
||||
Style::default().bg(Color::DarkGray).fg(Color::White)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
};
|
||||
ListItem::new(Line::from(Span::styled(m.as_str(), style)))
|
||||
}).collect();
|
||||
let list = List::new(items)
|
||||
.block(Block::default().borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::DarkGray)));
|
||||
let items: Vec<ListItem> = matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, m)| {
|
||||
let style = if i == completion_index % matches.len() {
|
||||
Style::default().bg(Color::DarkGray).fg(Color::White)
|
||||
} else {
|
||||
Style::default().fg(Color::Gray)
|
||||
};
|
||||
ListItem::new(Line::from(Span::styled(m.as_str(), style)))
|
||||
})
|
||||
.collect();
|
||||
let list = List::new(items).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::DarkGray)),
|
||||
);
|
||||
let popup = Rect {
|
||||
x: left[1].x,
|
||||
y: left[1].y.saturating_sub(matches.len().min(8) as u16 + 2),
|
||||
|
|
@ -575,9 +623,21 @@ fn draw_right_pane(
|
|||
lines.push(kv("Turns", &state.turn_count.to_string(), Color::White));
|
||||
lines.push(kv("Input", &state.input_tokens.to_string(), Color::White));
|
||||
lines.push(kv("Output", &state.output_tokens.to_string(), Color::White));
|
||||
lines.push(kv("Cache R", &state.cache_read_tokens.to_string(), Color::Gray));
|
||||
lines.push(kv("Cache W", &state.cache_creation_tokens.to_string(), Color::Gray));
|
||||
lines.push(kv("Cost", &format!("${:.4}", state.cost_usd), Color::Yellow));
|
||||
lines.push(kv(
|
||||
"Cache R",
|
||||
&state.cache_read_tokens.to_string(),
|
||||
Color::Gray,
|
||||
));
|
||||
lines.push(kv(
|
||||
"Cache W",
|
||||
&state.cache_creation_tokens.to_string(),
|
||||
Color::Gray,
|
||||
));
|
||||
lines.push(kv(
|
||||
"Cost",
|
||||
&format!("${:.4}", state.cost_usd),
|
||||
Color::Yellow,
|
||||
));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
let pct = state.context_percent;
|
||||
|
|
@ -587,9 +647,17 @@ fn draw_right_pane(
|
|||
_ => Color::Green,
|
||||
};
|
||||
lines.push(section("Context"));
|
||||
lines.push(kv("Used", &format!("{:.1}% of {}", pct, state.context_window), Color::White));
|
||||
lines.push(kv(
|
||||
"Used",
|
||||
&format!("{:.1}% of {}", pct, state.context_window),
|
||||
Color::White,
|
||||
));
|
||||
lines.push(Line::from(""));
|
||||
lines.push(kv("Compactions", &state.compaction_count.to_string(), Color::Gray));
|
||||
lines.push(kv(
|
||||
"Compactions",
|
||||
&state.compaction_count.to_string(),
|
||||
Color::Gray,
|
||||
));
|
||||
lines.push(Line::from(""));
|
||||
|
||||
if !state.lsp_servers.is_empty() {
|
||||
|
|
@ -601,7 +669,10 @@ fn draw_right_pane(
|
|||
_ => Color::Red,
|
||||
};
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {} ", lsp.language), Style::default().fg(Color::White)),
|
||||
Span::styled(
|
||||
format!(" {} ", lsp.language),
|
||||
Style::default().fg(Color::White),
|
||||
),
|
||||
Span::styled(lsp.status.clone(), Style::default().fg(c)),
|
||||
]));
|
||||
}
|
||||
|
|
@ -613,63 +684,104 @@ fn draw_right_pane(
|
|||
lines.push(kv("Name", &team.team_name, Color::White));
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" Progress ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(format!("{}/{} done", team.completed_agents, team.total_agents), Style::default().fg(Color::Green)),
|
||||
Span::styled(format!(", {} fail", team.failed_agents), Style::default().fg(Color::Red)),
|
||||
Span::styled(format!(", {} run", team.running_agents), Style::default().fg(Color::Cyan)),
|
||||
Span::styled(
|
||||
format!("{}/{} done", team.completed_agents, team.total_agents),
|
||||
Style::default().fg(Color::Green),
|
||||
),
|
||||
Span::styled(
|
||||
format!(", {} fail", team.failed_agents),
|
||||
Style::default().fg(Color::Red),
|
||||
),
|
||||
Span::styled(
|
||||
format!(", {} run", team.running_agents),
|
||||
Style::default().fg(Color::Cyan),
|
||||
),
|
||||
]));
|
||||
for agent in &team.agents {
|
||||
let c = match agent.status.as_str() {
|
||||
"completed" => Color::Green, "failed" => Color::Red, _ => Color::Cyan,
|
||||
"completed" => Color::Green,
|
||||
"failed" => Color::Red,
|
||||
_ => Color::Cyan,
|
||||
};
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" ● ", Style::default().fg(c)),
|
||||
Span::styled(&agent.name, Style::default().fg(Color::White)),
|
||||
Span::styled(format!(" ({})", agent.subagent_type.as_deref().unwrap_or("?")), Style::default().fg(Color::Gray)),
|
||||
Span::styled(
|
||||
format!(" ({})", agent.subagent_type.as_deref().unwrap_or("?")),
|
||||
Style::default().fg(Color::Gray),
|
||||
),
|
||||
]));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
lines.push(section("Session"));
|
||||
lines.push(kv("ID", state.session_id.as_deref().unwrap_or("-"), Color::Gray));
|
||||
lines.push(kv(
|
||||
"ID",
|
||||
state.session_id.as_deref().unwrap_or("-"),
|
||||
Color::Gray,
|
||||
));
|
||||
|
||||
if !state.status_message.is_empty() {
|
||||
let frame = SPINNER_FRAMES[spinner_frame];
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!("{frame} {}", state.status_message), Style::default().fg(Color::Blue),
|
||||
format!("{frame} {}", state.status_message),
|
||||
Style::default().fg(Color::Blue),
|
||||
)));
|
||||
}
|
||||
|
||||
lines.push(Line::from(""));
|
||||
lines.push(Line::from(Span::styled("─ Keys ─", Style::default().fg(Color::DarkGray))));
|
||||
lines.push(Line::from(Span::styled(" Enter Submit Shift+Enter Newline", Style::default().fg(Color::DarkGray))));
|
||||
lines.push(Line::from(Span::styled(" ^P Swap ^T Team ^C Cancel ^D Exit", Style::default().fg(Color::DarkGray))));
|
||||
lines.push(Line::from(Span::styled(
|
||||
"─ Keys ─",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" Enter Submit Shift+Enter Newline",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
lines.push(Line::from(Span::styled(
|
||||
" ^P Swap ^T Team ^C Cancel ^D Exit",
|
||||
Style::default().fg(Color::DarkGray),
|
||||
)));
|
||||
|
||||
let widget = Paragraph::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.border_style(Style::default().fg(Color::DarkGray))
|
||||
.title(Span::styled(" Dashboard ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))),
|
||||
.title(Span::styled(
|
||||
" Dashboard ",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
)
|
||||
.wrap(Wrap { trim: true });
|
||||
f.render_widget(widget, area);
|
||||
|
||||
let gauge_area = Rect {
|
||||
x: area.x + 2, y: area.y + 16,
|
||||
width: area.width.saturating_sub(4), height: 1,
|
||||
x: area.x + 2,
|
||||
y: area.y + 16,
|
||||
width: area.width.saturating_sub(4),
|
||||
height: 1,
|
||||
};
|
||||
let gauge = Gauge::default()
|
||||
.gauge_style(Style::default().fg(gauge_color).bg(Color::DarkGray))
|
||||
.ratio(if pct > 0.0 { (pct / 100.0).min(1.0) } else { 0.0 });
|
||||
.ratio(if pct > 0.0 {
|
||||
(pct / 100.0).min(1.0)
|
||||
} else {
|
||||
0.0
|
||||
});
|
||||
f.render_widget(gauge, gauge_area);
|
||||
}
|
||||
|
||||
fn section<'a>(label: &str) -> Line<'a> {
|
||||
Line::from(Span::styled(
|
||||
format!("─ {label} ─"),
|
||||
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue