diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e468e9f..03aa4f0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -264,6 +264,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The TUI footer, `/status`, `/mcp` manager, and command-palette MCP entries + now count trusted workspace-local `.codewhale/mcp.json` servers together with + the global MCP config, matching `codewhale mcp list` for merged global + + project setups (#2787). Thanks @yekern for the detailed reproduction. - Sub-agent shell completions now refresh the workspace branch/status chip immediately, and `/subagents` plus the Agents sidebar show each sub-agent's current workspace branch when it is running in a child worktree. diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 1e468e9f..03aa4f0b 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -264,6 +264,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The TUI footer, `/status`, `/mcp` manager, and command-palette MCP entries + now count trusted workspace-local `.codewhale/mcp.json` servers together with + the global MCP config, matching `codewhale mcp list` for merged global + + project setups (#2787). Thanks @yekern for the detailed reproduction. - Sub-agent shell completions now refresh the workspace branch/status chip immediately, and `/subagents` plus the Agents sidebar show each sub-agent's current workspace branch when it is running in a child worktree. diff --git a/crates/tui/src/mcp.rs b/crates/tui/src/mcp.rs index 31f50938..39d5875e 100644 --- a/crates/tui/src/mcp.rs +++ b/crates/tui/src/mcp.rs @@ -2859,6 +2859,7 @@ pub fn set_server_enabled(path: &Path, name: &str, enabled: bool) -> Result<()> save_config(path, &cfg) } +#[cfg(test)] pub fn manager_snapshot_from_config( path: &Path, restart_required: bool, @@ -2873,6 +2874,22 @@ pub fn manager_snapshot_from_config( )) } +pub fn manager_snapshot_from_config_with_workspace( + path: &Path, + workspace: &Path, + restart_required: bool, +) -> Result { + let cfg = load_config_with_workspace(path, workspace)?; + Ok(snapshot_from_config( + path, + path.exists(), + restart_required, + &cfg, + None, + )) +} + +#[cfg(test)] pub async fn discover_manager_snapshot( path: &Path, network_policy: Option, @@ -2898,6 +2915,32 @@ pub async fn discover_manager_snapshot( )) } +pub async fn discover_manager_snapshot_with_workspace( + path: &Path, + workspace: &Path, + network_policy: Option, + restart_required: bool, +) -> Result { + let cfg = load_config_with_workspace(path, workspace)?; + let mut pool = McpPool::new(cfg.clone()); + if let Some(policy) = network_policy { + pool = pool.with_network_policy(policy); + } + let errors = pool + .connect_all() + .await + .into_iter() + .map(|(name, err)| (name, format!("{err:#}"))) + .collect::>(); + Ok(snapshot_from_config( + path, + path.exists(), + restart_required, + &cfg, + Some((&pool, &errors)), + )) +} + fn snapshot_from_config( path: &Path, config_exists: bool, @@ -3385,6 +3428,49 @@ mod tests { assert_eq!(shared.cwd.as_deref(), Some(workspace.as_path())); } + #[test] + fn workspace_manager_snapshot_counts_global_and_project_servers() { + let dir = tempfile::tempdir().unwrap(); + let global_path = dir.path().join("global-mcp.json"); + let workspace = dir.path().join("workspace"); + let project_dir = workspace.join(".codewhale"); + fs::create_dir_all(&project_dir).unwrap(); + let _trust = mark_workspace_trusted(&workspace); + fs::write( + &global_path, + r#"{ + "servers": { + "chrome-devtools": {"command": "npx", "args": ["-y", "chrome-devtools-mcp@latest"]}, + "context7": {"command": "npx", "args": ["-y", "@upstash/context7-mcp@latest"]} + } + }"#, + ) + .unwrap(); + fs::write( + project_dir.join("mcp.json"), + r#"{ + "servers": { + "laravel-boost": {"command": "php", "args": ["artisan", "boost:mcp"]} + } + }"#, + ) + .unwrap(); + + let plain = manager_snapshot_from_config(&global_path, false).unwrap(); + let merged = + manager_snapshot_from_config_with_workspace(&global_path, &workspace, false).unwrap(); + + assert_eq!(plain.servers.len(), 2); + assert_eq!(merged.servers.len(), 3); + assert!( + merged + .servers + .iter() + .any(|server| server.name == "laravel-boost"), + "workspace-aware snapshots must include trusted project MCP servers" + ); + } + #[test] fn workspace_mcp_config_ignores_project_file_until_workspace_trusted() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 319aa507..ebf6d34a 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -2043,6 +2043,10 @@ impl App { } _ => (String::new(), 0, false), }; + let mcp_configured_count = + crate::mcp::load_config_with_workspace(&mcp_config_path, &workspace) + .map(|cfg| cfg.servers.len()) + .unwrap_or(0); Self { mode: initial_mode, composer: ComposerState { @@ -2201,11 +2205,9 @@ impl App { // Read the MCP config once at boot to know how many servers // the user has declared. The footer chip uses this even when // no live snapshot is available (#502). Cheap (just reads - // the JSON file); errors fall through to zero so a missing + // the JSON files); errors fall through to zero so a missing // or malformed config simply hides the chip. - mcp_configured_count: crate::mcp::load_config(&mcp_config_path) - .map(|cfg| cfg.servers.len()) - .unwrap_or(0), + mcp_configured_count, mcp_restart_required: false, tool_log: Vec::new(), active_skill: None, diff --git a/crates/tui/src/tui/command_palette.rs b/crates/tui/src/tui/command_palette.rs index 853b09ae..1eef0f4b 100644 --- a/crates/tui/src/tui/command_palette.rs +++ b/crates/tui/src/tui/command_palette.rs @@ -162,7 +162,7 @@ pub fn build_entries( tool_entries.sort_by(|a, b| a.label.cmp(&b.label)); entries.extend(tool_entries); - entries.extend(build_mcp_entries(mcp_config_path, mcp_snapshot)); + entries.extend(build_mcp_entries(workspace, mcp_config_path, mcp_snapshot)); entries.sort_by(|a, b| a.label.cmp(&b.label)); entries.sort_by_key(|entry| entry.section); @@ -170,11 +170,13 @@ pub fn build_entries( } fn build_mcp_entries( + workspace: &Path, mcp_config_path: &Path, mcp_snapshot: Option<&crate::mcp::McpManagerSnapshot>, ) -> Vec { let owned_snapshot = if mcp_snapshot.is_none() { - crate::mcp::manager_snapshot_from_config(mcp_config_path, false).ok() + crate::mcp::manager_snapshot_from_config_with_workspace(mcp_config_path, workspace, false) + .ok() } else { None }; diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 0af424c2..2f3b7ce1 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -6529,9 +6529,19 @@ async fn handle_mcp_ui_action( let network_policy = config.network.clone().map(|toml_cfg| { crate::network_policy::NetworkPolicyDecider::with_default_audit(toml_cfg.into_runtime()) }); - mcp::discover_manager_snapshot(&path, network_policy, app.mcp_restart_required).await + mcp::discover_manager_snapshot_with_workspace( + &path, + &app.workspace, + network_policy, + app.mcp_restart_required, + ) + .await } else { - mcp::manager_snapshot_from_config(&path, app.mcp_restart_required) + mcp::manager_snapshot_from_config_with_workspace( + &path, + &app.workspace, + app.mcp_restart_required, + ) }; match snapshot_result {