Skip to content

Commit 61485c6

Browse files
committed
feat(tui): add skills and MCP server count indicator in status bar
Rework per review on #399, rebased on the app/ module split: - discovery no longer runs inside App::new: the count is computed on a spawn_blocking background task (discover_skills can shell out to git clone via skills.urls) and drained through a one-shot channel, mirroring recent_sessions_pending - mcp count reads McpManager::server_count() (connected servers), set by the CLI at startup and on every reconnect, not the configured list - footer emits only the non-zero halves: 3 skills / 2 mcp instead of '3 skills 0 mcp' - comment indentation fixed
1 parent b0637c9 commit 61485c6

4 files changed

Lines changed: 94 additions & 0 deletions

File tree

src-rust/crates/cli/src/main.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1874,6 +1874,13 @@ async fn run_interactive(
18741874
// Set up terminal
18751875
let mut terminal = setup_terminal(live_config.mouse_capture_enabled())?;
18761876
let mut app = App::new(live_config.clone(), cost_tracker.clone());
1877+
// Status-bar indicator tracks *connected* servers, not the configured
1878+
// list (a server that fails to handshake is configured but not connected).
1879+
app.mcp_server_count = tool_ctx
1880+
.mcp_manager
1881+
.as_ref()
1882+
.map(|manager| manager.server_count())
1883+
.unwrap_or(0);
18771884
if let Some(error) = settings_load_error {
18781885
app.invalid_config_dialog =
18791886
claurst_tui::InvalidConfigDialogState::show_settings_error(&error);
@@ -4194,6 +4201,11 @@ async fn run_interactive(
41944201
let new_mcp_manager = connect_mcp_manager_arc(&decision.allowed).await;
41954202
tool_ctx.mcp_manager = new_mcp_manager.clone();
41964203
app.mcp_manager = new_mcp_manager.clone();
4204+
// Keep the status-bar indicator in sync with the live count.
4205+
app.mcp_server_count = new_mcp_manager
4206+
.as_ref()
4207+
.map(|manager| manager.server_count())
4208+
.unwrap_or(0);
41974209
tools_arc = build_tools_with_mcp(new_mcp_manager.clone());
41984210
if app.mcp_view.visible {
41994211
app.refresh_mcp_view();

src-rust/crates/tui/src/app/mod.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,19 @@ pub struct App {
246246
pub remote_session_url: Option<String>,
247247
/// Live MCP manager snapshot source when available.
248248
pub mcp_manager: Option<Arc<claurst_mcp::McpManager>>,
249+
/// Number of connected MCP servers for the status-bar indicator. Updated
250+
/// by the caller (CLI) at startup and on every MCP reconnect so it tracks
251+
/// `McpManager::server_count()`, not just the configured list.
252+
pub mcp_server_count: usize,
253+
/// Number of discovered skills for the status-bar indicator. Populated
254+
/// once in the background from the skill loader (discovery can shell out
255+
/// to `git clone`, so it must not run inside `App::new`).
256+
pub skill_count: usize,
257+
/// When `true`, the main event loop should spawn a one-shot background
258+
/// task to discover skills and report the count.
259+
pub skill_count_pending: bool,
260+
/// Receiver for the background skill-count discovery.
261+
pub skill_count_rx: Option<tokio::sync::mpsc::Receiver<usize>>,
249262
/// Queued request for a real MCP reconnect from the interactive loop.
250263
pub pending_mcp_reconnect: bool,
251264
/// Set after an in-session provider connection (e.g. a Claude Pro/Max OAuth
@@ -647,6 +660,12 @@ impl App {
647660
session_title: None,
648661
remote_session_url: None,
649662
mcp_manager: None,
663+
mcp_server_count: 0,
664+
skill_count: 0,
665+
// Discover skills once, lazily, on the first run-loop iteration
666+
// (never inside the constructor — see the field docs).
667+
skill_count_pending: true,
668+
skill_count_rx: None,
650669
pending_mcp_reconnect: false,
651670
pending_provider_reload: false,
652671
pending_mcp_panel_auth: None,

src-rust/crates/tui/src/app/run.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,39 @@ impl App {
393393
});
394394
}
395395

396+
// Drain the one-shot skill-count discovery into the status-bar
397+
// indicator. Discovery runs on a background task because it can
398+
// shell out to `git clone` for `skills.urls` entries.
399+
if let Some(ref mut rx) = self.skill_count_rx {
400+
match rx.try_recv() {
401+
Ok(count) => {
402+
self.skill_count = count;
403+
self.skill_count_rx = None;
404+
}
405+
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
406+
self.skill_count_rx = None;
407+
}
408+
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => {}
409+
}
410+
}
411+
412+
// Spawn the one-shot skill discovery when requested (startup).
413+
if self.skill_count_pending {
414+
self.skill_count_pending = false;
415+
let root = self.project_root();
416+
let skills_config = self.config.skills.clone();
417+
let (tx, rx) = tokio::sync::mpsc::channel(1);
418+
self.skill_count_rx = Some(rx);
419+
tokio::spawn(async move {
420+
let count = tokio::task::spawn_blocking(move || {
421+
claurst_core::discover_skills(&root, &skills_config).len()
422+
})
423+
.await
424+
.unwrap_or(0);
425+
let _ = tx.send(count).await;
426+
});
427+
}
428+
396429
// Drain voice transcription events (non-blocking).
397430
// When the background recording/transcription task emits a
398431
// TranscriptReady event we insert the text directly into the

src-rust/crates/tui/src/render.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2677,6 +2677,36 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
26772677
));
26782678
}
26792679

2680+
// 3c. Skills + MCP count indicator: emit only the non-zero
2681+
// halves so a user with skills but no MCP servers sees
2682+
// "3 skills" rather than "3 skills + 0 mcp".
2683+
if app.skill_count > 0 || app.mcp_server_count > 0 {
2684+
let mut label = String::new();
2685+
if app.skill_count > 0 {
2686+
label.push_str(&format!(
2687+
"{} skill{}",
2688+
app.skill_count,
2689+
if app.skill_count == 1 { "" } else { "s" }
2690+
));
2691+
}
2692+
if app.mcp_server_count > 0 {
2693+
if !label.is_empty() {
2694+
label.push_str(" \u{00b7} ");
2695+
}
2696+
label.push_str(&format!(
2697+
"{} mcp",
2698+
app.mcp_server_count
2699+
));
2700+
}
2701+
if !parts.is_empty() {
2702+
parts.push(Span::raw(" "));
2703+
}
2704+
parts.push(Span::styled(
2705+
label,
2706+
Style::default().fg(Color::DarkGray),
2707+
));
2708+
}
2709+
26802710
// 4. Rate limits
26812711
if let Some(pct) = app.rate_limit_5h_pct {
26822712
if pct > 0.0 {

0 commit comments

Comments
 (0)