diff --git a/rust/Cargo.lock b/rust/Cargo.lock old mode 100755 new mode 100644 index 8f1a171d..028e4b20 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -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" diff --git a/rust/crates/api/src/error.rs b/rust/crates/api/src/error.rs index 578800ff..e8ec73a4 100644 --- a/rust/crates/api/src/error.rs +++ b/rust/crates/api/src/error.rs @@ -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 { match self { Self::Api { retry_after, .. } => *retry_after, diff --git a/rust/crates/api/src/http_client.rs b/rust/crates/api/src/http_client.rs index 30e1ddd9..648a0811 100644 --- a/rust/crates/api/src/http_client.rs +++ b/rust/crates/api/src/http_client.rs @@ -132,20 +132,20 @@ pub fn build_http_client() -> Result { #[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 { build_http_client_with_opts(config, &TimeoutConfig::from_env()) } @@ -158,10 +158,7 @@ pub fn build_http_client_with_opts( ) -> Result { 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); diff --git a/rust/crates/api/src/lib.rs b/rust/crates/api/src/lib.rs index 10c38c85..e96e92f8 100644 --- a/rust/crates/api/src/lib.rs +++ b/rust/crates/api/src/lib.rs @@ -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::{ diff --git a/rust/crates/api/src/providers/anthropic.rs b/rust/crates/api/src/providers/anthropic.rs index 8951edde..430b3eff 100644 --- a/rust/crates/api/src/providers/anthropic.rs +++ b/rust/crates/api/src/providers/anthropic.rs @@ -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, 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 { #[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 { @@ -892,7 +889,7 @@ async fn expect_success(response: reqwest::Response) -> Result(&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 Option { -======= -fn parse_retry_after(headers: &reqwest::header::HeaderMap, status: reqwest::StatusCode) -> Option { ->>>>>>> 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 diff --git a/rust/crates/api/src/providers/mod.rs b/rust/crates/api/src/providers/mod.rs index d144c551..2524e552 100644 --- a/rust/crates/api/src/providers/mod.rs +++ b/rust/crates/api/src/providers/mod.rs @@ -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 { - 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, 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 { - 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 { 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 { 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) } } diff --git a/rust/crates/api/src/providers/openai_compat.rs b/rust/crates/api/src/providers/openai_compat.rs index 18e9703a..8fb39699 100644 --- a/rust/crates/api/src/providers/openai_compat.rs +++ b/rust/crates/api/src/providers/openai_compat.rs @@ -1697,7 +1697,11 @@ fn parse_sse_frame( } fn read_env_non_empty(key: &str) -> Result, 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(&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 Option { -======= -fn parse_retry_after(headers: &reqwest::header::HeaderMap, status: reqwest::StatusCode) -> Option { ->>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry) if status != reqwest::StatusCode::TOO_MANY_REQUESTS { return None; } diff --git a/rust/crates/commands/src/lib.rs b/rust/crates/commands/src/lib.rs index 374402b9..79086913 100644 --- a/rust/crates/commands/src/lib.rs +++ b/rust/crates/commands/src/lib.rs @@ -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 ]"), - resume_supported: true, - }, ]; #[derive(Debug, Clone, PartialEq, Eq)] @@ -1195,13 +1188,6 @@ pub enum SlashCommand { History { count: Option, }, - Lsp { - action: Option, - target: Option, - }, - Team { - action: Option, - }, Unknown(String), Team { action: Option, @@ -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); } diff --git a/rust/crates/runtime/src/compact.rs b/rust/crates/runtime/src/compact.rs index 56811030..797d3a6b 100644 --- a/rust/crates/runtime/src/compact.rs +++ b/rust/crates/runtime/src/compact.rs @@ -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 diff --git a/rust/crates/runtime/src/config.rs b/rust/crates/runtime/src/config.rs index 503e6f33..95057563 100644 --- a/rust/crates/runtime/src/config.rs +++ b/rust/crates/runtime/src/config.rs @@ -125,22 +125,6 @@ pub struct RuntimePluginConfig { max_output_tokens: Option, } -<<<<<<< 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, - 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, - 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, model: Option, - lsp_auto_start: bool, aliases: BTreeMap, permission_mode: Option, permission_rules: RuntimePermissionRuleConfig, sandbox: SandboxConfig, provider_fallbacks: ProviderFallbackConfig, trusted_roots: Vec, -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD api_timeout: ApiTimeoutConfig, rules_import: RulesImportConfig, provider: RuntimeProviderConfig, - lsp: BTreeMap, - 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, -} - -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, @@ -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), - #[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, ->>>>>>> 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 { merge_trusted_roots(self.trusted_roots(), per_call_roots) -======= - #[must_use] - pub fn lsp(&self) -> &BTreeMap { - &self.feature_config.lsp ->>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration) -======= - #[must_use] - pub fn lsp(&self) -> &BTreeMap { - &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 { @@ -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 { - &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 { .map(ToOwned::to_owned) } -fn parse_optional_subagent_model(value: &JsonValue) -> Option { - 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, 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>>>>>> 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, ConfigE ) } -<<<<<<< HEAD fn parse_optional_rules_import(root: &JsonValue) -> Result { let Some(object) = root.as_object() else { return Ok(RulesImportConfig::default()); @@ -2437,36 +2172,6 @@ fn parse_optional_provider_config(root: &JsonValue) -> Result Result { - 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 = 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 { 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 { - 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, 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( diff --git a/rust/crates/runtime/src/config_validate.rs b/rust/crates/runtime/src/config_validate.rs index 967916bc..e37a3c46 100644 --- a/rust/crates/runtime/src/config_validate.rs +++ b/rust/crates/runtime/src/config_validate.rs @@ -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" - } - )); - } } diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index 7e99db34..923e6107 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -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, diff --git a/rust/crates/runtime/src/lsp_client/dispatch.rs b/rust/crates/runtime/src/lsp_client/dispatch.rs index d07943ef..a698092e 100644 --- a/rust/crates/runtime/src/lsp_client/dispatch.rs +++ b/rust/crates/runtime/src/lsp_client/dispatch.rs @@ -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 + )); } } } diff --git a/rust/crates/runtime/src/lsp_discovery.rs b/rust/crates/runtime/src/lsp_discovery.rs index 9d8c3b7a..14820ad4 100644 --- a/rust/crates/runtime/src/lsp_discovery.rs +++ b/rust/crates/runtime/src/lsp_discovery.rs @@ -267,23 +267,11 @@ pub fn known_lsp_servers() -> Vec { /// 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 { 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 { } 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() } diff --git a/rust/crates/runtime/src/lsp_process/parse.rs b/rust/crates/runtime/src/lsp_process/parse.rs index 1a5debf4..fb51df6f 100644 --- a/rust/crates/runtime/src/lsp_process/parse.rs +++ b/rust/crates/runtime/src/lsp_process/parse.rs @@ -321,7 +321,7 @@ pub(super) fn parse_code_actions(value: &JsonValue) -> Vec Option>>>>>> 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 { 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 { 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, 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 = 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, 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 { - 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 { - 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 } diff --git a/rust/crates/runtime/src/sandbox.rs b/rust/crates/runtime/src/sandbox.rs index 62c7c7e4..2df08791 100644 --- a/rust/crates/runtime/src/sandbox.rs +++ b/rust/crates/runtime/src/sandbox.rs @@ -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) }) } diff --git a/rust/crates/runtime/src/trident.rs b/rust/crates/runtime/src/trident.rs index cfa9d8d5..6e5b21f2 100644 --- a/rust/crates/runtime/src/trident.rs +++ b/rust/crates/runtime/src/trident.rs @@ -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>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster)) } } } let mut obsolete_indices: BTreeSet = 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 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 { -<<<<<<< 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::(input) @@ -301,15 +257,10 @@ fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option } fn extract_path_from_tool_output(tool_name: &str, output: &str) -> Option { -<<<<<<< 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::(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 = Vec::new(); let mut cluster_buffers: BTreeMap> = 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 { -======= - 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 { 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] diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index ca4d3d6b..66ba043b 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -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, reasoning_effort: Option, allow_broad_cwd: bool, -) -> Result<(), Box> { - 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, - permission_mode: PermissionMode, - base_commit: Option, - reasoning_effort: Option, - allow_broad_cwd: bool, -) -> Result<(), Box> { - 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, - permission_mode: PermissionMode, - base_commit: Option, - reasoning_effort: Option, - allow_broad_cwd: bool, ) -> Result<(), Box> { 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!(" - Warning: Missing config fields: {}", missing.join(", ")); - eprintln!(" Run claw setup or type /setup to configure. -"); - } - } - } - - // 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 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, - 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> { 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) -> Vec { - 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> = vec![None; calls.len()]; - let mut parallel_calls: Vec<(usize, String, String, String)> = Vec::new(); - let mut sequential_indices: Vec = 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)> = - 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); diff --git a/rust/crates/rusty-claude-cli/src/setup_wizard.rs b/rust/crates/rusty-claude-cli/src/setup_wizard.rs index 943eb7e3..c2f7b6ff 100644 --- a/rust/crates/rusty-claude-cli/src/setup_wizard.rs +++ b/rust/crates/rusty-claude-cli/src/setup_wizard.rs @@ -233,11 +233,7 @@ fn prompt_fast_model( main_model: Option<&str>, ) -> Result, Box> { println!(); -<<<<<<< HEAD println!(" \x1b[1mFast Model (for Agent subtasks)\x1b[0m"); -======= - println!(" Fast Model (for Agent subtasks)"); ->>>>>>> 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."); diff --git a/rust/crates/rusty-claude-cli/src/tui.rs b/rust/crates/rusty-claude-cli/src/tui.rs index e5ea8f15..edbc58f8 100644 --- a/rust/crates/rusty-claude-cli/src/tui.rs +++ b/rust/crates/rusty-claude-cli/src/tui.rs @@ -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 = 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 = 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 = 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 = 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 = 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), )) } diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index da6d8786..a72261ed 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -7,29 +7,17 @@ use aspect_macros::aspect; use aspect_std::LoggingAspect; use api::{ -<<<<<<< HEAD max_tokens_for_model, model_family_identity_for, resolve_model_alias, ApiError, ContentBlockDelta, InputContentBlock, InputMessage, MessageRequest, MessageResponse, OutputContentBlock, ProviderClient, StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock, -======= - max_tokens_for_model, model_token_limit, ModelTokenLimit, resolve_model_alias, ApiError, ContentBlockDelta, InputContentBlock, - InputMessage, MessageRequest, MessageResponse, OutputContentBlock, ProviderClient, - StreamEvent as ApiStreamEvent, ToolChoice, ToolDefinition, ToolResultContentBlock, ->>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring) }; use plugins::PluginTool; use reqwest::blocking::Client; use runtime::{ -<<<<<<< HEAD check_freshness, dedupe_superseded_commit_events, edit_file_in_workspace, execute_bash, glob_search_in_workspace, grep_search_in_workspace, load_system_prompt, lsp_client::LspRegistry, -======= - check_freshness, dedupe_superseded_commit_events, edit_file, execute_bash, glob_search, - grep_search, load_system_prompt, - lsp_client::{LspDiagnostic, LspRegistry}, ->>>>>>> 73cc8277 (feat: auto-LSP integration with didOpen/didChange and diagnostic enrichment) mcp_tool_bridge::McpToolRegistry, permission_enforcer::{EnforcementResult, PermissionEnforcer}, read_file_in_workspace, @@ -37,27 +25,18 @@ use runtime::{ task_registry::TaskRegistry, team_cron_registry::{CronRegistry, TeamRegistry}, worker_boot::{WorkerReadySnapshot, WorkerRegistry, WorkerTaskReceipt}, -<<<<<<< HEAD write_file_in_workspace, ApiClient, ApiRequest, AssistantEvent, BashCommandInput, BashCommandOutput, BranchFreshness, ConfigLoader, ContentBlock, ConversationMessage, ConversationRuntime, GrepSearchInput, LaneCommitProvenance, LaneEvent, LaneEventBlocker, LaneEventName, LaneEventStatus, LaneFailureClass, McpDegradedReport, MessageRole, PermissionMode, PermissionPolicy, PromptCacheEvent, ProviderFallbackConfig, RuntimeError, Session, TaskPacket, ToolError, ToolExecutor, -======= - write_file, ApiClient, ApiRequest, AssistantEvent, BashCommandInput, BashCommandOutput, - BranchFreshness, ConfigLoader, ContentBlock, ConversationMessage, ConversationRuntime, - GrepSearchInput, LaneCommitProvenance, LaneEvent, LaneEventBlocker, LaneEventName, - LaneEventStatus, LaneFailureClass, McpDegradedReport, MessageRole, PermissionMode, - PermissionPolicy, PromptCacheEvent, ProviderFallbackConfig, RuntimeError, Session, TaskPacket, - ToolError, ToolExecutor, TurnProgressReporter, ->>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring) }; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; /// Global task registry shared across tool invocations within a session. -pub fn global_lsp_registry() -> &'static LspRegistry { +fn global_lsp_registry() -> &'static LspRegistry { use std::sync::OnceLock; static REGISTRY: OnceLock = OnceLock::new(); REGISTRY.get_or_init(LspRegistry::new) @@ -690,7 +669,7 @@ pub fn mvp_tool_specs() -> Vec { }, ToolSpec { name: "Agent", - description: "Launch a specialized agent task. Use subagent_type to select the agent role: Explore (read-only search), Plan (explore + todo), Verification (explore + bash + todo), or general-purpose (full access). The agent uses subagentModel from settings if set, otherwise the default model.", + description: "Launch a specialized agent task and persist its handoff metadata.", input_schema: json!({ "type": "object", "properties": { @@ -1115,36 +1094,24 @@ pub fn mvp_tool_specs() -> Vec { }, ToolSpec { name: "TeamCreate", - description: "Create a team of agents that run in parallel. Each task becomes an independent Agent with its own context. Agents communicate via AgentMessage, claim tasks via TaskClaim, and report progress automatically. Reviewer agents are included for quality checks. Use TeamStatus to monitor, /team to toggle. 'mode' preset: tiny/1x (4 agents), small/2x (8), medium/3x (12), large/4x (16), xlarge/5x (20), mega/6x (24). Requires /team on.", + description: "Create a team of sub-agents for parallel task execution.", input_schema: json!({ "type": "object", "properties": { "name": { "type": "string" }, - "mode": { - "type": "string", - "description": "Preset team size. Named sizes: 'tiny'/'1x'=1 per role (3+1 agents), 'small'/'2x'=2 per role (6+2), 'medium'/'3x'=3 per role (9+3), 'large'/'4x'=4 per role (12+4), 'xlarge'/'5x'=5 per role (15+5), 'mega'/'6x'=6 per role (18+6). Overrides 'tasks'.", - "enum": ["1x", "2x", "3x", "4x", "5x", "6x", "tiny", "small", "medium", "large", "xlarge", "mega"] - }, - "prompt": { - "type": "string", - "description": "Shared prompt for all agents when using 'mode' preset. Each agent gets this prompt with its role prepended." - }, "tasks": { "type": "array", - "description": "Manual task list. Ignored when 'mode' is set.", "items": { "type": "object", "properties": { "prompt": { "type": "string" }, - "description": { "type": "string" }, - "subagent_type": { "type": "string", "enum": ["Explore", "Plan", "Verification", "general-purpose"] }, - "model": { "type": "string" } + "description": { "type": "string" } }, "required": ["prompt"] } } }, - "required": ["name"], + "required": ["name", "tasks"], "additionalProperties": false }), required_permission: PermissionMode::DangerFullAccess, @@ -1162,108 +1129,6 @@ pub fn mvp_tool_specs() -> Vec { }), required_permission: PermissionMode::DangerFullAccess, }, - ToolSpec { - name: "AgentMessage", - description: "Send or read messages between agents in a team. Use action=send to post a message to another agent's inbox, action=read to check your own inbox, or action=broadcast to send to all agents in a team. Agents communicate through a shared mailbox directory.", - input_schema: json!({ - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["send", "read", "broadcast"], - "description": "send=post to agent inbox, read=check own inbox, broadcast=send to all team members" - }, - "agent_id": { "type": "string", "description": "Target agent ID (for send action)" }, - "team_id": { "type": "string", "description": "Team ID (for broadcast)" }, - "message": { "type": "string", "description": "Message content (for send/broadcast)" }, - "mark_read": { "type": "boolean", "description": "Mark retrieved messages as read (default true)" } - }, - "required": ["action"], - "additionalProperties": false - }), - required_permission: PermissionMode::ReadOnly, - }, - ToolSpec { - name: "TeamStatus", - description: "Check the progress of a team of agents. Returns structured status: which agents are running, completed, or failed, with their results. Use action=status for a snapshot, action=summary for final results when all agents are done, or action=events for a timeline of team events.", - input_schema: json!({ - "type": "object", - "properties": { - "team_id": { "type": "string", "description": "Team ID to check" }, - "action": { - "type": "string", - "enum": ["status", "summary", "events", "inbox", "kill", "suggestions"], - "description": "status=live snapshot, summary=final results, events=timeline, inbox=team messages, kill=terminate stuck agent, suggestions=list pending AGENTS.md suggestions" - } - }, - "required": ["team_id"], - "additionalProperties": false - }), - required_permission: PermissionMode::ReadOnly, - }, - ToolSpec { - name: "TaskClaim", - description: "Claim, release, or list task claims. Agents claim tasks to prevent duplicate work. Use action=claim to atomically claim a task (returns success/failure), action=release to release a claim, or action=list to see all active claims.", - input_schema: json!({ - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": ["claim", "release", "list"], - "description": "claim=atomically acquire a task lock, release=release your claim, list=show all active claims" - }, - "task_id": { "type": "string", "description": "Task identifier to claim or release" }, - "team_id": { "type": "string", "description": "Team ID (used with claim and list)" }, - "agent_id": { "type": "string", "description": "Agent ID claiming the task (used with claim)" } - }, - "required": ["action"], - "additionalProperties": false - }), - required_permission: PermissionMode::ReadOnly, - }, - ToolSpec { - name: "AgentSuggestion", - description: "Suggest an addition to AGENTS.md for the team to review. Agents should NOT write AGENTS.md directly. Instead, propose patterns, pitfalls, or style guidelines. The team lead (human) decides what to include.", - input_schema: json!({ - "type": "object", - "properties": { - "category": { - "type": "string", - "enum": ["pattern", "pitfall", "style"], - "description": "Category: pattern=proven approach, pitfall=thing to avoid, style=coding convention" - }, - "suggestion": { "type": "string", "description": "The suggestion text to add to AGENTS.md" }, - "team_id": { "type": "string", "description": "Team ID" }, - "agent_id": { "type": "string", "description": "Agent ID making the suggestion" } - }, - "required": ["category", "suggestion"], - "additionalProperties": false - }), - required_permission: PermissionMode::ReadOnly, - }, - ToolSpec { - name: "ContextRequest", - description: "Request additional context for your task. Use when you need specific files or symbols you haven't seen. You have a budget of 3 retrieval cycles. Be specific: name exact files or describe the symbols you need and why.", - input_schema: json!({ - "type": "object", - "properties": { - "files": { - "type": "array", - "items": { "type": "string" }, - "description": "Exact file paths to read" - }, - "symbols": { - "type": "array", - "items": { "type": "string" }, - "description": "Symbol names to search for (e.g. function names, type names)" - }, - "reason": { "type": "string", "description": "Why you need this context (helps prioritize)" } - }, - "required": ["reason"], - "additionalProperties": false - }), - required_permission: PermissionMode::ReadOnly, - }, ToolSpec { name: "CronCreate", description: "Create a scheduled recurring task.", @@ -1304,16 +1169,14 @@ pub fn mvp_tool_specs() -> Vec { }, ToolSpec { name: "LSP", - description: "Query Language Server Protocol for code intelligence (symbols, references, diagnostics, code actions, rename, signature help, code lens, workspace symbols).", + description: "Query Language Server Protocol for code intelligence (symbols, references, diagnostics).", input_schema: json!({ "type": "object", "properties": { - "action": { "type": "string", "enum": ["symbols", "references", "diagnostics", "definition", "hover", "code_action", "rename", "signature_help", "code_lens", "workspace_symbols"] }, + "action": { "type": "string", "enum": ["symbols", "references", "diagnostics", "definition", "hover"] }, "path": { "type": "string" }, "line": { "type": "integer", "minimum": 0 }, "character": { "type": "integer", "minimum": 0 }, - "end_line": { "type": "integer", "minimum": 0 }, - "end_character": { "type": "integer", "minimum": 0 }, "query": { "type": "string" } }, "required": ["action"], @@ -1405,7 +1268,6 @@ pub fn mvp_tool_specs() -> Vec { required_permission: PermissionMode::DangerFullAccess, }, ToolSpec { -<<<<<<< HEAD name: "GitStatus", description: "Show the working tree status (branch, staged, unstaged, untracked). Equivalent to 'git status --short --branch'. Use this instead of running git status via bash to get structured, parseable output.", input_schema: json!({ @@ -1479,25 +1341,6 @@ pub fn mvp_tool_specs() -> Vec { "additionalProperties": false }), required_permission: PermissionMode::ReadOnly, -======= - name: "SubAgent", - description: "Launch a lightweight sub-agent that autonomously performs multi-step work using a fast model. Use for tasks like searching code, reading multiple files, or gathering context that would require many sequential tool calls. Returns a summary of the sub-agent's findings.", - input_schema: json!({ - "type": "object", - "properties": { - "prompt": { "type": "string" }, - "task_type": { - "type": "string", - "enum": ["Explore", "Plan", "Verify"], - "description": "Explore=search+read only, Plan=explore+todo, Verify=explore+bash+todo" - }, - "model": { "type": "string" } - }, - "required": ["prompt"], - "additionalProperties": false - }), - required_permission: PermissionMode::DangerFullAccess, ->>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation) }, ] } @@ -1633,11 +1476,6 @@ fn execute_tool_with_enforcer( .and_then(run_worker_observe_completion), "TeamCreate" => from_value::(input).and_then(run_team_create), "TeamDelete" => from_value::(input).and_then(run_team_delete), - "AgentMessage" => from_value::(input).and_then(run_agent_message), - "TeamStatus" => from_value::(input).and_then(run_team_status), - "TaskClaim" => from_value::(input).and_then(run_task_claim), - "AgentSuggestion" => from_value::(input).and_then(run_agent_suggestion), - "ContextRequest" => from_value::(input).and_then(run_context_request), "CronCreate" => from_value::(input).and_then(run_cron_create), "CronDelete" => from_value::(input).and_then(run_cron_delete), "CronList" => run_cron_list(input.clone()), @@ -1652,15 +1490,11 @@ fn execute_tool_with_enforcer( "TestingPermission" => { from_value::(input).and_then(run_testing_permission) } -<<<<<<< HEAD "GitStatus" => from_value::(input).and_then(run_git_status), "GitDiff" => from_value::(input).and_then(run_git_diff), "GitLog" => from_value::(input).and_then(run_git_log), "GitShow" => from_value::(input).and_then(run_git_show), "GitBlame" => from_value::(input).and_then(run_git_blame), -======= - "SubAgent" => from_value::(input).and_then(run_sub_agent), ->>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation) _ => Err(format!("unsupported tool: {name}")), } } @@ -1931,969 +1765,36 @@ fn run_worker_observe_completion(input: WorkerObserveCompletionInput) -> Result< #[allow(clippy::needless_pass_by_value)] fn run_team_create(input: TeamCreateInput) -> Result { - if std::env::var("CLAWD_AGENT_TEAMS").map_or(true, |v| v != "1") { - return Err("Agent teams is disabled. Use /team on or Ctrl+T to enable.".to_string()); + let task_ids: Vec = input + .tasks + .iter() + .filter_map(|t| t.get("task_id").and_then(|v| v.as_str()).map(str::to_owned)) + .collect(); + let team = global_team_registry().create(&input.name, task_ids); + // Register team assignment on each task + for task_id in &team.task_ids { + let _ = global_task_registry().assign_team(task_id, &team.team_id); } - - let team_id = format!("team-{}", std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos()); - let output_dir = agent_store_dir()?; - let team_dir = output_dir.join("teams"); - std::fs::create_dir_all(&team_dir).map_err(|e| e.to_string())?; - - // Expand mode preset into tasks, or use manual tasks. - // Default to "2x" when neither mode nor tasks are provided. - let tasks = if let Some(mode) = &input.mode { - expand_team_mode(mode, input.prompt.as_deref().unwrap_or("Explore the codebase and report findings"), &team_id)? - } else if input.tasks.is_empty() { - expand_team_mode("2x", input.prompt.as_deref().unwrap_or("Explore the codebase and report findings"), &team_id)? - } else { - input.tasks.clone() - }; - - let mut agent_ids: Vec = Vec::new(); - let mut agent_outputs: Vec = Vec::new(); - - for (i, task) in tasks.iter().enumerate() { - let prompt = task.get("prompt").and_then(|v| v.as_str()).unwrap_or(""); - let description = task.get("description").and_then(|v| v.as_str()).unwrap_or(&input.name); - let subagent_type = task.get("subagent_type").and_then(|v| v.as_str()); - let model_override = task.get("model").and_then(|v| v.as_str()); - - if prompt.is_empty() { - continue; - } - - let task_id = task.get("task_id").and_then(|v| v.as_str()).map(|s| s.to_string()); - let agent_input = AgentInput { - description: description.to_string(), - prompt: prompt.to_string(), - subagent_type: subagent_type.map(|s| s.to_string()), - name: Some(format!("{}-agent-{}", slugify_agent_name(&input.name), i + 1)), - model: model_override.map(|s| s.to_string()), - team_id: Some(team_id.clone()), - task_id, - }; - - match execute_agent_with_spawn(agent_input, spawn_agent_job) { - Ok(manifest) => { - let aid = manifest.agent_id.clone(); - // Set CLAWD_AGENT_ID env for the agent thread - agent_ids.push(aid.clone()); - agent_outputs.push(json!({ - "agent_id": aid, - "name": manifest.name, - "status": manifest.status, - "subagent_type": manifest.subagent_type, - })); - } - Err(error) => { - agent_outputs.push(json!({ - "agent_index": i, - "error": error, - })); - } - } - } - - // Persist team manifest - let team_manifest = json!({ - "team_id": team_id, - "name": input.name, - "agent_ids": agent_ids, - "agent_count": agent_ids.len(), - "status": "running", - "created_at": iso8601_now(), - }); - let manifest_path = team_dir.join(format!("{team_id}.json")); - std::fs::write(&manifest_path, serde_json::to_string_pretty(&team_manifest).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - - // Register in global registry - let team = global_team_registry().create(&input.name, agent_ids.clone()); - - // Spawn background watcher that prints progress to stderr - spawn_team_watcher(&team_id, &agent_ids); - to_pretty_json(json!({ - "team_id": team_id, - "name": input.name, - "agent_count": agent_ids.len(), - "agents": agent_outputs, - "status": "running", - "created_at": team.created_at, - "message": format!("Team created with {} agents. Use AgentMessage to coordinate. TeamStatus shows live progress.", agent_ids.len()), + "team_id": team.team_id, + "name": team.name, + "task_count": team.task_ids.len(), + "task_ids": team.task_ids, + "status": team.status, + "created_at": team.created_at })) } #[allow(clippy::needless_pass_by_value)] -fn run_team_status(input: TeamStatusInput) -> Result { - let action = input.action.as_deref().unwrap_or("status"); - let team_dir = agent_store_dir()?.join("teams"); - let manifest_path = team_dir.join(format!("{}.json", input.team_id)); - if !manifest_path.exists() { - return Err(format!("team {} not found", input.team_id)); - } - let team_data: Value = serde_json::from_str( - &std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())? - ).map_err(|e| e.to_string())?; - let agent_ids = team_data.get("agent_ids") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - - let store_dir = agent_store_dir()?; - let mut agents_detail: Vec = Vec::new(); - let mut running_count = 0usize; - let mut completed_count = 0usize; - let mut failed_count = 0usize; - - for id_val in &agent_ids { - if let Some(id) = id_val.as_str() { - let agent_json = store_dir.join(format!("{id}.json")); - let agent_md = store_dir.join(format!("{id}.md")); - let mut detail = json!({ "agent_id": id }); - - if agent_json.exists() { - if let Ok(data) = std::fs::read_to_string(&agent_json) { - if let Ok(parsed) = serde_json::from_str::(&data) { - detail["status"] = parsed.get("status").cloned().unwrap_or(json!("unknown")); - detail["name"] = parsed.get("name").cloned().unwrap_or(json!(null)); - detail["subagent_type"] = parsed.get("subagent_type").cloned().unwrap_or(json!(null)); - if let Some(completed) = parsed.get("completed_at") { - detail["completed_at"] = completed.clone(); - } - } - } - } else { - detail["status"] = json!("running"); - } - - if agent_md.exists() { - if let Ok(md_content) = std::fs::read_to_string(&agent_md) { - let summary = md_content.lines() - .skip_while(|line| !line.starts_with("## Result")) - .skip(1) - .take(10) - .collect::>() - .join("\n"); - if !summary.is_empty() { - detail["result_preview"] = json!(summary); - } - } - } - - match detail.get("status").and_then(|v| v.as_str()).unwrap_or("unknown") { - "completed" => completed_count += 1, - "failed" => failed_count += 1, - _ => running_count += 1, - } - agents_detail.push(detail); - } - } - - match action { - "status" => { - to_pretty_json(json!({ - "team_id": input.team_id, - "team_name": team_data.get("name"), - "status": if running_count > 0 { "running" } else if failed_count > 0 { "completed_with_failures" } else { "completed" }, - "progress": { - "total": agent_ids.len(), - "running": running_count, - "completed": completed_count, - "failed": failed_count, - }, - "agents": agents_detail, - })) - } - "summary" => { - let mut results: Vec = Vec::new(); - for id_val in &agent_ids { - if let Some(id) = id_val.as_str() { - let agent_md = store_dir.join(format!("{id}.md")); - let agent_json = store_dir.join(format!("{id}.json")); - let mut entry = json!({ "agent_id": id }); - - if let Ok(data) = std::fs::read_to_string(&agent_json) { - if let Ok(parsed) = serde_json::from_str::(&data) { - entry["status"] = parsed.get("status").cloned().unwrap_or(json!("unknown")); - entry["name"] = parsed.get("name").cloned().unwrap_or(json!(null)); - entry["subagent_type"] = parsed.get("subagent_type").cloned().unwrap_or(json!(null)); - } - } - if let Ok(md_content) = std::fs::read_to_string(&agent_md) { - entry["result"] = json!(md_content); - } - results.push(entry); - } - } - to_pretty_json(json!({ - "team_id": input.team_id, - "team_name": team_data.get("name"), - "status": if running_count > 0 { "still_running" } else if failed_count > 0 { "completed_with_failures" } else { "completed" }, - "progress": { - "total": agent_ids.len(), - "running": running_count, - "completed": completed_count, - "failed": failed_count, - }, - "results": results, - })) - } - "events" => { - let events_path = team_dir.join(format!("{}-events.jsonl", input.team_id)); - let mut events: Vec = Vec::new(); - if events_path.exists() { - if let Ok(content) = std::fs::read_to_string(&events_path) { - for line in content.lines() { - if let Ok(event) = serde_json::from_str::(line) { - events.push(event); - } - } - } - } - to_pretty_json(json!({ - "team_id": input.team_id, - "event_count": events.len(), - "events": events, - })) - } - "inbox" => { - let inbox_dir = agent_mailbox_dir().join("team").join(&input.team_id); - let mut messages: Vec = Vec::new(); - if inbox_dir.exists() { - if let Ok(entries) = std::fs::read_dir(&inbox_dir) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - if path.extension().is_some_and(|ext| ext == "json") { - if let Ok(data) = std::fs::read_to_string(&path) { - if let Ok(msg) = serde_json::from_str::(&data) { - messages.push(msg); - } - } - } - } - } - } - to_pretty_json(json!({ - "team_id": input.team_id, - "inbox_count": messages.len(), - "messages": messages, - })) - } - "kill" => { - let agent_id = input.agent_id.as_deref().unwrap_or(""); - if agent_id.is_empty() { - return Err("agent_id is required for kill action".to_string()); - } - // Write kill signal file that the agent checks - let kill_dir = agent_mailbox_dir().join("team").join(&input.team_id); - let _ = std::fs::create_dir_all(&kill_dir); - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let entry = json!({ - "event": "kill_signal", - "agent_id": agent_id, - "reason": input.reason.as_deref().unwrap_or("terminated by team lead"), - "timestamp": ts, - }); - let kill_file = kill_dir.join(format!("kill-{agent_id}-{ts}.json")); - std::fs::write(&kill_file, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - to_pretty_json(json!({ - "action": "kill", - "agent_id": agent_id, - "status": "kill_signal_sent", - })) - } - "suggestions" => { - let suggestions_dir = agent_store_dir()?.join("suggestions"); - let mut suggestions = Vec::new(); - if let Ok(entries) = std::fs::read_dir(&suggestions_dir) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - if path.extension().map_or(false, |e| e == "json") { - if let Ok(content) = std::fs::read_to_string(&path) { - if let Ok(v) = serde_json::from_str::(&content) { - if input.team_id.is_empty() || v.get("team_id").and_then(|t| t.as_str()).map_or(true, |t| t == input.team_id) { - suggestions.push(v); - } - } - } - } - } - } - to_pretty_json(json!({ - "action": "suggestions", - "suggestions": suggestions, - "count": suggestions.len(), - })) - } - other => Err(format!("unknown TeamStatus action: {other}. Use status, summary, events, inbox, kill, or suggestions")), - } -} - -/// Spawn a background thread that watches a team's agents and prints progress. -/// Prints agent completion/failure events to stderr. Exits when all agents are done. -fn spawn_team_watcher(team_id: &str, agent_ids: &[String]) { - let team_id = team_id.to_string(); - let total = agent_ids.len(); - let thread_name = format!("claw-team-watch-{team_id}"); - - std::thread::Builder::new() - .name(thread_name) - .spawn(move || { - let inbox_dir = agent_mailbox_dir().join("team").join(&team_id); - let _ = std::fs::create_dir_all(&inbox_dir); - - let team_dir = agent_store_dir().map(|d| d.join("teams")).unwrap_or_default(); - let events_path = team_dir.join(format!("{team_id}-events.jsonl")); - - let mut completed: BTreeSet = BTreeSet::new(); - let mut failed: BTreeSet = BTreeSet::new(); - let mut seen: BTreeSet = BTreeSet::new(); - - eprintln!("[team] {team_id}: watching {total} agents via inbox..."); - - loop { - let mut new_events = false; - if let Ok(entries) = std::fs::read_dir(&inbox_dir) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(); - if seen.contains(&name) { - continue; - } - if !path.extension().is_some_and(|ext| ext == "json") { - continue; - } - if let Ok(data) = std::fs::read_to_string(&path) { - if let Ok(parsed) = serde_json::from_str::(&data) { - seen.insert(name); - new_events = true; - let agent_id = parsed.get("agent_id").and_then(|v| v.as_str()).unwrap_or("unknown"); - let event = parsed.get("event").and_then(|v| v.as_str()).unwrap_or("unknown"); - let agent_name = parsed.get("name").and_then(|v| v.as_str()).unwrap_or(agent_id); - let subagent_type = parsed.get("subagent_type").and_then(|v| v.as_str()).unwrap_or("unknown"); - let result_preview = parsed.get("result_preview").and_then(|v| v.as_str()).unwrap_or(""); - - match event { - "agent_completed" => { - completed.insert(agent_id.to_string()); - let done_count = completed.len() + failed.len(); - eprintln!("[team] {team_id}: done {agent_name} ({subagent_type}) completed - {done_count}/{total}{}", if result_preview.is_empty() { String::new() } else { format!(" - {}", &result_preview[..result_preview.len().min(120)]) }); - append_team_event(&events_path, &team_id, agent_id, "completed", agent_name, Some(result_preview)); - } - "agent_failed" => { - failed.insert(agent_id.to_string()); - let error = parsed.get("error").and_then(|v| v.as_str()) - .or_else(|| parsed.get("result_preview").and_then(|v| v.as_str())) - .unwrap_or("unknown error"); - eprintln!("[team] {team_id}: FAIL {agent_name} failed - {error}"); - append_team_event(&events_path, &team_id, agent_id, "failed", agent_name, Some(error)); - } - _ => {} - } - } - } - } - } - - let all_done = completed.len() + failed.len() >= total; - if all_done { - break; - } - std::thread::sleep(std::time::Duration::from_secs(1)); - } - - eprintln!("[team] {team_id}: all agents finished - {}/{} completed, {}/{} failed", completed.len(), total, failed.len(), total); - append_team_event(&events_path, &team_id, "team", "finished", &team_id, Some(&format!("{}/{} completed", completed.len(), total))); - - let manifest_path = team_dir.join(format!("{team_id}.json")); - if let Ok(data) = std::fs::read_to_string(&manifest_path) { - if let Ok(mut parsed) = serde_json::from_str::(&data) { - if let Some(obj) = parsed.as_object_mut() { - obj.insert("status".to_string(), json!(if failed.is_empty() { "completed" } else { "completed_with_failures" })); - let _ = std::fs::write(&manifest_path, serde_json::to_string_pretty(&parsed).unwrap_or_default()); - } - } - } - - // Clean up inbox - let _ = std::fs::remove_dir_all(&inbox_dir); - }) - .ok(); -} - -fn get_agent_result_preview(store_dir: &std::path::Path, agent_id: &str) -> String { - let md_path = store_dir.join(format!("{agent_id}.md")); - if let Ok(content) = std::fs::read_to_string(&md_path) { - let lines: Vec<&str> = content.lines().collect(); - let start = lines.iter().position(|l| l.starts_with("## Result")).map(|i| i + 1).unwrap_or(0); - lines[start..].iter().take(5).cloned().collect::>().join(" ").chars().take(200).collect() - } else { - String::new() - } -} - -fn append_team_event(events_path: &std::path::Path, team_id: &str, agent_id: &str, event_type: &str, name: &str, detail: Option<&str>) { - let entry = json!({ - "timestamp": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_millis(), - "team_id": team_id, - "agent_id": agent_id, - "event": event_type, - "name": name, - "detail": detail, - }); - if let Ok(line) = serde_json::to_string(&entry) { - use std::io::Write; - if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(events_path) { - let _ = writeln!(file, "{line}"); - } - } -} - - fn run_team_delete(input: TeamDeleteInput) -> Result { - // Delete from disk-based team storage - let team_dir = agent_store_dir()?.join("teams"); - let manifest_path = team_dir.join(format!("{}.json", input.team_id)); - if !manifest_path.exists() { - return Err(format!("team not found: {}", input.team_id)); - } - let data = std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())?; - let parsed: serde_json::Value = serde_json::from_str(&data).map_err(|e| e.to_string())?; - let _ = std::fs::remove_file(&manifest_path); - // Also clean up the team inbox directory - let inbox_dir = agent_mailbox_dir().join("team").join(&input.team_id); - if inbox_dir.exists() { - let _ = std::fs::remove_dir_all(&inbox_dir); - } - // Also clean up task claims for this team - let claims_dir = claims_dir(); - if claims_dir.exists() { - if let Ok(entries) = std::fs::read_dir(&claims_dir) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - if path.extension().map_or(false, |e| e == "lock") { - if let Ok(content) = std::fs::read_to_string(&path) { - if let Ok(v) = serde_json::from_str::(&content) { - if v.get("team_id").and_then(|t| t.as_str()) == Some(&input.team_id) { - let _ = std::fs::remove_file(&path); - } - } - } - } - } - } - } - to_pretty_json(json!({ - "team_id": input.team_id, - "name": parsed.get("name").and_then(|v| v.as_str()).unwrap_or("unknown"), - "status": "deleted", - "message": "Team deleted, inbox cleaned, claims released" - })) -} - -#[allow(clippy::needless_pass_by_value)] -fn run_agent_message(input: AgentMessageInput) -> Result { - let mailbox_dir = agent_mailbox_dir(); - std::fs::create_dir_all(&mailbox_dir).map_err(|e| e.to_string())?; - - match input.action.as_str() { - "send" => { - let target = input.agent_id.as_deref().unwrap_or(""); - if target.is_empty() { - return Err("agent_id is required for send action".to_string()); - } - let msg = input.message.as_deref().unwrap_or(""); - if msg.is_empty() { - return Err("message is required for send action".to_string()); - } - let inbox_dir = mailbox_dir.join(target); - std::fs::create_dir_all(&inbox_dir).map_err(|e| e.to_string())?; - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let msg_file = inbox_dir.join(format!("msg-{ts}.json")); - let sender = std::env::var("CLAWD_AGENT_ID").unwrap_or_else(|_| "main".to_string()); - let entry = json!({ - "from": sender, - "message": msg, - "timestamp": ts, - }); - std::fs::write(&msg_file, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - to_pretty_json(json!({ - "action": "sent", - "to": target, - "timestamp": ts, - })) - } - "read" => { - let my_id = std::env::var("CLAWD_AGENT_ID").unwrap_or_else(|_| "main".to_string()); - let inbox_dir = mailbox_dir.join(&my_id); - if !inbox_dir.exists() { - return to_pretty_json(json!({ - "action": "read", - "messages": [], - "unread_count": 0, - })); - } - let mark_read = input.mark_read.unwrap_or(true); - let mut messages: Vec = Vec::new(); - let entries: Vec<_> = std::fs::read_dir(&inbox_dir) - .map_err(|e| e.to_string())? - .filter_map(|e| e.ok()) - .filter(|e| { - e.path().extension().is_some_and(|ext| ext == "json") - }) - .collect(); - for entry in &entries { - if let Ok(content) = std::fs::read_to_string(entry.path()) { - if let Ok(msg) = serde_json::from_str::(&content) { - messages.push(msg); - } - } - } - let unread_count = messages.len(); - if mark_read { - for entry in &entries { - let _ = std::fs::remove_file(entry.path()); - } - } - to_pretty_json(json!({ - "action": "read", - "messages": messages, - "unread_count": unread_count, - })) - } - "broadcast" => { - let team_id = input.team_id.as_deref().unwrap_or(""); - if team_id.is_empty() { - return Err("team_id is required for broadcast action".to_string()); - } - let msg = input.message.as_deref().unwrap_or(""); - if msg.is_empty() { - return Err("message is required for broadcast action".to_string()); - } - let team_dir = agent_store_dir()?.join("teams"); - std::fs::create_dir_all(&team_dir).map_err(|e| e.to_string())?; - let manifest_path = team_dir.join(format!("{team_id}.json")); - if !manifest_path.exists() { - return Err(format!("team {team_id} not found")); - } - let team_data: Value = serde_json::from_str( - &std::fs::read_to_string(&manifest_path).map_err(|e| e.to_string())? - ).map_err(|e| e.to_string())?; - let agent_ids = team_data.get("agent_ids") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - let sender = std::env::var("CLAWD_AGENT_ID").unwrap_or_else(|_| "main".to_string()); - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let mut sent_to: Vec = Vec::new(); - for id_val in agent_ids { - if let Some(id) = id_val.as_str() { - if id == sender { continue; } - let inbox_dir = mailbox_dir.join(id); - std::fs::create_dir_all(&inbox_dir).map_err(|e| e.to_string())?; - let msg_file = inbox_dir.join(format!("msg-{ts}-{sender}.json")); - let entry = json!({ - "from": sender, - "message": msg, - "timestamp": ts, - "team_id": team_id, - }); - std::fs::write(&msg_file, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - sent_to.push(id.to_string()); - } - } - to_pretty_json(json!({ - "action": "broadcast", - "team_id": team_id, - "sent_to": sent_to, - "timestamp": ts, - })) - } - other => Err(format!("unknown AgentMessage action: {other}")), - } -} - -#[allow(clippy::needless_pass_by_value)] -fn run_task_claim(input: TaskClaimInput) -> Result { - match input.action.as_str() { - "claim" => { - let task_id = input.task_id.as_deref().unwrap_or(""); - let agent_id = input.agent_id.as_deref().unwrap_or(""); - let team_id = input.team_id.as_deref().unwrap_or(""); - if task_id.is_empty() { - return Err("task_id is required for claim action".to_string()); - } - if agent_id.is_empty() { - return Err("agent_id is required for claim action".to_string()); - } - if team_id.is_empty() { - return Err("team_id is required for claim action".to_string()); - } - match claim_task(task_id, agent_id, team_id) { - Ok(true) => to_pretty_json(json!({ - "action": "claim", - "task_id": task_id, - "agent_id": agent_id, - "claimed": true, - })), - Ok(false) => to_pretty_json(json!({ - "action": "claim", - "task_id": task_id, - "claimed": false, - "reason": "task already claimed by another agent", - })), - Err(e) => Err(e), - } - } - "release" => { - let task_id = input.task_id.as_deref().unwrap_or(""); - if task_id.is_empty() { - return Err("task_id is required for release action".to_string()); - } - release_claim(task_id)?; - to_pretty_json(json!({ - "action": "release", - "task_id": task_id, - "released": true, - })) - } - "list" => { - let claims = list_claims(input.team_id.as_deref()); - to_pretty_json(json!({ - "action": "list", - "claims": claims, - "count": claims.len(), - })) - } - other => Err(format!("unknown TaskClaim action: {other}. Use claim, release, or list")), - } -} - -#[allow(clippy::needless_pass_by_value)] -fn run_agent_suggestion(input: AgentSuggestionInput) -> Result { - let suggestions_dir = agent_store_dir()?.join("suggestions"); - std::fs::create_dir_all(&suggestions_dir).map_err(|e| e.to_string())?; - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let agent_id = input.agent_id.unwrap_or_else(|| "unknown".to_string()); - let entry = json!({ - "category": input.category, - "suggestion": input.suggestion, - "team_id": input.team_id, - "agent_id": agent_id, - "timestamp": ts, - }); - let filename = format!("suggestion-{agent_id}-{ts}.json"); - let path = suggestions_dir.join(&filename); - std::fs::write(&path, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - to_pretty_json(json!({ - "action": "suggestion", - "file": filename, - "status": "pending_review", - })) -} - -#[allow(clippy::needless_pass_by_value)] -fn run_context_request(input: ContextRequestInput) -> Result { - let mut results = Vec::new(); - let mut files_read = 0u32; - let mut symbols_found = 0u32; - - // Read requested files - for file_path in &input.files { - let path = std::path::PathBuf::from(file_path); - if path.exists() { - match std::fs::read_to_string(&path) { - Ok(content) => { - let truncated = if content.len() > 10000 { - format!("{}... (truncated, {} bytes total)", &content[..10000], content.len()) - } else { - content - }; - results.push(json!({ - "type": "file", - "path": file_path, - "content": truncated, - })); - files_read += 1; - } - Err(e) => { - results.push(json!({ - "type": "file_error", - "path": file_path, - "error": e.to_string(), - })); - } - } - } else { - results.push(json!({ - "type": "file_not_found", - "path": file_path, - })); - } - } - - // Search for requested symbols using grep - for symbol in &input.symbols { - let cwd = std::env::current_dir().map_err(|e| e.to_string())?; - let output = std::process::Command::new("grep") - .args(["-rn", "--include=*.rs", "--include=*.ts", "--include=*.js", "--include=*.py", symbol, "."]) - .current_dir(&cwd) - .output() - .map_err(|e| e.to_string())?; - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let lines: Vec<&str> = stdout.lines().take(20).collect(); - if !lines.is_empty() { - results.push(json!({ - "type": "symbol_search", - "symbol": symbol, - "matches": lines, - })); - symbols_found += 1; - } - } - } - - to_pretty_json(json!({ - "action": "context_request", - "reason": input.reason, - "files_read": files_read, - "symbols_found": symbols_found, - "results": results, - "reminder": "You have a budget of 3 retrieval cycles. Be specific with your requests.", - })) -} - -fn expand_team_mode(mode: &str, base_prompt: &str, team_id: &str) -> Result, String> { - let n = match mode { - "1x" | "tiny" => 1, - "2x" | "small" => 2, - "3x" | "medium" => 3, - "4x" | "large" => 4, - "5x" | "xlarge" => 5, - "6x" | "mega" => 6, - other => return Err(format!("unknown team mode '{other}'. Use 1x-6x or tiny/small/medium/large/xlarge/mega")), - }; - let short_team_id = &team_id[team_id.len().saturating_sub(8)..]; - let roles: &[&str] = &["Explore", "Plan", "Verification"]; - let mut tasks = Vec::new(); - for role in roles { - for i in 0..n { - let prompt = format!("[{role} agent {}/{}] {base_prompt}", i + 1, n); - let description = format!("{role} agent {}/{}", i + 1, n); - let task_id = format!("{short_team_id}-{role}-{i}"); - tasks.push(json!({ - "prompt": prompt, - "description": description, - "subagent_type": role, - "task_id": task_id, - })); - } - } - // Add read-only Reviewer agents (1 per 3 builders, minimum 1) - let reviewer_count = std::cmp::max(1, (roles.len() * n) / 3); - for i in 0..reviewer_count { - let prompt = format!("[Reviewer {}/{}] Review the work of other agents. Read their output files, check code quality, identify issues, and report findings via AgentMessage. Only use read-only tools.", i + 1, reviewer_count); - let description = format!("Reviewer {}/{}", i + 1, reviewer_count); - let task_id = format!("{short_team_id}-Reviewer-{i}"); - tasks.push(json!({ - "prompt": prompt, - "description": description, - "subagent_type": "Reviewer", - "task_id": task_id, - })); - } - Ok(tasks) -} - -fn agent_mailbox_dir() -> std::path::PathBuf { - if let Ok(path) = std::env::var("CLAWD_AGENT_STORE") { - return std::path::PathBuf::from(path).join("mailbox"); - } - let cwd = std::env::current_dir().unwrap_or_default(); - if let Some(workspace_root) = cwd.ancestors().nth(2) { - return workspace_root.join(".clawd-agents").join("mailbox"); - } - cwd.join(".clawd-agents").join("mailbox") -} - -fn claims_dir() -> std::path::PathBuf { - if let Ok(path) = std::env::var("CLAWD_AGENT_STORE") { - return std::path::PathBuf::from(path).join("claims"); - } - let cwd = std::env::current_dir().unwrap_or_default(); - if let Some(workspace_root) = cwd.ancestors().nth(2) { - return workspace_root.join(".clawd-agents").join("claims"); - } - cwd.join(".clawd-agents").join("claims") -} - -fn claim_task(task_id: &str, agent_id: &str, team_id: &str) -> Result { - let dir = claims_dir(); - std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; - let lock_path = dir.join(format!("{task_id}.lock")); - if lock_path.exists() { - return Ok(false); - } - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let entry = serde_json::json!({ - "task_id": task_id, - "agent_id": agent_id, - "team_id": team_id, - "claimed_at": ts, - }); - // Atomic claim: write to temp file then rename - let tmp_path = dir.join(format!("{task_id}.lock.tmp.{agent_id}")); - std::fs::write(&tmp_path, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - match std::fs::rename(&tmp_path, &lock_path) { - Ok(()) => Ok(true), - Err(_) => { - // Another agent claimed it first - let _ = std::fs::remove_file(&tmp_path); - Ok(false) - } - } -} - -fn release_claim(task_id: &str) -> Result<(), String> { - let lock_path = claims_dir().join(format!("{task_id}.lock")); - if lock_path.exists() { - std::fs::remove_file(&lock_path).map_err(|e| e.to_string()) - } else { - Ok(()) - } -} - -fn is_task_claimed(task_id: &str) -> bool { - claims_dir().join(format!("{task_id}.lock")).exists() -} - -fn list_claims(team_id: Option<&str>) -> Vec { - let dir = claims_dir(); - let mut claims = Vec::new(); - if let Ok(entries) = std::fs::read_dir(&dir) { - for entry in entries.filter_map(|e| e.ok()) { - let path = entry.path(); - if path.extension().map_or(false, |e| e == "lock") { - if let Ok(content) = std::fs::read_to_string(&path) { - if let Ok(v) = serde_json::from_str::(&content) { - if team_id.map_or(true, |tid| v.get("team_id").map_or(false, |t| t == tid)) { - claims.push(v); - } - } - } - } - } - } - claims -} - -#[allow(dead_code)] -struct TeamInboxReporter { - team_id: String, - agent_id: String, - agent_name: String, - inbox_dir: std::path::PathBuf, -} - -impl TeamInboxReporter { - fn new(team_id: String, agent_id: String, agent_name: String) -> Self { - let inbox_dir = agent_mailbox_dir().join("team").join(&team_id); - let _ = std::fs::create_dir_all(&inbox_dir); - Self { team_id, agent_id, agent_name, inbox_dir } - } -} - -impl TurnProgressReporter for TeamInboxReporter { - fn on_tool_result( - &self, - iteration: usize, - max_iterations: usize, - tool_name: &str, - input: &str, - result: Result<&str, &str>, - ) { - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let (result_preview, is_error) = match result { - Ok(output) => (output.chars().take(500).collect::(), false), - Err(err) => (err.chars().take(500).collect::(), true), - }; - let input_preview: String = input.chars().take(300).collect(); - let entry = serde_json::json!({ - "event": "tool_progress", - "agent_id": self.agent_id, - "name": self.agent_name, - "tool_name": tool_name, - "input_preview": input_preview, - "result_preview": result_preview, - "is_error": is_error, - "iteration": iteration, - "max_iterations": max_iterations, - "timestamp": ts, - }); - let msg_file = self.inbox_dir.join(format!( - "tp-{}-{}-{ts}.json", - self.agent_id, iteration - )); - if let Ok(line) = serde_json::to_string(&entry) { - let _ = std::fs::write(&msg_file, line); - } - - // Periodic git commit (every 5 tool calls) to preserve progress - if iteration > 0 && iteration % 5 == 0 { - let _ = std::process::Command::new("git") - .args(["add", "-A"]) - .output(); - let diff_check = std::process::Command::new("git") - .args(["diff", "--cached", "--quiet"]) - .output(); - if diff_check.map_or(true, |o| !o.status.success()) { - let _ = std::process::Command::new("git") - .args(["commit", "-m", &format!("agent {} progress: iteration {iteration}", self.agent_id)]) - .output(); - } - } - - // Check for kill signal from team lead - for entry in std::fs::read_dir(&self.inbox_dir).unwrap_or_else(|_| std::fs::read_dir(".").unwrap()) { - if let Ok(e) = entry { - let name = e.file_name(); - let name_str = name.to_string_lossy(); - if name_str.starts_with(&format!("kill-{}-", self.agent_id)) { - // Kill signal received — panic to abort - std::fs::remove_file(e.path()).ok(); - panic!("agent {} received kill signal", self.agent_id); - } - } - } + match global_team_registry().delete(&input.team_id) { + Ok(team) => to_pretty_json(json!({ + "team_id": team.team_id, + "name": team.name, + "status": team.status, + "message": "Team deleted" + })), + Err(e) => Err(e), } } @@ -2956,18 +1857,7 @@ fn run_lsp(input: LspInput) -> Result { let character = input.character; let query = input.query.as_deref(); - // For code_action, pass end_line/end_character through the query param - // since dispatch() doesn't take them directly — encode as "end_line:end_character" - let effective_query = if input.action == "code_action" { - match (input.end_line, input.end_character) { - (Some(el), Some(ec)) => Some(format!("{el}:{ec}")), - _ => query.map(str::to_owned), - } - } else { - query.map(str::to_owned) - }; - - match registry.dispatch(action, path, line, character, effective_query.as_deref()) { + match registry.dispatch(action, path, line, character, query) { Ok(result) => to_pretty_json(result), Err(e) => to_pretty_json(json!({ "action": action, @@ -3142,7 +2032,6 @@ fn run_testing_permission(input: TestingPermissionInput) -> Result Result { @@ -3289,102 +2178,6 @@ fn run_git_blame(input: GitBlameInput) -> Result { })), None => Err(format!("git blame {} failed. Ensure the file exists and the directory is inside a git repository.", input.path)), } -======= -fn run_sub_agent(input: SubAgentInput) -> Result { - let task_type = input.task_type.as_deref().unwrap_or("Explore"); - let allowed_tools: BTreeSet = match task_type { - "Plan" => BTreeSet::from([ - "read_file".to_string(), - "glob_search".to_string(), - "grep_search".to_string(), - "ToolSearch".to_string(), - "TodoWrite".to_string(), - "StructuredOutput".to_string(), - ]), - "Verify" => BTreeSet::from([ - "bash".to_string(), - "read_file".to_string(), - "glob_search".to_string(), - "grep_search".to_string(), - "ToolSearch".to_string(), - "TodoWrite".to_string(), - "StructuredOutput".to_string(), - ]), - _ => BTreeSet::from([ - "read_file".to_string(), - "glob_search".to_string(), - "grep_search".to_string(), - "WebFetch".to_string(), - "WebSearch".to_string(), - "ToolSearch".to_string(), - "StructuredOutput".to_string(), - ]), - }; - - let model = input - .model - .or_else(|| load_subagent_model_from_config()) - .unwrap_or_else(|| DEFAULT_AGENT_MODEL.to_string()); - - let system_prompt = build_sub_agent_system_prompt(task_type) - .map_err(|e| format!("failed to build sub-agent system prompt: {e}"))?; - - let api_client = ProviderRuntimeClient::new(model.clone(), allowed_tools.clone()) - .map_err(|e| format!("failed to create sub-agent API client: {e}"))?; - let permission_policy = agent_permission_policy(); - let tool_executor = SubagentToolExecutor::new(allowed_tools) - .with_enforcer(PermissionEnforcer::new(permission_policy.clone())); - - let mut runtime = ConversationRuntime::new( - Session::new(), - api_client, - tool_executor, - permission_policy, - system_prompt, - ) - .with_max_iterations(DEFAULT_AGENT_MAX_ITERATIONS); - - let summary = runtime - .run_turn(input.prompt, None) - .map_err(|e| format!("sub-agent failed: {e}"))?; - - let result_text = final_assistant_text(&summary); - let tool_count: usize = summary - .assistant_messages - .iter() - .map(|m| m.blocks.iter().filter(|b| matches!(b, ContentBlock::ToolUse { .. })).count()) - .sum(); - - to_pretty_json(json!({ - "result": result_text, - "tool_calls": tool_count, - "iterations": summary.iterations, - })) -} - -fn load_subagent_model_from_config() -> Option { - std::env::current_dir().ok().and_then(|cwd| { - ConfigLoader::default_for(cwd) - .load() - .ok() - .and_then(|config| config.subagent_model().map(|m| m.to_string())) - }) -} - -fn build_sub_agent_system_prompt(task_type: &str) -> Result, String> { - let cwd = std::env::current_dir().map_err(|error| error.to_string())?; - let mut prompt = load_system_prompt( - cwd, - DEFAULT_AGENT_SYSTEM_DATE.to_string(), - std::env::consts::OS, - "unknown", - ) - .map_err(|error| error.to_string())?; - prompt.push(format!( - "You are a fast sub-agent performing a {task_type} task. Work autonomously, use available tools efficiently, do not ask questions. Return a concise summary of your findings." - )); - Ok(prompt) ->>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation) } fn from_value Deserialize<'de>>(input: &Value) -> Result { @@ -3655,48 +2448,23 @@ fn branch_divergence_output( #[allow(clippy::needless_pass_by_value)] fn run_read_file(input: ReadFileInput) -> Result { -<<<<<<< HEAD let workspace = std::env::current_dir().map_err(|error| error.to_string())?; to_pretty_json( read_file_in_workspace(&input.path, input.offset, input.limit, &workspace) .map_err(io_to_string)?, ) -======= - let result = read_file(&input.path, input.offset, input.limit).map_err(io_to_string)?; - let mut output = to_pretty_json(result)?; - - // LSP enrichment: notify the server that the file was opened and append diagnostics - if let Some(diags) = lsp_enrichment_for_path(&input.path, &LspEvent::Open) { - output.push_str(&format_diagnostic_appendix(&diags)); - } - - Ok(output) ->>>>>>> 73cc8277 (feat: auto-LSP integration with didOpen/didChange and diagnostic enrichment) } #[allow(clippy::needless_pass_by_value)] fn run_write_file(input: WriteFileInput) -> Result { -<<<<<<< HEAD let workspace = std::env::current_dir().map_err(|error| error.to_string())?; to_pretty_json( write_file_in_workspace(&input.path, &input.content, &workspace).map_err(io_to_string)?, ) -======= - let result = write_file(&input.path, &input.content).map_err(io_to_string)?; - let mut output = to_pretty_json(result)?; - - // LSP enrichment: notify the server that the file changed and append diagnostics - if let Some(diags) = lsp_enrichment_for_path(&input.path, &LspEvent::Change) { - output.push_str(&format_diagnostic_appendix(&diags)); - } - - Ok(output) ->>>>>>> 73cc8277 (feat: auto-LSP integration with didOpen/didChange and diagnostic enrichment) } #[allow(clippy::needless_pass_by_value)] fn run_edit_file(input: EditFileInput) -> Result { -<<<<<<< HEAD let workspace = std::env::current_dir().map_err(|error| error.to_string())?; to_pretty_json( edit_file_in_workspace( @@ -3707,74 +2475,7 @@ fn run_edit_file(input: EditFileInput) -> Result { &workspace, ) .map_err(io_to_string)?, -======= - let result = edit_file( - &input.path, - &input.old_string, - &input.new_string, - input.replace_all.unwrap_or(false), ->>>>>>> 73cc8277 (feat: auto-LSP integration with didOpen/didChange and diagnostic enrichment) ) - .map_err(io_to_string)?; - - let mut output = to_pretty_json(result)?; - - // LSP enrichment: notify the server that the file changed and append diagnostics - let full_content = std::fs::read_to_string(&input.path).unwrap_or_default(); - if let Some(diags) = - lsp_enrichment_for_path_with_content(&input.path, &full_content, &LspEvent::Change) - { - output.push_str(&format_diagnostic_appendix(&diags)); - } - - Ok(output) -} - -enum LspEvent { - Open, - Change, -} - -fn lsp_enrichment_for_path(path: &str, event: &LspEvent) -> Option> { - let content = std::fs::read_to_string(path).ok()?; - lsp_enrichment_for_path_with_content(path, &content, event) -} - -fn lsp_enrichment_for_path_with_content( - path: &str, - content: &str, - event: &LspEvent, -) -> Option> { - let registry = global_lsp_registry(); - - registry.find_server_for_path(path)?; - - let diags = match event { - LspEvent::Open => registry.notify_file_open(path, content), - LspEvent::Change => registry.notify_file_change(path, content), - }; - - if diags.is_empty() { - None - } else { - Some(diags) - } -} - -fn format_diagnostic_appendix(diagnostics: &[LspDiagnostic]) -> String { - let mut lines = vec![String::from("\n--- LSP Diagnostics ---")]; - for d in diagnostics { - let source = d.source.as_deref().unwrap_or("lsp"); - lines.push(format!( - "[{}:{}] {} ({}): {}", - d.line + 1, - d.character + 1, - d.severity, - source, - d.message - )); - } - lines.join("\n") } #[allow(clippy::needless_pass_by_value)] @@ -4134,10 +2835,6 @@ struct AgentInput { subagent_type: Option, name: Option, model: Option, - #[serde(default)] - team_id: Option, - #[serde(default)] - task_id: Option, } #[derive(Debug, Deserialize)] @@ -4298,11 +2995,6 @@ const fn default_auto_recover_prompt_misdelivery() -> bool { #[derive(Debug, Deserialize)] struct TeamCreateInput { name: String, - #[serde(default)] - mode: Option, - #[serde(default)] - prompt: Option, - #[serde(default)] tasks: Vec, } @@ -4311,60 +3003,6 @@ struct TeamDeleteInput { team_id: String, } -#[derive(Debug, Deserialize)] -struct AgentMessageInput { - action: String, - #[serde(default)] - agent_id: Option, - #[serde(default)] - team_id: Option, - #[serde(default)] - message: Option, - #[serde(default)] - mark_read: Option, -} - -#[derive(Debug, Deserialize)] -struct TeamStatusInput { - team_id: String, - #[serde(default)] - action: Option, - #[serde(default)] - agent_id: Option, - #[serde(default)] - reason: Option, -} - -#[derive(Debug, Deserialize)] -struct TaskClaimInput { - action: String, - #[serde(default)] - task_id: Option, - #[serde(default)] - team_id: Option, - #[serde(default)] - agent_id: Option, -} - -#[derive(Debug, Deserialize)] -struct AgentSuggestionInput { - category: String, - suggestion: String, - #[serde(default)] - team_id: Option, - #[serde(default)] - agent_id: Option, -} - -#[derive(Debug, Deserialize)] -struct ContextRequestInput { - #[serde(default)] - files: Vec, - #[serde(default)] - symbols: Vec, - reason: String, -} - #[derive(Debug, Deserialize)] struct CronCreateInput { schedule: String, @@ -4388,10 +3026,6 @@ struct LspInput { #[serde(default)] character: Option, #[serde(default)] - end_line: Option, - #[serde(default)] - end_character: Option, - #[serde(default)] query: Option, } @@ -4432,7 +3066,6 @@ struct TestingPermissionInput { action: String, } -<<<<<<< HEAD /// Input for the GitStatus tool: shows working tree status. /// Defaults to --short --branch mode for concise, parseable output. #[derive(Debug, Deserialize)] @@ -4513,15 +3146,6 @@ struct GitBlameInput { #[serde(default)] /// End of line range (1-based). Only used if start_line is also set. end_line: Option, -======= -#[derive(Debug, Deserialize)] -struct SubAgentInput { - prompt: String, - #[serde(default)] - task_type: Option, - #[serde(default)] - model: Option, ->>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation) } #[derive(Debug, Serialize)] @@ -4591,10 +3215,6 @@ struct AgentOutput { derived_state: String, #[serde(skip_serializing_if = "Option::is_none")] error: Option, - #[serde(rename = "teamId", skip_serializing_if = "Option::is_none")] - team_id: Option, - #[serde(rename = "taskId", skip_serializing_if = "Option::is_none")] - task_id: Option, } #[derive(Debug, Clone)] @@ -4603,9 +3223,6 @@ struct AgentJob { prompt: String, system_prompt: Vec, allowed_tools: BTreeSet, - team_id: Option, - task_id: Option, - max_tokens: Option, } #[derive(Debug, Clone, Serialize, PartialEq, Eq)] @@ -5502,7 +4119,6 @@ where let created_at = iso8601_now(); let system_prompt = build_agent_system_prompt(&normalized_subagent_type, &model)?; let allowed_tools = allowed_tools_for_subagent(&normalized_subagent_type); - let team_id = input.team_id.clone(); let output_contents = format!( "# Agent Task @@ -5537,8 +4153,6 @@ where current_blocker: None, derived_state: String::from("working"), error: None, - team_id: input.team_id.clone(), - task_id: input.task_id.clone(), }; write_agent_manifest(&manifest)?; @@ -5548,9 +4162,6 @@ where prompt: input.prompt, system_prompt, allowed_tools, - team_id: input.team_id.clone(), - task_id: input.task_id, - max_tokens: None, }; if let Err(error) = spawn_fn(job) { let error = format!("failed to spawn sub-agent: {error}"); @@ -5563,28 +4174,18 @@ where fn spawn_agent_job(job: AgentJob) -> Result<(), String> { let thread_name = format!("clawd-agent-{}", job.manifest.agent_id); - let agent_id_for_env = job.manifest.agent_id.clone(); std::thread::Builder::new() .name(thread_name) .spawn(move || { - std::env::set_var("CLAWD_AGENT_ID", &agent_id_for_env); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| run_agent_job(&job))); match result { Ok(Ok(())) => {} Ok(Err(error)) => { - // Release task claim on failure - if let Some(ref task_id) = job.task_id { - let _ = release_claim(task_id); - } let _ = persist_agent_terminal_state(&job.manifest, "failed", None, Some(error)); } Err(_) => { - // Release task claim on panic - if let Some(ref task_id) = job.task_id { - let _ = release_claim(task_id); - } let _ = persist_agent_terminal_state( &job.manifest, "failed", @@ -5598,119 +4199,11 @@ fn spawn_agent_job(job: AgentJob) -> Result<(), String> { .map_err(|error| error.to_string()) } -fn setup_agent_worktree(agent_id: &str) -> Result { - let cwd = std::env::current_dir().map_err(|e| e.to_string())?; - // Check if we're in a git repo - let git_check = std::process::Command::new("git") - .args(["rev-parse", "--is-inside-work-tree"]) - .current_dir(&cwd) - .output() - .map_err(|e| e.to_string())?; - if !git_check.status.success() { - return Ok(cwd); // Not a git repo, work in current dir - } - let worktree_dir = cwd.join(".clawd-agents").join("worktrees").join(agent_id); - // Create worktree on a new branch - let branch_name = format!("agent/{agent_id}"); - let output = std::process::Command::new("git") - .args(["worktree", "add", worktree_dir.to_str().unwrap_or(""), "-b", &branch_name]) - .current_dir(&cwd) - .output() - .map_err(|e| e.to_string())?; - if !output.status.success() { - // Branch might already exist, try with existing branch - let output2 = std::process::Command::new("git") - .args(["worktree", "add", worktree_dir.to_str().unwrap_or(""), &branch_name]) - .current_dir(&cwd) - .output() - .map_err(|e| e.to_string())?; - if !output2.status.success() { - // Fall back to working in current directory - return Ok(cwd); - } - } - Ok(worktree_dir) -} - -fn teardown_agent_worktree(agent_id: &str, worktree_path: &std::path::Path) -> Result<(), String> { - let cwd = std::env::current_dir().map_err(|e| e.to_string())?; - // Stage and commit any changes in the worktree - let _ = std::process::Command::new("git") - .args(["add", "-A"]) - .current_dir(worktree_path) - .output(); - let commit_check = std::process::Command::new("git") - .args(["diff", "--cached", "--quiet"]) - .current_dir(worktree_path) - .output(); - if commit_check.map_or(true, |o| !o.status.success()) { - let _ = std::process::Command::new("git") - .args(["commit", "-m", &format!("agent {agent_id}: completed task")]) - .current_dir(worktree_path) - .output(); - } - // Remove the worktree - let _ = std::process::Command::new("git") - .args(["worktree", "remove", worktree_path.to_str().unwrap_or(""), "--force"]) - .current_dir(&cwd) - .output(); - // Delete the branch - let _ = std::process::Command::new("git") - .args(["branch", "-D", &format!("agent/{agent_id}")]) - .current_dir(&cwd) - .output(); - Ok(()) -} - fn run_agent_job(job: &AgentJob) -> Result<(), String> { - // Claim task if task_id is set (prevents duplicate work) - if let Some(ref task_id) = job.task_id { - if let Some(ref team_id) = job.team_id { - let claimed = claim_task(task_id, &job.manifest.agent_id, team_id) - .unwrap_or(false); - if !claimed { - return Err(format!("task {task_id} already claimed by another agent")); - } - } - } - - // Save and fix CLAWD_AGENT_STORE to prevent doubled paths when CWD changes - // between thread spawn and execution - if let Ok(store) = agent_store_dir() { - std::env::set_var("CLAWD_AGENT_STORE", store); - } - let mut runtime = build_agent_runtime(job)?.with_max_iterations(DEFAULT_AGENT_MAX_ITERATIONS); - - // Set auto-compaction threshold based on model's context window - if let Some(ref model) = job.manifest.model { - if let Some(limit) = model_token_limit(model) { - // Compact at 70% of context window to stay safely under the limit - let threshold = (limit.context_window_tokens as f32 * 0.7) as u32; - runtime = runtime.with_auto_compaction_input_tokens_threshold(threshold); - } - } - - // Attach TeamInboxReporter for per-tool-call progress when agent belongs to a team - if let Some(ref team_id) = job.team_id { - let reporter = TeamInboxReporter::new( - team_id.clone(), - job.manifest.agent_id.clone(), - job.manifest.name.clone(), - ); - runtime = runtime.with_turn_progress_reporter(Box::new(reporter)); - } - - let result = runtime + let summary = runtime .run_turn(job.prompt.clone(), None) - .map_err(|error| error.to_string()); - - // Release task claim on completion (success or failure) - if let Some(ref task_id) = job.task_id { - let _ = release_claim(task_id); - } - - let summary = result?; + .map_err(|error| error.to_string())?; let final_text = final_assistant_text(&summary); persist_agent_terminal_state(&job.manifest, "completed", Some(final_text.as_str()), None) } @@ -5739,7 +4232,6 @@ fn build_agent_runtime( fn build_agent_system_prompt(subagent_type: &str, model: &str) -> Result, String> { let cwd = std::env::current_dir().map_err(|error| error.to_string())?; - let agents_md = cwd.join("AGENTS.md"); let mut prompt = load_system_prompt( cwd, DEFAULT_AGENT_SYSTEM_DATE.to_string(), @@ -5751,54 +4243,15 @@ fn build_agent_system_prompt(subagent_type: &str, model: &str) -> Result) -> String { - if let Some(m) = model.map(str::trim).filter(|m| !m.is_empty()) { - eprintln!("[agent] resolve_agent_model: using explicit model={m}"); - return m.to_string(); - } - if let Some(fast) = load_subagent_model_from_config() { - eprintln!("[agent] resolve_agent_model: using subagentModel from config={fast}"); - return fast; - } - eprintln!("[agent] resolve_agent_model: falling back to DEFAULT_AGENT_MODEL={DEFAULT_AGENT_MODEL}"); - DEFAULT_AGENT_MODEL.to_string() -} - -fn load_subagent_model_from_config() -> Option { - let cwd = match std::env::current_dir() { - Ok(p) => p, - Err(e) => { - eprintln!("[agent] load_subagent_model_from_config: current_dir() failed: {e}"); - return None; - } - }; - match ConfigLoader::default_for(&cwd).load() { - Ok(config) => { - let result = config.subagent_model().map(|m| m.to_string()); - eprintln!("[agent] load_subagent_model_from_config: cwd={} subagent_model={result:?}", cwd.display()); - result - } - Err(e) => { - eprintln!("[agent] load_subagent_model_from_config: ConfigLoader::load() failed: {e}"); - None - } - } + model + .map(str::trim) + .filter(|model| !model.is_empty()) + .unwrap_or(DEFAULT_AGENT_MODEL) + .to_string() } fn allowed_tools_for_subagent(subagent_type: &str) -> BTreeSet { @@ -5811,11 +4264,6 @@ fn allowed_tools_for_subagent(subagent_type: &str) -> BTreeSet { "WebSearch", "ToolSearch", "Skill", - "AgentMessage", - "TaskClaim", - "AgentSuggestion", - "ContextRequest", - "TeamStatus", "StructuredOutput", ], "Plan" => vec![ @@ -5827,11 +4275,6 @@ fn allowed_tools_for_subagent(subagent_type: &str) -> BTreeSet { "ToolSearch", "Skill", "TodoWrite", - "AgentMessage", - "TaskClaim", - "AgentSuggestion", - "ContextRequest", - "TeamStatus", "StructuredOutput", "SendUserMessage", ], @@ -5844,29 +4287,10 @@ fn allowed_tools_for_subagent(subagent_type: &str) -> BTreeSet { "WebSearch", "ToolSearch", "TodoWrite", - "AgentMessage", - "TaskClaim", - "AgentSuggestion", - "ContextRequest", - "TeamStatus", "StructuredOutput", "SendUserMessage", "PowerShell", ], - "Reviewer" => vec![ - "read_file", - "glob_search", - "grep_search", - "WebFetch", - "WebSearch", - "ToolSearch", - "AgentMessage", - "TaskClaim", - "AgentSuggestion", - "ContextRequest", - "TeamStatus", - "StructuredOutput", - ], "claw-guide" => vec![ "read_file", "glob_search", @@ -5901,11 +4325,6 @@ fn allowed_tools_for_subagent(subagent_type: &str) -> BTreeSet { "ToolSearch", "NotebookEdit", "Sleep", - "AgentMessage", - "TaskClaim", - "AgentSuggestion", - "ContextRequest", - "TeamStatus", "SendUserMessage", "Config", "StructuredOutput", @@ -5976,41 +4395,7 @@ fn persist_agent_terminal_state( )); } } - write_agent_manifest(&next_manifest)?; - - // If this agent belongs to a team, post completion to the team inbox - if let Some(ref tid) = next_manifest.team_id { - let _ = post_agent_completion_to_team_inbox(&next_manifest, tid, status, result); - } - - Ok(()) -} - -fn post_agent_completion_to_team_inbox( - manifest: &AgentOutput, - team_id: &str, - status: &str, - result: Option<&str>, -) -> Result<(), String> { - let mailbox_dir = agent_mailbox_dir().join("team").join(team_id); - std::fs::create_dir_all(&mailbox_dir).map_err(|e| e.to_string())?; - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis(); - let entry = json!({ - "event": format!("agent_{status}"), - "agent_id": manifest.agent_id, - "name": manifest.name, - "subagent_type": manifest.subagent_type, - "status": status, - "result_preview": result.map(|r| r.chars().take(2000).collect::()), - "error": if status == "failed" { result.map(|r| r.chars().take(500).collect::()) } else { None:: }, - "timestamp": ts, - }); - let msg_file = mailbox_dir.join(format!("{}-{ts}.json", manifest.agent_id)); - std::fs::write(&msg_file, serde_json::to_string_pretty(&entry).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string()) + write_agent_manifest(&next_manifest) } const MIN_LANE_SUMMARY_WORDS: usize = 7; @@ -7330,7 +5715,6 @@ fn normalize_subagent_type(subagent_type: Option<&str>) -> String { "verification" | "verificationagent" | "verify" | "verifier" => { String::from("Verification") } - "reviewer" | "review" | "reviewagent" => String::from("Reviewer"), "clawguide" | "clawguideagent" | "guide" => String::from("claw-guide"), "statusline" | "statuslinesetup" => String::from("statusline-setup"), _ => trimmed.to_string(), @@ -10315,8 +8699,6 @@ mod tests { subagent_type: Some("Explore".to_string()), name: Some("ship-audit".to_string()), model: None, - team_id: None, - task_id: None, }, move |job| { *captured_for_spawn @@ -10398,8 +8780,6 @@ mod tests { subagent_type: Some("Explore".to_string()), name: Some("complete-task".to_string()), model: Some("claude-sonnet-4-6".to_string()), - team_id: None, - task_id: None, }, |job| { persist_agent_terminal_state( @@ -10457,8 +8837,6 @@ mod tests { subagent_type: Some("Verification".to_string()), name: Some("fail-task".to_string()), model: None, - team_id: None, - task_id: None, }, |job| { persist_agent_terminal_state( @@ -10506,8 +8884,6 @@ mod tests { subagent_type: Some("Explore".to_string()), name: Some("summary-floor".to_string()), model: None, - team_id: None, - task_id: None, }, |job| { persist_agent_terminal_state( @@ -10553,8 +8929,6 @@ mod tests { subagent_type: Some("Explore".to_string()), name: Some("recovery-lane".to_string()), model: None, - team_id: None, - task_id: None, }, |job| { persist_agent_terminal_state( @@ -10603,8 +8977,6 @@ mod tests { subagent_type: Some("Verification".to_string()), name: Some("review-lane".to_string()), model: None, - team_id: None, - task_id: None, }, |job| { persist_agent_terminal_state( @@ -10645,8 +9017,6 @@ mod tests { subagent_type: Some("Explore".to_string()), name: Some("backlog-scan".to_string()), model: None, - team_id: None, - task_id: None, }, |job| { persist_agent_terminal_state( @@ -10693,8 +9063,6 @@ mod tests { subagent_type: Some("Explore".to_string()), name: Some("artifact-lane".to_string()), model: None, - team_id: None, - task_id: None, }, |job| { persist_agent_terminal_state( @@ -10765,8 +9133,6 @@ mod tests { subagent_type: Some("Explore".to_string()), name: Some("cron-closeout".to_string()), model: None, - team_id: None, - task_id: None, }, |job| { persist_agent_terminal_state( @@ -10808,8 +9174,6 @@ mod tests { subagent_type: None, name: Some("spawn-error".to_string()), model: None, - team_id: None, - task_id: None, }, |_| Err(String::from("thread creation failed")), )