fix: resolve rebase conflicts, clean TUI alignment, and sync with upstream

- Split multi-line user input and system messages on newlines so each
  visual line gets its own ConversationLine (same pattern push_output
  already used). This fixes text misalignment when Enter is pressed.
- Don't enable Wrap on the conversation Paragraph — the FIFO viewport
  counts Line items to fill the pane, so soft-wrapping would throw off
  the row count and misalign content.
- Restore non-TUI files from upstream/main to clear conflict artifacts.
- Fix clippy warnings in lsp_client, lsp_process/parse, and trident.
- Add HandlerSwap and TeamToggle match arms in main.rs ReadOutcome.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
TheArchitectit 2026-06-11 10:03:16 -05:00
parent 2971728abb
commit 28155a331f
22 changed files with 510 additions and 3577 deletions

255
rust/Cargo.lock generated Executable file → Normal file
View File

@ -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"

View File

@ -135,10 +135,6 @@ impl ApiError {
/// Return the `Retry-After` delay if this error came from a 429 response
/// that included a `retry-after` header. Callers should prefer this value
/// over the computed backoff delay when it exists.
<<<<<<< HEAD
=======
#[must_use]
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
pub fn retry_after(&self) -> Option<Duration> {
match self {
Self::Api { retry_after, .. } => *retry_after,

View File

@ -132,20 +132,20 @@ pub fn build_http_client() -> Result<reqwest::Client, ApiError> {
#[must_use]
pub fn build_http_client_or_default() -> reqwest::Client {
build_http_client_with_opts(&ProxyConfig::from_env(), &TimeoutConfig::from_env())
<<<<<<< HEAD
.unwrap_or_else(|_| {
reqwest::Client::builder()
.user_agent("clawd-rust-tools/0.1")
.build()
.expect("default client with user_agent should always succeed")
})
=======
.unwrap_or_else(|_| reqwest::Client::new())
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
}
/// Build a `reqwest::Client` from an explicit [`ProxyConfig`]. Used by tests
/// and by callers that want to override process-level environment lookups.
///
/// When `config.proxy_url` is set it overrides the per-scheme `http_proxy`
/// and `https_proxy` fields and is registered as both an HTTP and HTTPS
/// proxy so a single value can route every outbound request.
pub fn build_http_client_with(config: &ProxyConfig) -> Result<reqwest::Client, ApiError> {
build_http_client_with_opts(config, &TimeoutConfig::from_env())
}
@ -158,10 +158,7 @@ pub fn build_http_client_with_opts(
) -> Result<reqwest::Client, ApiError> {
let mut builder = reqwest::Client::builder()
.no_proxy()
<<<<<<< HEAD
.user_agent("clawd-rust-tools/0.1")
=======
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
.connect_timeout(timeout.connect_timeout)
.timeout(timeout.request_timeout);

View File

@ -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::{

View File

@ -467,12 +467,8 @@ impl AnthropicClient {
break;
}
<<<<<<< HEAD
let delay = if let Some(retry_after) = last_error.as_ref().and_then(|e| e.retry_after())
{
=======
let delay = if let Some(retry_after) = last_error.as_ref().and_then(|e| e.retry_after()) {
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
retry_after
} else {
self.jittered_backoff_for_attempt(attempts)?
@ -760,7 +756,11 @@ fn now_unix_timestamp() -> u64 {
}
fn read_env_non_empty(key: &str) -> Result<Option<String>, ApiError> {
super::read_env_or_config(key)
match std::env::var(key) {
Ok(value) if !value.is_empty() => Ok(Some(value)),
Ok(_) | Err(std::env::VarError::NotPresent) => Ok(super::dotenv_value(key)),
Err(error) => Err(ApiError::from(error)),
}
}
#[cfg(test)]
@ -781,10 +781,7 @@ fn read_auth_token() -> Option<String> {
#[must_use]
pub fn read_base_url() -> String {
super::read_env_or_config("ANTHROPIC_BASE_URL")
.ok()
.flatten()
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string())
std::env::var("ANTHROPIC_BASE_URL").unwrap_or_else(|_| DEFAULT_BASE_URL.to_string())
}
fn request_id_from_headers(headers: &reqwest::header::HeaderMap) -> Option<String> {
@ -892,7 +889,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
let request_id = request_id_from_headers(&headers);
let body = response.text().await.unwrap_or_else(|_| String::new());
let parsed_error = serde_json::from_str::<AnthropicErrorEnvelope>(&body).ok();
let retryable = is_retryable_status(status) || is_retryable_400(status, &body);
let retryable = is_retryable_status(status);
let retry_after = parse_retry_after(&headers, status);
Err(ApiError::Api {
@ -911,14 +908,10 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
})
}
<<<<<<< HEAD
fn parse_retry_after(
headers: &reqwest::header::HeaderMap,
status: reqwest::StatusCode,
) -> Option<std::time::Duration> {
=======
fn parse_retry_after(headers: &reqwest::header::HeaderMap, status: reqwest::StatusCode) -> Option<std::time::Duration> {
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
if status != reqwest::StatusCode::TOO_MANY_REQUESTS {
return None;
}
@ -943,15 +936,10 @@ fn is_retryable_400(status: reqwest::StatusCode, body: &str) -> bool {
return false;
}
let lowered = body.to_ascii_lowercase();
// Gateway/proxy flakes that return 400 with transient error bodies
lowered.contains("no parseable body")
|| lowered.contains("connection reset")
|| lowered.contains("broken pipe")
|| lowered.contains("empty reply from server")
// Anthropic sometimes returns 400 invalid_request_error when their
// backend flakes — the body contains "no parseable body" in the
// message field of the JSON error envelope.
|| (lowered.contains("invalid_request_error") && lowered.contains("no parseable body"))
}
/// Anthropic API keys (`sk-ant-*`) are accepted over the `x-api-key` header

View File

@ -391,65 +391,9 @@ pub fn detect_provider_kind(model: &str) -> ProviderKind {
if std::env::var_os("OPENAI_BASE_URL").is_some() {
return ProviderKind::OpenAi;
}
// Fallback: check stored provider config from setup wizard.
if let Some(kind) = stored_provider_kind() {
return kind;
}
ProviderKind::Anthropic
}
/// Look up a stored provider config value by env var name.
/// Returns the stored API key or base URL when the env var matches the
/// configured provider kind, enabling the setup wizard to persist credentials
/// that work without shell env vars.
pub fn provider_config_value(key: &str) -> Option<String> {
let cwd = std::env::current_dir().ok()?;
let config = runtime::ConfigLoader::default_for(&cwd).load().ok()?;
let provider = config.provider();
let kind = provider.kind()?;
match (key, kind) {
("ANTHROPIC_API_KEY" | "ANTHROPIC_AUTH_TOKEN", "anthropic")
| ("XAI_API_KEY", "xai")
| ("OPENAI_API_KEY", "openai")
| ("DASHSCOPE_API_KEY", "dashscope") => provider.api_key().map(ToOwned::to_owned),
("ANTHROPIC_BASE_URL", "anthropic")
| ("XAI_BASE_URL", "xai")
| ("OPENAI_BASE_URL", "openai")
| ("DASHSCOPE_BASE_URL", "dashscope") => provider.base_url().map(ToOwned::to_owned),
_ => None,
}
}
/// Read an env var with a 3-tier fallback: process env -> .env file -> stored config.
/// Environment variables always take priority over stored settings.
pub fn read_env_or_config(key: &str) -> Result<Option<String>, ApiError> {
match std::env::var(key) {
Ok(value) if !value.is_empty() => return Ok(Some(value)),
Ok(_) | Err(std::env::VarError::NotPresent) => {}
Err(error) => return Err(ApiError::from(error)),
}
if let Some(value) = dotenv_value(key) {
return Ok(Some(value));
}
if let Some(value) = provider_config_value(key) {
return Ok(Some(value));
}
Ok(None)
}
/// Return the stored `ProviderKind` from config, if set.
fn stored_provider_kind() -> Option<ProviderKind> {
let cwd = std::env::current_dir().ok()?;
let config = runtime::ConfigLoader::default_for(&cwd).load().ok()?;
let kind = config.provider().kind()?;
match kind {
"anthropic" => Some(ProviderKind::Anthropic),
"xai" => Some(ProviderKind::Xai),
"openai" => Some(ProviderKind::OpenAi),
_ => None,
}
}
#[must_use]
pub const fn model_family_identity_for_kind(kind: ProviderKind) -> runtime::ModelFamilyIdentity {
match kind {
@ -722,7 +666,6 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
max_output_tokens: 16_384,
context_window_tokens: 256_000,
}),
<<<<<<< HEAD
"qwen-max" => Some(ModelTokenLimit {
max_output_tokens: 8_192,
context_window_tokens: 131_072,
@ -732,23 +675,6 @@ pub fn model_token_limit(model: &str) -> Option<ModelTokenLimit> {
context_window_tokens: 131_072,
}),
_ => None,
=======
// Qwen models via DashScope / OpenAI-compat
"qwen3.6-35b-fast" | "qwen3-235b-a22b" | "qwen-max" | "qwen-plus" | "qwen-turbo" | "qwen-qwq" => Some(ModelTokenLimit {
max_output_tokens: 16_384,
context_window_tokens: 131_072,
}),
"glm-5.1-fast" => Some(ModelTokenLimit {
max_output_tokens: 16_384,
context_window_tokens: 200_000,
}),
// Generic fallback for any model: assume 128K context, 8K output
// This prevents the "unknown model → no limit check → context overflow" bug
_ => Some(ModelTokenLimit {
max_output_tokens: 8_192,
context_window_tokens: 131_072,
}),
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
}
}

View File

@ -1697,7 +1697,11 @@ fn parse_sse_frame(
}
fn read_env_non_empty(key: &str) -> Result<Option<String>, ApiError> {
super::read_env_or_config(key)
match std::env::var(key) {
Ok(value) if !value.is_empty() => Ok(Some(value)),
Ok(_) | Err(std::env::VarError::NotPresent) => Ok(super::dotenv_value(key)),
Err(error) => Err(ApiError::from(error)),
}
}
#[must_use]
@ -1710,10 +1714,7 @@ pub fn has_api_key(key: &str) -> bool {
#[must_use]
pub fn read_base_url(config: OpenAiCompatConfig) -> String {
super::read_env_or_config(config.base_url_env)
.ok()
.flatten()
.unwrap_or_else(|| config.default_base_url.to_string())
std::env::var(config.base_url_env).unwrap_or_else(|_| config.default_base_url.to_string())
}
fn chat_completions_endpoint(base_url: &str) -> String {
@ -1743,7 +1744,7 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
let request_id = request_id_from_headers(&headers);
let body = response.text().await.unwrap_or_default();
let parsed_error = serde_json::from_str::<ErrorEnvelope>(&body).ok();
let retryable = is_retryable_status(status) || is_retryable_400(status, &body);
let retryable = is_retryable_status(status);
let retry_after = parse_retry_after(&headers, status);
let suggested_action = suggested_action_for_status(status);
@ -1764,14 +1765,10 @@ async fn expect_success(response: reqwest::Response) -> Result<reqwest::Response
})
}
<<<<<<< HEAD
fn parse_retry_after(
headers: &reqwest::header::HeaderMap,
status: reqwest::StatusCode,
) -> Option<std::time::Duration> {
=======
fn parse_retry_after(headers: &reqwest::header::HeaderMap, status: reqwest::StatusCode) -> Option<std::time::Duration> {
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
if status != reqwest::StatusCode::TOO_MANY_REQUESTS {
return None;
}

View File

@ -1042,13 +1042,6 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
argument_hint: None,
resume_supported: true,
},
SlashCommandSpec {
name: "lsp",
aliases: &[],
summary: "Show or manage LSP server status",
argument_hint: Some("[start|stop|restart <language>]"),
resume_supported: true,
},
];
#[derive(Debug, Clone, PartialEq, Eq)]
@ -1195,13 +1188,6 @@ pub enum SlashCommand {
History {
count: Option<String>,
},
Lsp {
action: Option<String>,
target: Option<String>,
},
Team {
action: Option<String>,
},
Unknown(String),
Team {
action: Option<String>,
@ -1292,11 +1278,6 @@ impl SlashCommand {
Self::Theme { .. } => "/theme",
Self::Voice { .. } => "/voice",
Self::Usage { .. } => "/usage",
<<<<<<< HEAD
=======
Self::Team { .. } => "/team",
Self::Setup => "/setup",
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
Self::Rename { .. } => "/rename",
Self::Copy { .. } => "/copy",
Self::Hooks { .. } => "/hooks",
@ -1309,15 +1290,7 @@ impl SlashCommand {
Self::Tag { .. } => "/tag",
Self::OutputStyle { .. } => "/output-style",
Self::AddDir { .. } => "/add-dir",
<<<<<<< HEAD
<<<<<<< HEAD
Self::Team { .. } => "/team",
=======
Self::Lsp { .. } => "/lsp",
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
=======
Self::Lsp { .. } => "/lsp",
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
Self::Sandbox => "/sandbox",
Self::Mcp { .. } => "/mcp",
Self::Export { .. } => "/export",
@ -1521,11 +1494,6 @@ pub fn validate_slash_command_input(
"theme" => SlashCommand::Theme { name: remainder },
"voice" => SlashCommand::Voice { mode: remainder },
"usage" => SlashCommand::Usage { scope: remainder },
<<<<<<< HEAD
=======
"team" => SlashCommand::Team { action: remainder },
"setup" => SlashCommand::Setup,
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
"rename" => SlashCommand::Rename { name: remainder },
"copy" => SlashCommand::Copy { target: remainder },
"hooks" => SlashCommand::Hooks { args: remainder },
@ -1538,10 +1506,6 @@ pub fn validate_slash_command_input(
"tag" => SlashCommand::Tag { label: remainder },
"output-style" => SlashCommand::OutputStyle { style: remainder },
"add-dir" => SlashCommand::AddDir { path: remainder },
"lsp" => SlashCommand::Lsp {
action: args.first().map(|s| (*s).to_string()),
target: args.get(1).map(|s| (*s).to_string()),
},
"history" => SlashCommand::History {
count: optional_single_arg(command, &args, "[count]")?,
},
@ -5429,27 +5393,9 @@ pub fn handle_slash_command(
| SlashCommand::OutputStyle { .. }
| SlashCommand::AddDir { .. }
| SlashCommand::History { .. }
<<<<<<< HEAD
<<<<<<< HEAD
| SlashCommand::Team { .. }
=======
| SlashCommand::Lsp { .. }
<<<<<<< HEAD
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
| SlashCommand::Setup
=======
| SlashCommand::Lsp { .. }
<<<<<<< HEAD
| SlashCommand::Setup
| SlashCommand::Unknown(_) => None,
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
>>>>>>> 0b227b62 (fix: resolve cherry-pick conflicts and remove non-LSP artifacts)
| SlashCommand::Unknown(_) => None,
=======
| SlashCommand::Team { .. }
| SlashCommand::Setup | SlashCommand::Unknown(_) => None,
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
}
}
@ -6065,20 +6011,8 @@ mod tests {
assert!(help.contains("aliases: /skill"));
assert!(!help.contains("/login"));
assert!(!help.contains("/logout"));
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
assert!(help.contains("/setup"));
assert_eq!(slash_command_specs().len(), 140);
=======
assert_eq!(slash_command_specs().len(), 141);
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
=======
assert_eq!(slash_command_specs().len(), 141);
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
assert_eq!(slash_command_specs().len(), 140);
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
assert!(resume_supported_slash_commands().len() >= 39);
}

View File

@ -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

View File

@ -125,22 +125,6 @@ pub struct RuntimePluginConfig {
max_output_tokens: Option<u32>,
}
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
=======
=======
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
/// Per-language LSP server configuration supplied by the user in settings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LspServerConfig {
pub command: String,
pub args: Vec<String>,
pub enabled: bool,
}
<<<<<<< HEAD
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
/// API timeout and retry configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiTimeoutConfig {
@ -160,66 +144,27 @@ impl Default for ApiTimeoutConfig {
max_retries: 8,
}
}
=======
/// Per-language LSP server configuration supplied by the user in settings.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LspServerConfig {
pub command: String,
pub args: Vec<String>,
pub enabled: bool,
<<<<<<< HEAD
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
=======
/// API timeout and retry configuration.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApiTimeoutConfig {
/// Connect timeout in seconds. Defaults to 30.
pub connect_timeout_secs: u64,
/// Request timeout in seconds. Defaults to 300 (5 minutes).
pub request_timeout_secs: u64,
/// Maximum retry attempts on transient failures. Defaults to 8.
pub max_retries: u32,
}
impl Default for ApiTimeoutConfig {
fn default() -> Self {
Self {
connect_timeout_secs: 30,
request_timeout_secs: 300,
max_retries: 8,
}
}
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
}
=======
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
/// Structured feature configuration consumed by runtime subsystems.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RuntimeFeatureConfig {
hooks: RuntimeHookConfig,
plugins: RuntimePluginConfig,
mcp: McpConfigCollection,
oauth: Option<OAuthConfig>,
model: Option<String>,
lsp_auto_start: bool,
aliases: BTreeMap<String, String>,
permission_mode: Option<ResolvedPermissionMode>,
permission_rules: RuntimePermissionRuleConfig,
sandbox: SandboxConfig,
provider_fallbacks: ProviderFallbackConfig,
trusted_roots: Vec<String>,
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
api_timeout: ApiTimeoutConfig,
rules_import: RulesImportConfig,
provider: RuntimeProviderConfig,
lsp: BTreeMap<String, LspServerConfig>,
api_timeout: ApiTimeoutConfig,
}
<<<<<<< HEAD
/// Controls which external AI coding framework rules are imported into the system prompt.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum RulesImportConfig {
@ -241,26 +186,6 @@ impl RulesImportConfig {
Self::List(frameworks) => frameworks
.iter()
.any(|candidate| candidate.eq_ignore_ascii_case(framework)),
=======
impl Default for RuntimeFeatureConfig {
fn default() -> Self {
Self {
hooks: RuntimeHookConfig::default(),
plugins: RuntimePluginConfig::default(),
mcp: McpConfigCollection::default(),
oauth: None,
model: None,
lsp_auto_start: true,
aliases: BTreeMap::new(),
permission_mode: None,
permission_rules: RuntimePermissionRuleConfig::default(),
sandbox: SandboxConfig::default(),
provider_fallbacks: ProviderFallbackConfig::default(),
trusted_roots: Vec::new(),
provider: RuntimeProviderConfig::default(),
lsp: BTreeMap::new(),
api_timeout: ApiTimeoutConfig::default(),
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
}
}
}
@ -270,34 +195,6 @@ impl Default for RuntimeFeatureConfig {
/// Represents the `provider` section in `~/.claw/settings.json`, used as a
/// fallback when environment variables are absent (3-tier resolution:
/// env var > .env file > stored config).
=======
provider: RuntimeProviderConfig,
lsp: BTreeMap<String, LspServerConfig>,
}
impl Default for RuntimeFeatureConfig {
fn default() -> Self {
Self {
hooks: RuntimeHookConfig::default(),
plugins: RuntimePluginConfig::default(),
mcp: McpConfigCollection::default(),
oauth: None,
model: None,
lsp_auto_start: true,
aliases: BTreeMap::new(),
permission_mode: None,
permission_rules: RuntimePermissionRuleConfig::default(),
sandbox: SandboxConfig::default(),
provider_fallbacks: ProviderFallbackConfig::default(),
trusted_roots: Vec::new(),
provider: RuntimeProviderConfig::default(),
lsp: BTreeMap::new(),
}
}
}
/// Stored provider configuration from the setup wizard.
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RuntimeProviderConfig {
kind: Option<String>,
@ -326,37 +223,6 @@ impl RuntimeProviderConfig {
pub fn model(&self) -> Option<&str> {
self.model.as_deref()
}
=======
rules_import: RulesImportConfig,
}
/// Controls which external AI coding framework rules are auto-imported
/// into the system prompt.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum RulesImportConfig {
/// Auto-import from all supported frameworks (Cursor, Copilot, Windsurf, Aider)
Auto,
/// No auto-import — only .claw/rules/ and CLAUDE.md files are loaded
None,
/// Import only from the listed frameworks
List(Vec<String>),
#[default]
/// Default: auto-import all detected frameworks
Default,
}
impl RulesImportConfig {
pub fn should_import(&self, framework: &str) -> bool {
match self {
Self::Auto | Self::Default => true,
Self::None => false,
Self::List(frameworks) => frameworks.iter().any(|f| f.eq_ignore_ascii_case(framework)),
}
}
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
=======
subagent_model: Option<String>,
>>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation)
}
/// Ordered chain of fallback model identifiers used when the primary
@ -630,7 +496,6 @@ impl ConfigLoader {
build_runtime_config(merged, loaded_entries, mcp)
}
<<<<<<< HEAD
/// Like [`load`] but also returns the list of validation warnings collected during
/// loading, without emitting them to stderr. Callers that want to surface warnings
/// through a structured channel (e.g. the JSON config envelope) should use this.
@ -787,50 +652,6 @@ impl ConfigLoader {
load_error.get_or_insert_with(|| error.to_string());
None
}
=======
let feature_config = RuntimeFeatureConfig {
hooks: parse_optional_hooks_config(&merged_value)?,
plugins: parse_optional_plugin_config(&merged_value)?,
mcp: McpConfigCollection {
servers: mcp_servers,
},
oauth: parse_optional_oauth_config(&merged_value, "merged settings.oauth")?,
model: parse_optional_model(&merged_value),
aliases: parse_optional_aliases(&merged_value)?,
permission_mode: parse_optional_permission_mode(&merged_value)?,
permission_rules: parse_optional_permission_rules(&merged_value)?,
sandbox: parse_optional_sandbox_config(&merged_value)?,
provider_fallbacks: parse_optional_provider_fallbacks(&merged_value)?,
trusted_roots: parse_optional_trusted_roots(&merged_value)?,
<<<<<<< HEAD
<<<<<<< HEAD
provider: parse_optional_provider_config(&merged_value)?,
lsp: parse_optional_lsp_config(&merged_value)?,
<<<<<<< HEAD
<<<<<<< HEAD
<<<<<<< HEAD
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
=======
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
lsp_auto_start: merged_value
.as_object()
.and_then(|o| o.get("lspAutoStart"))
.and_then(JsonValue::as_bool)
.unwrap_or(true),
<<<<<<< HEAD
api_timeout: parse_optional_api_timeout_config(&merged_value)?,
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
=======
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
=======
rules_import: parse_optional_rules_import(&merged_value)?,
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
=======
subagent_model: parse_optional_subagent_model(&merged_value),
>>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation)
};
ConfigInspection {
@ -1089,23 +910,16 @@ impl RuntimeConfig {
&self.feature_config.trusted_roots
}
<<<<<<< HEAD
<<<<<<< HEAD
#[must_use]
<<<<<<< HEAD
pub fn rules_import(&self) -> &RulesImportConfig {
&self.feature_config.rules_import
}
#[must_use]
=======
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
pub fn provider(&self) -> &RuntimeProviderConfig {
&self.feature_config.provider
}
<<<<<<< HEAD
<<<<<<< HEAD
/// Merge config-level default trusted roots with per-call roots.
///
/// Config roots are defaults and are kept first; per-call roots extend the
@ -1115,40 +929,7 @@ impl RuntimeConfig {
#[must_use]
pub fn trusted_roots_with_overrides(&self, per_call_roots: &[String]) -> Vec<String> {
merge_trusted_roots(self.trusted_roots(), per_call_roots)
=======
#[must_use]
pub fn lsp(&self) -> &BTreeMap<String, LspServerConfig> {
&self.feature_config.lsp
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
=======
#[must_use]
pub fn lsp(&self) -> &BTreeMap<String, LspServerConfig> {
&self.feature_config.lsp
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
}
#[must_use]
pub fn lsp_auto_start(&self) -> bool {
self.feature_config.lsp_auto_start
}
<<<<<<< HEAD
=======
pub fn rules_import(&self) -> &RulesImportConfig {
&self.feature_config.rules_import
}
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
=======
=======
>>>>>>> 6b0af2bc (refactor: remove SubAgent tool, make Agent use subagentModel config)
#[must_use]
pub fn subagent_model(&self) -> Option<&str> {
self.feature_config.subagent_model.as_deref()
}
<<<<<<< HEAD
>>>>>>> 7e7baeaa (feat: SubAgent tool for fast sub-agent delegation)
=======
>>>>>>> 6b0af2bc (refactor: remove SubAgent tool, make Agent use subagentModel config)
}
impl RuntimeFeatureConfig {
@ -1227,12 +1008,10 @@ impl RuntimeFeatureConfig {
}
#[must_use]
<<<<<<< HEAD
pub fn rules_import(&self) -> &RulesImportConfig {
&self.rules_import
}
<<<<<<< HEAD
/// Merge this config's default trusted roots with per-call roots.
#[must_use]
pub fn trusted_roots_with_overrides(&self, per_call_roots: &[String]) -> Vec<String> {
@ -1248,29 +1027,6 @@ fn merge_trusted_roots(config_roots: &[String], per_call_roots: &[String]) -> Ve
}
}
merged
=======
=======
pub fn provider(&self) -> &RuntimeProviderConfig {
&self.provider
}
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
#[must_use]
pub fn lsp(&self) -> &BTreeMap<String, LspServerConfig> {
&self.lsp
}
<<<<<<< HEAD
<<<<<<< HEAD
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
=======
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
#[must_use]
pub fn lsp_auto_start(&self) -> bool {
self.lsp_auto_start
}
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
}
impl ProviderFallbackConfig {
@ -1961,19 +1717,6 @@ fn parse_optional_model(root: &JsonValue) -> Option<String> {
.map(ToOwned::to_owned)
}
fn parse_optional_subagent_model(value: &JsonValue) -> Option<String> {
value
.as_object()
.and_then(|object| {
object
.get("subagentModel")
.or_else(|| object.get("subagent_model"))
})
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.map(|s| s.trim().to_string())
}
fn parse_optional_aliases(root: &JsonValue) -> Result<BTreeMap<String, String>, ConfigError> {
let Some(object) = root.as_object() else {
return Ok(BTreeMap::new());
@ -2357,15 +2100,8 @@ fn parse_optional_api_timeout_config(root: &JsonValue) -> Result<ApiTimeoutConfi
return Ok(ApiTimeoutConfig::default());
};
let context = "merged settings.apiTimeout";
<<<<<<< HEAD
let connect_timeout_secs = optional_u64(obj, "connectTimeout", context)?.unwrap_or(30);
let request_timeout_secs = optional_u64(obj, "requestTimeout", context)?.unwrap_or(300);
=======
let connect_timeout_secs = optional_u64(obj, "connectTimeout", context)?
.unwrap_or(30);
let request_timeout_secs = optional_u64(obj, "requestTimeout", context)?
.unwrap_or(300);
>>>>>>> 07ce5aee (feat: API timeout config, Retry-After header support, and configurable retry)
let max_retries = optional_u64(obj, "maxRetries", context)?
.map(|v| v as u32)
.unwrap_or(8);
@ -2386,7 +2122,6 @@ fn parse_optional_trusted_roots(root: &JsonValue) -> Result<Vec<String>, ConfigE
)
}
<<<<<<< HEAD
fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, ConfigError> {
let Some(object) = root.as_object() else {
return Ok(RulesImportConfig::default());
@ -2437,36 +2172,6 @@ fn parse_optional_provider_config(root: &JsonValue) -> Result<RuntimeProviderCon
})
}
=======
fn parse_optional_rules_import(root: &JsonValue) -> Result<RulesImportConfig, ConfigError> {
let Some(object) = root.as_object() else {
return Ok(RulesImportConfig::Default);
};
let Some(value) = object.get("rulesImport") else {
return Ok(RulesImportConfig::Default);
};
match value {
JsonValue::String(s) => match s.as_str() {
"auto" => Ok(RulesImportConfig::Auto),
"none" => Ok(RulesImportConfig::None),
other => Err(ConfigError::Parse(format!(
r#"merged settings.rulesImport: expected "auto", "none", or an array, got "{other}""#
))),
},
JsonValue::Array(arr) => {
let frameworks: Vec<String> = arr
.iter()
.filter_map(|v| v.as_str().map(str::to_owned))
.collect();
Ok(RulesImportConfig::List(frameworks))
}
_ => Err(ConfigError::Parse(format!(
r#"merged settings.rulesImport: expected "auto", "none", or an array"#
))),
}
}
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
fn parse_filesystem_mode_label(value: &str) -> Result<FilesystemIsolationMode, ConfigError> {
match value {
"off" => Ok(FilesystemIsolationMode::Off),
@ -2503,7 +2208,6 @@ fn parse_optional_oauth_config(
}))
}
<<<<<<< HEAD
/// #92: expand `${VAR}` environment variable references and `~/` home directory
/// prefix in a config string value. Returns the expanded string.
fn expand_config_value(value: &str) -> String {
@ -2541,53 +2245,6 @@ fn expand_config_value(value: &str) -> String {
}
}
result
=======
fn parse_optional_provider_config(root: &JsonValue) -> Result<RuntimeProviderConfig, ConfigError> {
let Some(provider_value) = root.as_object().and_then(|object| object.get("provider")) else {
return Ok(RuntimeProviderConfig::default());
};
let Some(object) = provider_value.as_object() else {
return Ok(RuntimeProviderConfig::default());
};
let kind = optional_string(object, "kind", "provider")?.map(str::to_string);
let api_key = optional_string(object, "apiKey", "provider")?.map(str::to_string);
let base_url = optional_string(object, "baseUrl", "provider")?.map(str::to_string);
let model = optional_string(object, "model", "provider")?.map(str::to_string);
Ok(RuntimeProviderConfig {
kind,
api_key,
base_url,
model,
})
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
}
fn parse_optional_lsp_config(
root: &JsonValue,
) -> Result<BTreeMap<String, LspServerConfig>, ConfigError> {
let Some(lsp_value) = root.as_object().and_then(|object| object.get("lsp")) else {
return Ok(BTreeMap::new());
};
let lsp_object = expect_object(lsp_value, "merged settings.lsp")?;
let mut result = BTreeMap::new();
for (language, value) in lsp_object {
let entry = expect_object(value, &format!("merged settings.lsp.{language}"))?;
let command = expect_string(entry, "command", &format!("merged settings.lsp.{language}"))?
.to_string();
let args = optional_string_array(entry, "args", &format!("merged settings.lsp.{language}"))?
.unwrap_or_default();
let enabled = optional_bool(entry, "enabled", &format!("merged settings.lsp.{language}"))?
.unwrap_or(true);
result.insert(
language.clone(),
LspServerConfig {
command,
args,
enabled,
},
);
}
Ok(result)
}
fn parse_mcp_server_config(

View File

@ -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"
}
));
}
}

View File

@ -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,

View File

@ -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
));
}
}
}

View File

@ -267,23 +267,11 @@ pub fn known_lsp_servers() -> Vec<LspServerDescriptor> {
/// Check whether a command exists on the user's PATH by attempting to run it
/// with `--version`. Returns `true` if the command could be spawned
/// successfully, `false` otherwise.
<<<<<<< HEAD
<<<<<<< HEAD
///
/// Some LSP servers (like rust-analyzer via rustup) exit non-zero on --version
/// but are still functional. We treat "spawned successfully" as found, regardless
/// of the exit code. Only a failure to spawn (command not found) returns false.
=======
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
#[must_use]
pub fn command_exists_on_path(command: &str) -> bool {
Command::new(command)
.arg("--version")
.output()
<<<<<<< HEAD
<<<<<<< HEAD
.is_ok()
}
@ -307,16 +295,6 @@ fn rustup_component_works(component: &str) -> bool {
.args(["run", "stable", component, "--version"])
.output()
.is_ok_and(|o| o.status.success())
<<<<<<< HEAD
=======
.map(|output| output.status.success())
.unwrap_or(false)
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
.is_ok()
>>>>>>> 73cc8277 (feat: auto-LSP integration with didOpen/didChange and diagnostic enrichment)
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
}
/// Detect the current platform/distro for install suggestion filtering.
@ -471,35 +449,11 @@ pub fn format_install_prompt(actions: &[LspInstallAction]) -> String {
}
/// Discover LSP servers that are actually installed on the current system.
<<<<<<< HEAD
///
/// Iterates over the known server table and returns only those whose command
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
/// is found on `PATH` **and** is actually functional. For `rust-analyzer`,
/// rustup ships a stub proxy that always exists on PATH but prints
/// "Unknown binary" when the component isn't installed. We detect that
/// case and either rewrite to `rustup run stable rust-analyzer` (when the
/// component is installed) or skip the server entirely (when it's not).
<<<<<<< HEAD
=======
/// is found on `PATH`.
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
=======
>>>>>>> 353da088 (feat(lsp): install prompts, new servers, and advanced LSP features)
#[must_use]
pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
KNOWN_LSP_SERVERS_TABLE
.iter()
.filter(|desc| command_exists_on_path(desc.command))
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
.filter_map(|desc| {
let mut server = desc.to_descriptor();
if desc.command == "rust-analyzer" && is_rustup_proxy("rust-analyzer") {
@ -516,12 +470,6 @@ pub fn discover_available_servers() -> Vec<LspServerDescriptor> {
}
Some(server)
})
<<<<<<< HEAD
=======
.map(StaticLspServerDescriptor::to_descriptor)
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
.collect()
}

View File

@ -321,7 +321,7 @@ pub(super) fn parse_code_actions(value: &JsonValue) -> Vec<crate::lsp_client::Ls
let title = item.get("title")?.as_str()?.to_owned();
let kind = item.get("kind").and_then(JsonValue::as_str).map(str::to_owned);
let is_preferred = item.get("isPreferred").and_then(JsonValue::as_bool).unwrap_or(false);
let edit = item.get("edit").and_then(|e| parse_workspace_edit(e));
let edit = item.get("edit").and_then(parse_workspace_edit);
let command = item.get("command").and_then(parse_command);
Some(crate::lsp_client::LspCodeAction { title, kind, is_preferred, edit, command })
}).collect()
@ -372,12 +372,12 @@ pub(super) fn parse_signature_help(value: &JsonValue) -> Option<crate::lsp_clien
.and_then(|d| d.get("value").and_then(JsonValue::as_str).or_else(|| d.as_str()))
.map(str::to_owned);
let parameters = sig.get("parameters").and_then(JsonValue::as_array)
.map(|arr| arr.iter().filter_map(|p| {
.map(|arr| arr.iter().map(|p| {
let plabel = p.get("label").and_then(|l| l.as_str().or_else(|| l.get("value").and_then(JsonValue::as_str))).unwrap_or("").to_owned();
let pdoc = p.get("documentation")
.and_then(|d| d.get("value").and_then(JsonValue::as_str).or_else(|| d.as_str()))
.map(str::to_owned);
Some(crate::lsp_client::LspParameterInfo { label: plabel, documentation: pdoc })
crate::lsp_client::LspParameterInfo { label: plabel, documentation: pdoc }
}).collect())
.unwrap_or_default();
let active_parameter = sig.get("activeParameter").and_then(JsonValue::as_u64).map(|v| v as u32);

View File

@ -295,7 +295,6 @@ fn discover_instruction_files(
let mut files = Vec::new();
for dir in directories {
// Single-file instruction files (existing)
for candidate in [
dir.join("CLAUDE.md"),
dir.join("CLAW.md"),
@ -307,23 +306,13 @@ fn discover_instruction_files(
] {
push_context_file(&mut files, candidate)?;
}
<<<<<<< HEAD
push_rules_dir(&mut files, dir.join(".claw").join("rules"))?;
push_rules_dir(&mut files, dir.join(".claw").join("rules.local"))?;
push_framework_imports(&mut files, &dir, rules_import)?
=======
// .claw/rules/ directory: all .md files loaded in sorted order
push_rules_dir(&mut files, dir.join(".claw").join("rules"))?;
// .claw/rules.local/ directory: personal/local rules (gitignored)
push_rules_dir(&mut files, dir.join(".claw").join("rules.local"))?;
// Auto-import from other frameworks (Cursor, Copilot, Windsurf, Aider)
push_framework_imports(&mut files, &dir)?;
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
}
Ok(dedupe_instruction_files(files))
}
<<<<<<< HEAD
fn instruction_discovery_dirs(cwd: &Path) -> Vec<PathBuf> {
let boundary = nearest_git_root(cwd).unwrap_or_else(|| cwd.to_path_buf());
let mut directories = Vec::new();
@ -346,94 +335,6 @@ fn nearest_git_root(cwd: &Path) -> Option<PathBuf> {
return Some(dir.to_path_buf());
}
cursor = dir.parent();
=======
/// Load all .md files from a rules directory, sorted alphabetically.
fn push_rules_dir(files: &mut Vec<ContextFile>, dir: PathBuf) -> std::io::Result<()> {
let entries = match fs::read_dir(&dir) {
Ok(entries) => entries,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => return Err(e),
};
let mut paths: Vec<PathBuf> = entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
|| p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("txt"))
|| p.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("mdc"))
})
.collect();
paths.sort();
for path in paths {
push_context_file(files, path)?;
}
Ok(())
}
/// Detect and import rules from other AI coding frameworks so that
/// users switching to claw-code don't have to duplicate their rules.
///
/// Supported frameworks:
/// - Cursor: .cursorrules, .cursor/rules/
/// - GitHub Copilot: .github/copilot-instructions.md
/// - Windsurf: .windsurfrules, .windsurfrules/
/// - Aider: .aider.conf.yml instructions block
/// - Pi (Plandex): .plandex/plan.md, .plandex/instructions.md
/// - OpenCode: opencode.json instructions field
/// - CrushCode / Crush: .crush/rules/, .crush/CLAUDE.md
fn push_framework_imports(files: &mut Vec<ContextFile>, dir: &Path) -> std::io::Result<()> {
// Cursor
push_context_file(files, dir.join(".cursorrules"))?;
push_rules_dir(files, dir.join(".cursor").join("rules"))?;
// GitHub Copilot
push_context_file(files, dir.join(".github").join("copilot-instructions.md"))?;
// Windsurf
push_context_file(files, dir.join(".windsurfrules"))?;
push_rules_dir(files, dir.join(".windsurfrules"))?;
// Aider — reads the instruction lines from .aider.conf.yml
if let Some(aider_instructions) = read_aider_instructions(dir) {
files.push(ContextFile {
path: dir.join(".aider.conf.yml").join("instructions"),
content: aider_instructions,
});
}
// Pi (Plandex)
push_context_file(files, dir.join(".plandex").join("instructions.md"))?;
push_context_file(files, dir.join(".plandex").join("plan.md"))?;
// OpenCode — reads instructions from opencode.json config
if let Some(opencode_instructions) = read_opencode_instructions(dir) {
files.push(ContextFile {
path: dir.join("opencode.json").join("instructions"),
content: opencode_instructions,
});
}
// CrushCode / Crush
push_context_file(files, dir.join(".crush").join("CLAUDE.md"))?;
push_rules_dir(files, dir.join(".crush").join("rules"))?;
Ok(())
}
/// Extract instructions from an opencode.json config file.
/// OpenCode stores rules in a top-level "instructions" field.
fn read_opencode_instructions(dir: &Path) -> Option<String> {
let content = fs::read_to_string(dir.join("opencode.json")).ok()?;
let parsed: serde_json::Value = serde_json::from_str(&content).ok()?;
parsed.get("instructions")?.as_str().map(str::to_owned)
}
/// Extract instruction lines from an .aider.conf.yml file.
/// Aider stores instructions like: `instructions: ...` or multiline block.
fn read_aider_instructions(dir: &Path) -> Option<String> {
let content = fs::read_to_string(dir.join(".aider.conf.yml")).ok()?;
for line in content.lines() {
let trimmed = line.trim();
if let Some(val) = trimmed.strip_prefix("instructions:") {
let instruction = val.trim();
if !instruction.is_empty() {
return Some(instruction.to_owned());
}
}
>>>>>>> 22f948b7 (feat: project rules with .claw/rules/ and multi-framework auto-import)
}
None
}

View File

@ -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)
})
}

View File

@ -29,7 +29,7 @@ impl Default for TridentConfig {
}
/// Statistics from a Trident compaction run.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct TridentStats {
pub superseded_count: usize,
pub collapsed_chains: usize,
@ -41,21 +41,6 @@ pub struct TridentStats {
pub final_message_count: usize,
}
impl Default for TridentStats {
fn default() -> Self {
Self {
superseded_count: 0,
collapsed_chains: 0,
messages_collapsed: 0,
clusters_found: 0,
messages_clustered: 0,
tokens_saved_estimate: 0,
original_message_count: 0,
final_message_count: 0,
}
}
}
impl TridentStats {
pub fn format_report(&self) -> String {
let compression = if self.final_message_count > 0 {
@ -78,14 +63,10 @@ impl TridentStats {
self.messages_clustered, self.clusters_found
),
format!(" Original: {} messages", self.original_message_count),
<<<<<<< HEAD
format!(
" Final: {} messages ({:.1}x compression)",
self.final_message_count, compression
),
=======
format!(" Final: {} messages ({:.1}x compression)", self.final_message_count, compression),
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
];
if self.tokens_saved_estimate > 0 {
lines.push(format!(
@ -128,12 +109,8 @@ pub fn trident_compact_session(
}
if trident_config.collapse_enabled {
<<<<<<< HEAD
let (collapsed, chains, collapsed_count) =
stage2_collapse(&messages, trident_config.collapse_threshold);
=======
let (collapsed, chains, collapsed_count) = stage2_collapse(&messages, trident_config.collapse_threshold);
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
stats.collapsed_chains = chains;
stats.messages_collapsed = collapsed_count;
messages = collapsed;
@ -190,24 +167,17 @@ fn stage1_supersede(messages: &[ConversationMessage]) -> (Vec<ConversationMessag
for (i, msg) in messages.iter().enumerate() {
for block in &msg.blocks {
if let Some((path, op_type)) = extract_file_operation(block) {
<<<<<<< HEAD
file_ops
.entry(path)
.or_default()
.push(FileOperation { index: i, op_type });
=======
file_ops.entry(path).or_default().push(FileOperation {
index: i,
op_type,
});
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
}
}
}
let mut obsolete_indices: BTreeSet<usize> = BTreeSet::new();
for (_path, ops) in &file_ops {
for ops in file_ops.values() {
if ops.len() < 2 {
continue;
}
@ -220,9 +190,7 @@ fn stage1_supersede(messages: &[ConversationMessage]) -> (Vec<ConversationMessag
if let Some(last_write) = last_write_idx {
for op in ops {
if op.op_type == FileOp::Read && op.index < last_write {
obsolete_indices.insert(op.index);
} else if (op.op_type == FileOp::Write || op.op_type == FileOp::Edit)
if (op.op_type == FileOp::Read || op.op_type == FileOp::Write || op.op_type == FileOp::Edit)
&& op.index < last_write
{
obsolete_indices.insert(op.index);
@ -254,13 +222,9 @@ fn extract_file_operation(block: &ContentBlock) -> Option<(String, FileOp)> {
};
Some((path, op_type))
}
<<<<<<< HEAD
ContentBlock::ToolResult {
tool_name, output, ..
} => {
=======
ContentBlock::ToolResult { tool_name, output, .. } => {
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
let path = extract_path_from_tool_output(tool_name, output)?;
let op_type = match tool_name.as_str() {
"read_file" | "Read" => FileOp::Read,
@ -271,23 +235,15 @@ fn extract_file_operation(block: &ContentBlock) -> Option<(String, FileOp)> {
Some((path, op_type))
}
ContentBlock::Text { .. } => None,
<<<<<<< HEAD
ContentBlock::Thinking { .. } => None,
=======
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
}
}
fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option<String> {
<<<<<<< HEAD
if !matches!(
tool_name,
"read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit"
) {
=======
if !matches!(tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit")
{
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
return None;
}
serde_json::from_str::<serde_json::Value>(input)
@ -301,15 +257,10 @@ fn extract_path_from_tool_input(tool_name: &str, input: &str) -> Option<String>
}
fn extract_path_from_tool_output(tool_name: &str, output: &str) -> Option<String> {
<<<<<<< HEAD
if !matches!(
tool_name,
"read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit"
) {
=======
if !matches!(tool_name, "read_file" | "write_file" | "edit_file" | "Read" | "Write" | "Edit")
{
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
return None;
}
serde_json::from_str::<serde_json::Value>(output)
@ -357,7 +308,7 @@ fn stage2_collapse(
usage: None,
});
} else {
result.extend(buffer.drain(..));
result.append(&mut buffer);
}
buffer.clear();
result.push(msg.clone());
@ -383,7 +334,6 @@ fn stage2_collapse(
}
fn is_chatty_message(msg: &ConversationMessage) -> bool {
<<<<<<< HEAD
let total_chars: usize = msg
.blocks
.iter()
@ -403,16 +353,6 @@ fn is_chatty_message(msg: &ConversationMessage) -> bool {
.blocks
.iter()
.any(|b| matches!(b, ContentBlock::ToolResult { .. }));
=======
let total_chars: usize = msg.blocks.iter().map(|b| match b {
ContentBlock::Text { text } => text.len(),
ContentBlock::ToolUse { input, .. } => input.len(),
ContentBlock::ToolResult { output, .. } => output.len(),
}).sum();
let has_tool_use = msg.blocks.iter().any(|b| matches!(b, ContentBlock::ToolUse { .. }));
let has_tool_result = msg.blocks.iter().any(|b| matches!(b, ContentBlock::ToolResult { .. }));
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
if has_tool_use || has_tool_result {
return false;
@ -519,7 +459,7 @@ fn stage3_cluster(
}
let total_clustered: usize = cluster_assignments.len();
let clusters_found = cluster_id as usize;
let clusters_found = cluster_id;
let mut result: Vec<ConversationMessage> = Vec::new();
let mut cluster_buffers: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
@ -528,24 +468,12 @@ fn stage3_cluster(
cluster_buffers.entry(cid).or_default().push(*msg_idx);
}
<<<<<<< HEAD
=======
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
for (i, msg) in messages.iter().enumerate() {
if let Some(&cid) = cluster_assignments.get(&i) {
if let Some(buffer) = cluster_buffers.get_mut(&cid) {
if buffer[0] == i {
<<<<<<< HEAD
let cluster_messages: Vec<&ConversationMessage> =
buffer.iter().filter_map(|&idx| messages.get(idx)).collect();
=======
let cluster_messages: Vec<&ConversationMessage> = buffer
.iter()
.filter_map(|&idx| messages.get(idx))
.collect();
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
let summary = generate_cluster_summary(&cluster_messages);
result.push(ConversationMessage {
role: MessageRole::System,
@ -591,13 +519,9 @@ fn fingerprint_message(index: usize, msg: &ConversationMessage) -> Option<Messag
}
text_length += input.len();
}
<<<<<<< HEAD
ContentBlock::ToolResult {
tool_name, output, ..
} => {
=======
ContentBlock::ToolResult { tool_name, output, .. } => {
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
tool_names.insert(tool_name.clone());
if let Some(path) = extract_path_from_tool_output(tool_name, output) {
file_paths.insert(path);
@ -607,12 +531,9 @@ fn fingerprint_message(index: usize, msg: &ConversationMessage) -> Option<Messag
ContentBlock::Text { text } => {
text_length += text.len();
}
<<<<<<< HEAD
ContentBlock::Thinking { thinking, .. } => {
text_length += thinking.len();
}
=======
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
}
}
@ -676,23 +597,16 @@ fn generate_cluster_summary(messages: &[&ConversationMessage]) -> String {
file_paths.insert(path);
}
}
<<<<<<< HEAD
ContentBlock::ToolResult {
tool_name, output, ..
} => {
=======
ContentBlock::ToolResult { tool_name, output, .. } => {
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
tool_names.insert(tool_name.clone());
if let Some(path) = extract_path_from_tool_output(tool_name, output) {
file_paths.insert(path);
}
}
ContentBlock::Text { .. } => {}
<<<<<<< HEAD
ContentBlock::Thinking { .. } => {}
=======
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
}
}
}
@ -728,10 +642,7 @@ fn estimate_message_tokens(message: &ConversationMessage) -> usize {
ContentBlock::ToolResult {
tool_name, output, ..
} => (tool_name.len() + output.len()) / 4 + 1,
<<<<<<< HEAD
ContentBlock::Thinking { thinking, .. } => thinking.len() / 4 + 1,
=======
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
})
.sum()
}
@ -759,31 +670,23 @@ mod tests {
name: "read_file".to_string(),
input: r#"{"path":"src/main.rs"}"#.to_string(),
}]),
<<<<<<< HEAD
ConversationMessage::tool_result(
"1",
"read_file",
r#"{"path":"src/main.rs","content":"old"}"#,
false,
),
=======
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"old"}"#, false),
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
ConversationMessage::assistant(vec![ContentBlock::ToolUse {
id: "2".to_string(),
name: "edit_file".to_string(),
input: r#"{"path":"src/main.rs","old":"old","new":"new"}"#.to_string(),
}]),
<<<<<<< HEAD
ConversationMessage::tool_result(
"2",
"edit_file",
r#"{"path":"src/main.rs","ok":true}"#,
false,
),
=======
ConversationMessage::tool_result("2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false),
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
];
let (kept, superseded) = stage1_supersede(&messages);
@ -799,16 +702,12 @@ mod tests {
name: "read_file".to_string(),
input: r#"{"path":"src/main.rs"}"#.to_string(),
}]),
<<<<<<< HEAD
ConversationMessage::tool_result(
"1",
"read_file",
r#"{"path":"src/main.rs","content":"data"}"#,
false,
),
=======
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"data"}"#, false),
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
];
let (kept, superseded) = stage1_supersede(&messages);
@ -825,7 +724,6 @@ mod tests {
text: format!("got {i}"),
}]));
}
<<<<<<< HEAD
messages.push(ConversationMessage::assistant(vec![
ContentBlock::ToolUse {
id: "t".to_string(),
@ -833,13 +731,6 @@ mod tests {
input: r#"{"command":"ls"}"#.to_string(),
},
]));
=======
messages.push(ConversationMessage::assistant(vec![ContentBlock::ToolUse {
id: "t".to_string(),
name: "bash".to_string(),
input: r#"{"command":"ls"}"#.to_string(),
}]));
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
let (result, chains, collapsed) = stage2_collapse(&messages, 4);
assert!(chains > 0, "should collapse at least one chain");
@ -851,7 +742,6 @@ mod tests {
fn stage3_clusters_similar_messages() {
let mut messages = vec![];
for i in 0..5 {
<<<<<<< HEAD
messages.push(ConversationMessage::assistant(vec![
ContentBlock::ToolUse {
id: format!("read_{i}"),
@ -859,13 +749,6 @@ mod tests {
input: format!(r#"{{"path":"src/{i}.rs"}}"#),
},
]));
=======
messages.push(ConversationMessage::assistant(vec![ContentBlock::ToolUse {
id: format!("read_{i}"),
name: "read_file".to_string(),
input: format!(r#"{{"path":"src/{i}.rs"}}"#),
}]));
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
messages.push(ConversationMessage::tool_result(
&format!("read_{i}"),
"read_file",
@ -874,12 +757,7 @@ mod tests {
));
}
<<<<<<< HEAD
let (result, clusters, clustered) = stage3_cluster(&messages, 3, 0.4);
=======
let (result, clusters, clustered) =
stage3_cluster(&messages, 3, 0.4);
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
assert!(clusters > 0, "should find at least one cluster");
assert!(clustered > 0);
assert!(result.len() < messages.len());
@ -895,31 +773,23 @@ mod tests {
name: "read_file".to_string(),
input: r#"{"path":"src/main.rs"}"#.to_string(),
}]),
<<<<<<< HEAD
ConversationMessage::tool_result(
"1",
"read_file",
r#"{"path":"src/main.rs","content":"fn main() { buggy }"}"#,
false,
),
=======
ConversationMessage::tool_result("1", "read_file", r#"{"path":"src/main.rs","content":"fn main() { buggy }"}"#, false),
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
ConversationMessage::assistant(vec![ContentBlock::ToolUse {
id: "2".to_string(),
name: "edit_file".to_string(),
input: r#"{"path":"src/main.rs","old":"buggy","new":"fixed"}"#.to_string(),
}]),
<<<<<<< HEAD
ConversationMessage::tool_result(
"2",
"edit_file",
r#"{"path":"src/main.rs","ok":true}"#,
false,
),
=======
ConversationMessage::tool_result("2", "edit_file", r#"{"path":"src/main.rs","ok":true}"#, false),
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
ConversationMessage::assistant(vec![ContentBlock::Text {
text: "Fixed the bug in main.rs".to_string(),
}]),
@ -935,14 +805,10 @@ mod tests {
&trident_config,
);
<<<<<<< HEAD
assert!(
result.removed_message_count > 0
|| result.compacted_session.messages.len() < session.messages.len()
);
=======
assert!(result.removed_message_count > 0 || result.compacted_session.messages.len() < session.messages.len());
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
}
#[test]

View File

@ -16,7 +16,6 @@
)]
mod init;
mod input;
mod tui;
mod render;
mod setup_wizard;
@ -6926,30 +6925,10 @@ fn run_resume_command(
| SlashCommand::Ide { .. }
| SlashCommand::Tag { .. }
| SlashCommand::OutputStyle { .. }
<<<<<<< HEAD
<<<<<<< HEAD
| SlashCommand::AddDir { .. }
<<<<<<< HEAD
| SlashCommand::Team { .. }
=======
| SlashCommand::Lsp { .. }
>>>>>>> 856409d3 (feat: full LSP (Language Server Protocol) integration)
=======
| SlashCommand::AddDir { .. } => Err("unsupported resumed slash command".into()),
| SlashCommand::AddDir { .. }
| SlashCommand::Lsp { .. }
<<<<<<< HEAD
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
| SlashCommand::Setup => Err("unsupported resumed slash command".into()),
=======
| SlashCommand::AddDir { .. }
| SlashCommand::Lsp { .. } => Err("unsupported resumed slash command".into()),
>>>>>>> 0b227b62 (fix: resolve cherry-pick conflicts and remove non-LSP artifacts)
}
=======
| SlashCommand::Team { .. }
| SlashCommand::Setup => Err("unsupported resumed slash command".into()), }
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
}
/// Detect if the current working directory is "broad" (home directory or
@ -7073,349 +7052,17 @@ fn run_repl(
base_commit: Option<String>,
reasoning_effort: Option<String>,
allow_broad_cwd: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if io::stdout().is_terminal() {
return run_repl_tui(model, allowed_tools, permission_mode, base_commit, reasoning_effort, allow_broad_cwd);
}
run_repl_classic(model, allowed_tools, permission_mode, base_commit, reasoning_effort, allow_broad_cwd)
}
#[allow(clippy::too_many_lines)]
fn run_repl_tui(
model: String,
allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode,
base_commit: Option<String>,
reasoning_effort: Option<String>,
allow_broad_cwd: bool,
) -> Result<(), Box<dyn std::error::Error>> {
enforce_broad_cwd_policy(allow_broad_cwd, CliOutputFormat::Text)?;
run_stale_base_preflight(base_commit.as_deref());
let resolved_model = resolve_repl_model(model.clone());
let reasoning_effort_clone = reasoning_effort.clone();
let mut cli = LiveCli::new(resolved_model.clone(), true, allowed_tools.clone(), permission_mode)?;
let cwd = std::env::current_dir().unwrap_or_default();
let lsp_auto = runtime::ConfigLoader::default_for(&cwd)
.load()
.map(|c| c.lsp_auto_start())
.unwrap_or(true);
cli.lsp_auto_start = lsp_auto;
cli.set_reasoning_effort(reasoning_effort_clone);
// Build shared dashboard state
let dashboard_state = tui::SharedDashboardState::new(std::sync::RwLock::new({
let mut ds = tui::DashboardState::new();
ds.model = cli.model.clone();
ds.permission_mode = permission_mode.as_str().to_string();
ds.session_id = Some(cli.session.id.clone());
if let Ok(config) = runtime::ConfigLoader::default_for(&cwd).load() {
if let Some(base_url) = config.provider().base_url() {
ds.provider_url = base_url.to_string();
}
}
ds
}));
// Discover and register LSP servers (before TUI takes over the terminal)
let lsp_servers = runtime::lsp_discovery::discover_available_servers();
if !lsp_servers.is_empty() {
for server in &lsp_servers {
tools::global_lsp_registry().register_with_descriptor(
&server.language,
runtime::lsp_client::LspServerStatus::Starting,
None,
vec![],
server.clone(),
);
}
if cli.lsp_auto_start {
let registry = tools::global_lsp_registry();
for server in &lsp_servers {
let _ = registry.start_server(&server.language);
}
}
}
update_dashboard_lsp(&dashboard_state);
// Initialize TUI
let mut app = match tui::TuiApp::init(dashboard_state.clone()) {
Ok(app) => app,
Err(e) => {
eprintln!("TUI init failed, falling back to classic mode: {e}");
return run_repl_classic(model, allowed_tools, permission_mode, base_commit, reasoning_effort, allow_broad_cwd);
}
};
// TUI banner — styled ASCII art with info lines
{
let status = status_context(None).ok();
let git_branch = status
.as_ref()
.and_then(|ctx| ctx.git_branch.as_deref())
.unwrap_or("unknown");
let workspace = status.as_ref().map_or_else(
|| "unknown".to_string(),
|ctx| ctx.git_summary.headline(),
);
let banner_lines = vec![
tui::BannerLine { text: " ██████╗██╗ █████╗ ██╗ ██╗".to_string(), color: ratatui::style::Color::Red },
tui::BannerLine { text: "██╔════╝██║ ██╔══██╗██║ ██║".to_string(), color: ratatui::style::Color::Red },
tui::BannerLine { text: "██║ ██║ ███████║██║ █╗ ██║".to_string(), color: ratatui::style::Color::Red },
tui::BannerLine { text: "██║ ██║ ██╔══██║██║███╗██║".to_string(), color: ratatui::style::Color::Red },
tui::BannerLine { text: "╚██████╗███████╗██║ ██║╚███╔███╔╝".to_string(), color: ratatui::style::Color::Red },
tui::BannerLine { text: " ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ Code".to_string(), color: ratatui::style::Color::Red },
tui::BannerLine { text: String::new(), color: ratatui::style::Color::White },
tui::BannerLine { text: format!(" Model {}", cli.model), color: ratatui::style::Color::White },
tui::BannerLine { text: format!(" Mode {}", permission_mode.as_str()), color: ratatui::style::Color::White },
tui::BannerLine { text: format!(" Branch {}", git_branch), color: ratatui::style::Color::Green },
tui::BannerLine { text: format!(" Workspace {}", workspace), color: ratatui::style::Color::White },
tui::BannerLine { text: format!(" Session {}", cli.session.id), color: ratatui::style::Color::Gray },
tui::BannerLine { text: String::new(), color: ratatui::style::Color::White },
];
app.push_banner(banner_lines);
}
// Main TUI event loop
loop {
app.set_slash_completions(cli.repl_completion_candidates().unwrap_or_default());
match app.read_line()? {
tui::TuiReadOutcome::Pending => continue,
tui::TuiReadOutcome::Submit(input) => {
let trimmed = input.trim().to_string();
if trimmed.is_empty() {
continue;
}
if matches!(trimmed.as_str(), "/exit" | "/quit") {
let _ = app.restore_terminal();
cli.shutdown_lsp_servers();
cli.persist_session()?;
break;
}
match SlashCommand::parse(&trimmed) {
Ok(Some(command)) => {
let cmd_result = cli.handle_repl_command(command);
if let Ok(true) = cmd_result {
cli.persist_session()?;
}
if let Err(e) = cmd_result {
app.push_system_message(&format!("Command error: {e}"));
}
continue;
}
Ok(None) => {}
Err(error) => {
app.push_system_message(&error.to_string());
continue;
}
}
let cwd_check = std::env::current_dir().unwrap_or_default();
let prompt = if let Some(p) = try_resolve_bare_skill_prompt(&cwd_check, &trimmed) {
p
} else {
trimmed.clone()
};
app.push_user_input(&input);
cli.record_prompt_history(&trimmed);
update_dashboard(&dashboard_state, &cli);
app.set_status("Thinking...");
// Run turn in-place. Output goes to the alternate screen buffer
// which ratatui owns - it will be overwritten on next redraw.
// This avoids the fragile suspend/resume pattern.
let result = cli.run_turn(&prompt);
// Read the last assistant message from the session for the conversation pane
{
let messages = &cli.runtime.session().messages;
if let Some(msg) = messages.last() {
if msg.role == runtime::MessageRole::Assistant {
for block in &msg.blocks {
if let runtime::ContentBlock::Text { text } = block {
app.push_output(text, false);
}
}
}
}
}
match result {
Ok(()) => {
app.set_status("Done");
if let Ok(mut ds) = dashboard_state.write() {
ds.status_message.clear();
}
}
Err(e) => {
app.push_system_message(&format!("Error: {e}"));
app.set_status("");
}
}
update_dashboard(&dashboard_state, &cli);
}
tui::TuiReadOutcome::ProviderSwap => {
// Provider swap wizard needs interactive terminal
let _ = app.restore_terminal();
println!();
setup_wizard::run_setup_wizard()?;
let cwd = std::env::current_dir().unwrap_or_default();
let config = runtime::ConfigLoader::default_for(&cwd).load().ok();
if let Some(new_model) = config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) {
let _ = cli.set_model(Some(new_model));
}
app.push_system_message("Provider updated - restart for full effect");
update_dashboard(&dashboard_state, &cli);
}
tui::TuiReadOutcome::TeamToggle => {
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
if current == "1" {
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
app.push_system_message("[team] Agent teams disabled");
} else {
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
app.push_system_message("[team] Agent teams enabled");
}
}
tui::TuiReadOutcome::Cancel => {
// Ctrl+C clears input (handled in TUI), just continue
}
tui::TuiReadOutcome::Exit => {
let _ = app.restore_terminal();
cli.shutdown_lsp_servers();
cli.persist_session()?;
break;
}
}
}
Ok(())
}
fn update_dashboard(state: &tui::SharedDashboardState, cli: &LiveCli) {
if let Ok(mut ds) = state.write() {
ds.model = cli.model.clone();
ds.turn_count = cli.runtime.usage().turns();
let usage = cli.runtime.usage().cumulative_usage();
ds.input_tokens = usage.input_tokens;
ds.output_tokens = usage.output_tokens;
ds.cache_read_tokens = usage.cache_read_input_tokens;
ds.cache_creation_tokens = usage.cache_creation_input_tokens;
ds.cost_usd = usage.estimate_cost_usd().total_cost_usd();
ds.session_id = Some(cli.session.id.clone());
}
update_dashboard_lsp(state);
}
fn update_dashboard_lsp(state: &tui::SharedDashboardState) {
if let Ok(mut ds) = state.write() {
ds.lsp_servers = tools::global_lsp_registry()
.list_servers()
.into_iter()
.map(|s| tui::LspInfo {
language: s.language,
status: s.status.to_string(),
})
.collect();
}
}
#[allow(clippy::too_many_lines)]
fn run_repl_classic(
model: String,
allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode,
base_commit: Option<String>,
reasoning_effort: Option<String>,
allow_broad_cwd: bool,
) -> Result<(), Box<dyn std::error::Error>> {
enforce_broad_cwd_policy(allow_broad_cwd, CliOutputFormat::Text)?;
run_stale_base_preflight(base_commit.as_deref());
let resolved_model = resolve_repl_model(model)?;
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
// Read config for LSP auto-start setting
let cwd = std::env::current_dir().unwrap_or_default();
let lsp_auto = runtime::ConfigLoader::default_for(&cwd)
.load()
.map(|c| c.lsp_auto_start())
.unwrap_or(true);
cli.lsp_auto_start = lsp_auto;
cli.set_reasoning_effort(reasoning_effort);
let mut editor =
input::LineEditor::new("> ", cli.repl_completion_candidates().unwrap_or_default());
println!("{}", cli.startup_banner());
println!("{}", format_connected_line(&cli.model));
// Validate key config fields and prompt setup wizard if missing
{
let cwd = std::env::current_dir().unwrap_or_default();
if let Ok(config) = runtime::ConfigLoader::default_for(&cwd).load() {
let mut missing: Vec<&str> = Vec::new();
if config.provider().api_key().is_none() {
missing.push("provider.apiKey");
}
if config.provider().base_url().is_none() {
missing.push("provider.baseUrl");
}
if config.subagent_model().is_none() {
missing.push("subagentModel");
}
if !missing.is_empty() {
eprintln!("
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 <language> or set lspAutoStart=true in settings.json");
}
<<<<<<< HEAD
=======
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
}
// Show install suggestions for missing LSP servers
{
let availability = runtime::lsp_discovery::check_lsp_availability();
let prompt = runtime::lsp_discovery::format_install_prompt(&availability);
if !prompt.is_empty() {
eprintln!("{prompt}");
}
}
loop {
editor.set_completions(cli.repl_completion_candidates().unwrap_or_default());
match editor.read_line()? {
@ -7425,7 +7072,6 @@ fn run_repl_classic(
continue;
}
if matches!(trimmed.as_str(), "/exit" | "/quit") {
cli.shutdown_lsp_servers();
cli.persist_session()?;
break;
}
@ -7456,33 +7102,31 @@ fn run_repl_classic(
cli.record_prompt_history(&trimmed);
cli.run_turn(&trimmed)?;
}
input::ReadOutcome::Cancel => {}
input::ReadOutcome::Exit => {
cli.persist_session()?;
break;
}
input::ReadOutcome::ProviderSwap => {
// Ctrl+P triggered — launch setup wizard and hot-swap model
setup_wizard::run_setup_wizard()?;
let _ = setup_wizard::run_setup_wizard();
let cwd = std::env::current_dir().unwrap_or_default();
let config = runtime::ConfigLoader::default_for(&cwd).load().ok();
if let Some(new_model) = config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) {
cli.set_model(Some(new_model))?;
if let Some(new_model) =
config.as_ref().and_then(|c| c.provider().model().map(str::to_string))
{
let _ = cli.set_model(Some(new_model));
}
println!("{}", format_connected_line(&cli.model));
}
input::ReadOutcome::TeamToggle => {
// Ctrl+T toggles agent teams mode
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
if current == "1" {
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
eprintln!("[team] Agent teams disabled");
} else {
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
eprintln!("[team] Agent teams enabled");
}
}
input::ReadOutcome::Cancel => {}
input::ReadOutcome::Exit => {
cli.shutdown_lsp_servers();
cli.persist_session()?;
break;
}
}
}
@ -7516,7 +7160,6 @@ struct LiveCli {
runtime: BuiltRuntime,
session: SessionHandle,
prompt_history: Vec<PromptHistoryEntry>,
lsp_auto_start: bool,
}
#[derive(Debug, Clone)]
@ -8031,7 +7674,6 @@ impl LiveCli {
runtime,
session,
prompt_history: Vec::new(),
lsp_auto_start: true,
};
cli.persist_session()?;
Ok(cli)
@ -8188,8 +7830,6 @@ impl LiveCli {
// ============================================================================
let error_str = error.to_string();
<<<<<<< HEAD
<<<<<<< HEAD
// Detect context window overflow. Some providers (e.g. OpenAI-compat backends)
// return 400 with "no parseable body" instead of a proper context_length_exceeded
// error when the request is too large to even parse — treat that as context overflow too.
@ -8252,57 +7892,11 @@ impl LiveCli {
);
let removed = result.removed_message_count;
if removed == 0 && round > 0 {
// No more messages to compact — further rounds won't help
println!(" No further compaction possible.");
break;
=======
let is_context_window = error_str.contains("context_window") || error_str.contains("Context window");
=======
let is_context_window = error_str.contains("context_window")
|| error_str.contains("Context window")
|| error_str.contains("no parseable body");
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
if is_context_window {
// Progressive auto-compact retry loop:
// Each round compacts more aggressively (fewer preserved messages)
// until the request fits in the model's context window.
// Max 4 rounds of compaction before giving up.
let max_compact_rounds = 4;
let preserve_schedule = [4, 2, 1, 0];
for round in 0..max_compact_rounds {
let preserve = preserve_schedule[round];
println!(
" Auto-compacting session (round {}/{}, preserving {} recent messages)...",
round + 1,
max_compact_rounds,
preserve
);
// Run Trident pipeline then summary-based compaction
let result = runtime::trident::trident_compact_session(
runtime.session(),
CompactionConfig {
preserve_recent_messages: preserve,
max_estimated_tokens: 0,
},
&runtime::trident::TridentConfig::default(),
);
let removed = result.removed_message_count;
if removed == 0 && round > 0 {
// No more messages to compact — further rounds won't help
println!(" No further compaction possible.");
break;
}
if removed > 0 {
println!("{}", format_compact_report(removed, result.compacted_session.messages.len(), false));
>>>>>>> 5e19cf1c (feat: Trident compaction pipeline (supersede + collapse + cluster))
}
<<<<<<< HEAD
if removed > 0 {
println!(
@ -8325,31 +7919,16 @@ impl LiveCli {
self.prepare_turn_runtime(true)?;
drop(hook_abort_monitor);
=======
// Replace self.runtime's session with the compacted version
// so prepare_turn_runtime builds from the compacted session
*self.runtime.session_mut() = result.compacted_session.clone();
// Build a new runtime with the compacted session and retry
let (mut new_runtime, hook_abort_monitor) = self.prepare_turn_runtime(true)?;
drop(hook_abort_monitor);
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
let mut rp = CliPermissionPrompter::new(self.permission_mode);
match new_runtime.run_turn(input, Some(&mut rp)) {
Ok(summary) => {
self.replace_runtime(new_runtime)?;
spinner.finish(
<<<<<<< HEAD
if round == 0 {
"✨ Done (after auto-compact)"
} else {
"✨ Done (after aggressive auto-compact)"
},
=======
if round == 0 { "✨ Done (after auto-compact)" } else { "✨ Done (after aggressive auto-compact)" },
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
TerminalRenderer::new().color_theme(),
&mut stdout,
)?;
@ -8367,7 +7946,6 @@ impl LiveCli {
let retry_str = retry_error.to_string();
let still_context_window = retry_str.contains("context_window")
|| retry_str.contains("Context window")
<<<<<<< HEAD
|| retry_str.contains("no parseable body")
|| retry_str.contains("exceed_context_size")
|| retry_str.contains("exceeds the available context size")
@ -8394,21 +7972,11 @@ impl LiveCli {
// The compacted session was still too large for the model's context.
// Shut down the old runtime, adopt the partially-compacted one,
// and loop — the next round will compact more aggressively.
=======
|| retry_str.contains("no parseable body");
if still_context_window && round + 1 < max_compact_rounds {
// Still too large — compact more aggressively next round
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
runtime.shutdown_plugins()?;
runtime = new_runtime;
continue;
}
<<<<<<< HEAD
=======
>>>>>>> 1ff5617c (fix: sync all bug fixes to combined branch)
// Not a context window error, or out of rounds
return Err(Box::new(retry_error));
}
@ -8592,52 +8160,6 @@ impl LiveCli {
run_init(CliOutputFormat::Text)?;
false
}
<<<<<<< HEAD
=======
SlashCommand::Team { action } => {
match action.as_deref().unwrap_or("") {
"on" | "enable" => {
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
}
"off" | "disable" => {
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
eprintln!("[team] Agent teams disabled");
}
"status" => {
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
if current == "1" {
eprintln!("[team] Agent teams: ENABLED");
} else {
eprintln!("[team] Agent teams: DISABLED (use /team on or Ctrl+T to enable)");
}
}
"" => {
// Toggle
let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default();
if current == "1" {
std::env::set_var("CLAWD_AGENT_TEAMS", "0");
eprintln!("[team] Agent teams disabled");
} else {
std::env::set_var("CLAWD_AGENT_TEAMS", "1");
eprintln!("[team] Agent teams enabled (TeamCreate now available)");
}
}
other => eprintln!("[team] unknown action: {other}. Use: /team [on|off|status]"),
}
false
}
SlashCommand::Setup => {
setup_wizard::run_setup_wizard()?;
// Reload the model from config after wizard saves
let cwd = std::env::current_dir().unwrap_or_default();
let config = runtime::ConfigLoader::default_for(&cwd).load().ok();
if let Some(new_model) = config.as_ref().and_then(|c| c.provider().model().map(str::to_string)) {
self.set_model(Some(new_model))?;
}
false
}
>>>>>>> 7ab899c0 (feat: agent teams with task claiming, context management, and team monitoring)
SlashCommand::Diff => {
Self::print_diff()?;
false
@ -8744,10 +8266,6 @@ impl LiveCli {
eprintln!("{cmd_name} is not yet implemented in this build.");
false
}
SlashCommand::Lsp { action, target } => {
self.handle_lsp_command(action.as_deref(), target.as_deref());
false
}
SlashCommand::Unknown(name) => {
eprintln!("{}", format_unknown_slash_command(&name));
false
@ -8755,79 +8273,6 @@ impl LiveCli {
})
}
<<<<<<< HEAD
<<<<<<< HEAD
fn handle_lsp_command(&mut self, action: Option<&str>, target: Option<&str>) {
=======
fn handle_lsp_command(&self, action: Option<&str>, target: Option<&str>) {
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
fn handle_lsp_command(&mut self, action: Option<&str>, target: Option<&str>) {
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
let registry = tools::global_lsp_registry();
match action {
Some("start") => {
let lang = target.unwrap_or("unknown");
match registry.start_server(lang) {
Ok(()) => eprintln!("LSP server '{lang}' started."),
Err(e) => eprintln!("Failed to start LSP server '{lang}': {e}"),
}
}
Some("stop") => {
let lang = target.unwrap_or("unknown");
match registry.stop_server(lang) {
Ok(()) => eprintln!("LSP server '{lang}' stopped."),
Err(e) => eprintln!("Failed to stop LSP server '{lang}': {e}"),
}
}
Some("restart") => {
let lang = target.unwrap_or("unknown");
let _ = registry.stop_server(lang);
match registry.start_server(lang) {
Ok(()) => eprintln!("LSP server '{lang}' restarted."),
Err(e) => eprintln!("Failed to restart LSP server '{lang}': {e}"),
}
}
<<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
Some("toggle") => {
self.lsp_auto_start = !self.lsp_auto_start;
let state = if self.lsp_auto_start { "on" } else { "off" };
eprintln!("LSP auto-start: {state}");
}
_ => {
let servers = registry.list_servers();
let auto_state = if self.lsp_auto_start { "on" } else { "off" };
eprintln!("LSP auto-start: {auto_state}");
<<<<<<< HEAD
=======
_ => {
let servers = registry.list_servers();
>>>>>>> e9582034 (feat: full LSP (Language Server Protocol) integration)
=======
>>>>>>> ab3550e5 (feat(lsp): add lspAutoStart config, remove unused LSP client/process/transport modules)
if servers.is_empty() {
eprintln!("No LSP servers registered.");
} else {
for s in &servers {
eprintln!(" {} [{}]", s.language, s.status);
}
}
}
}
}
fn shutdown_lsp_servers(&self) {
let registry = tools::global_lsp_registry();
for server in registry.list_servers() {
if server.status == runtime::lsp_client::LspServerStatus::Connected {
let _ = registry.stop_server(&server.language);
}
}
}
fn persist_session(&self) -> Result<(), Box<dyn std::error::Error>> {
self.runtime.session().save_to_path(&self.session.path)?;
Ok(())
@ -14557,153 +14002,6 @@ impl ToolExecutor for CliToolExecutor {
}
}
}
fn execute_batch(&mut self, calls: Vec<runtime::ToolCall>) -> Vec<runtime::ToolResult> {
if calls.len() <= 1 {
return calls
.into_iter()
.map(|call| {
let result = self.execute(&call.tool_name, &call.input);
runtime::ToolResult {
tool_use_id: call.tool_use_id,
tool_name: call.tool_name,
result,
}
})
.collect();
}
/// Tools that are safe to run in parallel because they only read
/// state and dispatch through the stateless tool registry.
const PARALLEL_SAFE_TOOLS: &[&str] = &[
"read_file",
"glob_search",
"grep_search",
"WebFetch",
"WebSearch",
"ToolSearch",
"Skill",
"LSP",
"Agent",
"AgentMessage",
"TeamStatus",
"TaskClaim",
"AgentSuggestion",
"ContextRequest", "TaskGet",
"TaskList",
"TaskOutput",
"GitStatus",
"GitDiff",
"GitLog",
"GitShow",
"GitBlame",
];
let emit_output = self.emit_output;
let mut results: Vec<Option<runtime::ToolResult>> = vec![None; calls.len()];
let mut parallel_calls: Vec<(usize, String, String, String)> = Vec::new();
let mut sequential_indices: Vec<usize> = Vec::new();
// Classify calls as parallel-safe or sequential
for (i, call) in calls.iter().enumerate() {
if self
.allowed_tools
.as_ref()
.is_some_and(|allowed| !allowed.contains(&call.tool_name))
{
results[i] = Some(runtime::ToolResult {
tool_use_id: call.tool_use_id.clone(),
tool_name: call.tool_name.clone(),
result: Err(ToolError::new(format!(
"tool `{}` is not enabled by the current --allowedTools setting",
call.tool_name
))),
});
} else if PARALLEL_SAFE_TOOLS.contains(&call.tool_name.as_str())
&& !self.tool_registry.has_runtime_tool(&call.tool_name)
{
parallel_calls.push((
i,
call.tool_use_id.clone(),
call.tool_name.clone(),
call.input.clone(),
));
} else {
sequential_indices.push(i);
}
}
// Execute parallel-safe tools concurrently
if !parallel_calls.is_empty() {
let registry = self.tool_registry.clone();
let parallel_results: Vec<(usize, String, String, Result<String, ToolError>)> =
std::thread::scope(|s| {
let mut handles = Vec::new();
for (idx, tool_use_id, tool_name, input) in &parallel_calls {
let registry = &registry;
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);

View File

@ -233,11 +233,7 @@ fn prompt_fast_model(
main_model: Option<&str>,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
println!();
<<<<<<< HEAD
println!(" \x1b[1mFast Model (for Agent subtasks)\x1b[0m");
=======
println!(" 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.");

View File

@ -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 = &current_text;
let matches: Vec<&String> = self.slash_completions.iter()
let matches: Vec<&String> = self
.slash_completions
.iter()
.filter(|c| c.starts_with(prefix))
.collect();
if matches.len() == 1 {
self.input.select_all();
self.input.cut();
for ch in matches[0].chars() { self.input.insert_char(ch); }
for ch in matches[0].chars() {
self.input.insert_char(ch);
}
self.showing_completions = false;
} else if !matches.is_empty() {
self.showing_completions = true;
@ -471,8 +494,15 @@ fn draw_frame(
.split(size);
draw_left_pane(
f, main[0], conversation, conversation_scroll,
input, input_lines, slash_completions, completion_index, showing_completions,
f,
main[0],
conversation,
conversation_scroll,
input,
input_lines,
slash_completions,
completion_index,
showing_completions,
);
draw_right_pane(f, main[1], dashboard, spinner_frame);
}
@ -495,11 +525,16 @@ fn draw_left_pane(
.split(area);
// --- conversation ---
let conv_lines: Vec<Line> = conversation.iter().map(|line| {
let mut style = Style::default().fg(line.color);
if line.bold { style = style.add_modifier(Modifier::BOLD); }
Line::from(Span::styled(&line.text, style))
}).collect();
let conv_lines: Vec<Line> = conversation
.iter()
.map(|line| {
let mut style = Style::default().fg(line.color);
if line.bold {
style = style.add_modifier(Modifier::BOLD);
}
Line::from(Span::styled(&line.text, style))
})
.collect();
// FIFO viewport: newest content at the bottom, older above.
let pane_rows = (left[0].height.saturating_sub(1) as usize).max(1);
@ -510,13 +545,19 @@ fn draw_left_pane(
let start = total.saturating_sub(pane_rows + offset);
let visible: Vec<Line> = conv_lines.into_iter().skip(start).take(pane_rows).collect();
let conversation_widget = Paragraph::new(visible)
.block(
Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(Color::DarkGray))
.title(Span::styled(" Conversation ", Style::default().fg(Color::DarkGray))),
);
// NOTE: We intentionally do NOT enable Wrap here. The FIFO viewport
// counts Line items to fill the pane — if a Line soft-wraps to 2+
// visual rows the count is off and text misaligns. Long lines get
// clipped rather than wrapped, which keeps scrolling accurate.
let conversation_widget = Paragraph::new(visible).block(
Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(Color::DarkGray))
.title(Span::styled(
" Conversation ",
Style::default().fg(Color::DarkGray),
)),
);
f.render_widget(conversation_widget, left[0]);
// --- input (real TextArea widget) ---
@ -526,21 +567,28 @@ fn draw_left_pane(
// --- completions popup ---
if showing_completions {
let current_text: String = input_lines.join("");
let matches: Vec<&String> = slash_completions.iter()
let matches: Vec<&String> = slash_completions
.iter()
.filter(|c| c.starts_with(current_text.as_str()))
.collect();
if !matches.is_empty() {
let items: Vec<ListItem> = matches.iter().enumerate().map(|(i, m)| {
let style = if i == completion_index % matches.len() {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default().fg(Color::Gray)
};
ListItem::new(Line::from(Span::styled(m.as_str(), style)))
}).collect();
let list = List::new(items)
.block(Block::default().borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)));
let items: Vec<ListItem> = matches
.iter()
.enumerate()
.map(|(i, m)| {
let style = if i == completion_index % matches.len() {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default().fg(Color::Gray)
};
ListItem::new(Line::from(Span::styled(m.as_str(), style)))
})
.collect();
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray)),
);
let popup = Rect {
x: left[1].x,
y: left[1].y.saturating_sub(matches.len().min(8) as u16 + 2),
@ -575,9 +623,21 @@ fn draw_right_pane(
lines.push(kv("Turns", &state.turn_count.to_string(), Color::White));
lines.push(kv("Input", &state.input_tokens.to_string(), Color::White));
lines.push(kv("Output", &state.output_tokens.to_string(), Color::White));
lines.push(kv("Cache R", &state.cache_read_tokens.to_string(), Color::Gray));
lines.push(kv("Cache W", &state.cache_creation_tokens.to_string(), Color::Gray));
lines.push(kv("Cost", &format!("${:.4}", state.cost_usd), Color::Yellow));
lines.push(kv(
"Cache R",
&state.cache_read_tokens.to_string(),
Color::Gray,
));
lines.push(kv(
"Cache W",
&state.cache_creation_tokens.to_string(),
Color::Gray,
));
lines.push(kv(
"Cost",
&format!("${:.4}", state.cost_usd),
Color::Yellow,
));
lines.push(Line::from(""));
let pct = state.context_percent;
@ -587,9 +647,17 @@ fn draw_right_pane(
_ => Color::Green,
};
lines.push(section("Context"));
lines.push(kv("Used", &format!("{:.1}% of {}", pct, state.context_window), Color::White));
lines.push(kv(
"Used",
&format!("{:.1}% of {}", pct, state.context_window),
Color::White,
));
lines.push(Line::from(""));
lines.push(kv("Compactions", &state.compaction_count.to_string(), Color::Gray));
lines.push(kv(
"Compactions",
&state.compaction_count.to_string(),
Color::Gray,
));
lines.push(Line::from(""));
if !state.lsp_servers.is_empty() {
@ -601,7 +669,10 @@ fn draw_right_pane(
_ => Color::Red,
};
lines.push(Line::from(vec![
Span::styled(format!(" {} ", lsp.language), Style::default().fg(Color::White)),
Span::styled(
format!(" {} ", lsp.language),
Style::default().fg(Color::White),
),
Span::styled(lsp.status.clone(), Style::default().fg(c)),
]));
}
@ -613,63 +684,104 @@ fn draw_right_pane(
lines.push(kv("Name", &team.team_name, Color::White));
lines.push(Line::from(vec![
Span::styled(" Progress ", Style::default().fg(Color::Gray)),
Span::styled(format!("{}/{} done", team.completed_agents, team.total_agents), Style::default().fg(Color::Green)),
Span::styled(format!(", {} fail", team.failed_agents), Style::default().fg(Color::Red)),
Span::styled(format!(", {} run", team.running_agents), Style::default().fg(Color::Cyan)),
Span::styled(
format!("{}/{} done", team.completed_agents, team.total_agents),
Style::default().fg(Color::Green),
),
Span::styled(
format!(", {} fail", team.failed_agents),
Style::default().fg(Color::Red),
),
Span::styled(
format!(", {} run", team.running_agents),
Style::default().fg(Color::Cyan),
),
]));
for agent in &team.agents {
let c = match agent.status.as_str() {
"completed" => Color::Green, "failed" => Color::Red, _ => Color::Cyan,
"completed" => Color::Green,
"failed" => Color::Red,
_ => Color::Cyan,
};
lines.push(Line::from(vec![
Span::styled("", Style::default().fg(c)),
Span::styled(&agent.name, Style::default().fg(Color::White)),
Span::styled(format!(" ({})", agent.subagent_type.as_deref().unwrap_or("?")), Style::default().fg(Color::Gray)),
Span::styled(
format!(" ({})", agent.subagent_type.as_deref().unwrap_or("?")),
Style::default().fg(Color::Gray),
),
]));
}
lines.push(Line::from(""));
}
lines.push(section("Session"));
lines.push(kv("ID", state.session_id.as_deref().unwrap_or("-"), Color::Gray));
lines.push(kv(
"ID",
state.session_id.as_deref().unwrap_or("-"),
Color::Gray,
));
if !state.status_message.is_empty() {
let frame = SPINNER_FRAMES[spinner_frame];
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
format!("{frame} {}", state.status_message), Style::default().fg(Color::Blue),
format!("{frame} {}", state.status_message),
Style::default().fg(Color::Blue),
)));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled("─ Keys ─", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(Span::styled(" Enter Submit Shift+Enter Newline", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(Span::styled(" ^P Swap ^T Team ^C Cancel ^D Exit", Style::default().fg(Color::DarkGray))));
lines.push(Line::from(Span::styled(
"─ Keys ─",
Style::default().fg(Color::DarkGray),
)));
lines.push(Line::from(Span::styled(
" Enter Submit Shift+Enter Newline",
Style::default().fg(Color::DarkGray),
)));
lines.push(Line::from(Span::styled(
" ^P Swap ^T Team ^C Cancel ^D Exit",
Style::default().fg(Color::DarkGray),
)));
let widget = Paragraph::new(lines)
.block(
Block::default()
.borders(Borders::LEFT)
.border_style(Style::default().fg(Color::DarkGray))
.title(Span::styled(" Dashboard ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))),
.title(Span::styled(
" Dashboard ",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)),
)
.wrap(Wrap { trim: true });
f.render_widget(widget, area);
let gauge_area = Rect {
x: area.x + 2, y: area.y + 16,
width: area.width.saturating_sub(4), height: 1,
x: area.x + 2,
y: area.y + 16,
width: area.width.saturating_sub(4),
height: 1,
};
let gauge = Gauge::default()
.gauge_style(Style::default().fg(gauge_color).bg(Color::DarkGray))
.ratio(if pct > 0.0 { (pct / 100.0).min(1.0) } else { 0.0 });
.ratio(if pct > 0.0 {
(pct / 100.0).min(1.0)
} else {
0.0
});
f.render_widget(gauge, gauge_area);
}
fn section<'a>(label: &str) -> Line<'a> {
Line::from(Span::styled(
format!("{label}"),
Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
))
}

File diff suppressed because it is too large Load Diff