From c6855bb703936ebc746db8f4a6697861e835e568 Mon Sep 17 00:00:00 2001 From: Isabel Wu <231155141+wuisabel-gif@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:11:40 -0700 Subject: [PATCH 1/2] feat(tui): show tool and MCP schema costs Add bounded per-tool catalog estimates and per-server MCP announcement totals to the context inspector without changing prompt assembly or cache behavior. Cover ordering, truncation, and empty-server cases for #5553. --- CHANGELOG.md | 3 + crates/tui/CHANGELOG.md | 3 + crates/tui/src/tui/context_inspector.rs | 169 +++++++++++++++++++++++- 3 files changed, 173 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c45ba4081..b8e99d5db2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,6 +154,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fleet roster members in a selected Fleet now expose a visible edit affordance and a direct `m` model-picker shortcut, while the coordinator row remains read-only (#5604, covers #5589). +- The context inspector now attributes bounded tool-schema cost to the built-in + catalog and each discovered MCP server without changing prompt assembly or + cache behavior (#5553). - The goal-continuation quiet period (`[goal] continuation_delay_seconds`, added in #5508) now applies on every dispatch path. Previously the within-turn dispatch hook fired the next continuation prompt immediately diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 4ab4786655..f2099ccf76 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -154,6 +154,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fleet roster members in a selected Fleet now expose a visible edit affordance and a direct `m` model-picker shortcut, while the coordinator row remains read-only (#5604, covers #5589). +- The context inspector now attributes bounded tool-schema cost to the built-in + catalog and each discovered MCP server without changing prompt assembly or + cache behavior (#5553). - The goal-continuation quiet period (`[goal] continuation_delay_seconds`, added in #5508) now applies on every dispatch path. Previously the within-turn dispatch hook fired the next continuation prompt immediately diff --git a/crates/tui/src/tui/context_inspector.rs b/crates/tui/src/tui/context_inspector.rs index 0bcb3472d0..58220c618a 100644 --- a/crates/tui/src/tui/context_inspector.rs +++ b/crates/tui/src/tui/context_inspector.rs @@ -16,7 +16,7 @@ use ratatui::{ use crate::compaction::estimate_input_tokens_for_pressure; use crate::localization::{Locale, MessageId, tr}; -use crate::models::SystemPrompt; +use crate::models::{SystemPrompt, Tool}; use crate::palette; use crate::session_manager::SessionContextReference; use crate::tui::app::{App, ToolDetailRecord}; @@ -36,6 +36,8 @@ pub(crate) const CONTEXT_WARNING_THRESHOLD_PERCENT: f64 = 85.0; pub(crate) const CONTEXT_CRITICAL_THRESHOLD_PERCENT: f64 = 95.0; const MAX_REFERENCE_ROWS: usize = 12; const MAX_TOOL_ROWS: usize = 8; +const MAX_SCHEMA_COST_ROWS: usize = 24; +const SCHEMA_TOKEN_DIVISOR: usize = 4; const SYSTEM_LAYER_MARKERS: &[(&str, &str, PromptLayerKind)] = &[ ( @@ -226,6 +228,7 @@ pub fn build_context_inspector_text(app: &App, locale: Locale) -> String { push_references(&mut out, &app.session_context_references, locale); let _ = writeln!(out); push_tools(&mut out, app, locale); + push_tool_schema_costs(&mut out, app, locale); out } @@ -557,6 +560,69 @@ fn push_tool_row(out: &mut String, locale: Locale, location: &str, detail: &Tool ); } +fn tool_schema_tokens(tool: &Tool) -> usize { + serde_json::to_string(tool) + .map(|schema| schema.chars().count().div_ceil(SCHEMA_TOKEN_DIVISOR)) + .unwrap_or_default() +} + +fn push_tool_schema_costs(out: &mut String, app: &App, locale: Locale) { + let Some(catalog) = app.session.last_tool_catalog.as_ref() else { + return; + }; + + let tokens = tr(locale, MessageId::CtxInspTokens); + let tools_label = tr(locale, MessageId::CtxInspRecentTools); + let _ = writeln!(out, "{} ({})", tools_label, tokens); + let _ = writeln!(out, "------------"); + + let mut built_in: Vec<(String, usize)> = catalog + .iter() + .filter(|tool| !crate::mcp::McpPool::is_mcp_tool(&tool.name)) + .map(|tool| (tool.name.clone(), tool_schema_tokens(tool))) + .collect(); + built_in.sort_by(|left, right| right.1.cmp(&left.1).then_with(|| left.0.cmp(&right.0))); + let built_in_total: usize = built_in.iter().map(|(_, cost)| cost).sum(); + let _ = writeln!( + out, + "- [catalog] ~{built_in_total} {tokens} ({} tools)", + built_in.len() + ); + for (name, cost) in built_in.iter().take(MAX_SCHEMA_COST_ROWS) { + let _ = writeln!(out, " - {name}: ~{cost} {tokens}"); + } + if built_in.len() > MAX_SCHEMA_COST_ROWS { + let _ = writeln!( + out, + " - ... {} more catalog tools", + built_in.len() - MAX_SCHEMA_COST_ROWS + ); + } + + let Some(snapshot) = app.mcp_snapshot.as_ref() else { + return; + }; + for server in &snapshot.servers { + let mut server_tokens = 0usize; + let mut catalog_tools = 0usize; + for announced in &server.tools { + if let Some(tool) = catalog + .iter() + .find(|tool| tool.name == announced.model_name) + { + server_tokens += tool_schema_tokens(tool); + catalog_tools += 1; + } + } + let _ = writeln!( + out, + "- [mcp:{}] ~{server_tokens} {tokens} ({catalog_tools}/{} announced tools)", + server.name, + server.tools.len() + ); + } +} + fn short_tool_id(id: &str) -> String { // Slice by characters, not bytes: a tool id from a gateway can contain // multibyte characters, and `&id[..8]` panics on a byte index that lands @@ -839,7 +905,8 @@ mod tests { assert_eq!(short_tool_id("café"), "café"); } - use crate::models::{ContentBlock, Message}; + use crate::mcp::{McpDiscoveredItem, McpManagerSnapshot, McpServerSnapshot}; + use crate::models::{ContentBlock, Message, Tool}; use crate::session_manager::SessionContextReference; use crate::tui::app::TuiOptions; use crate::tui::file_mention::{ @@ -912,6 +979,104 @@ mod tests { assert!(text.contains("No tool activity recorded yet.")); } + fn schema_tool(name: &str, property_count: usize) -> Tool { + let properties = (0..property_count) + .map(|index| { + ( + format!("field_{index}"), + serde_json::json!({"type": "string"}), + ) + }) + .collect::>(); + Tool { + tool_type: Some("function".to_string()), + name: name.to_string(), + description: format!("schema for {name}"), + input_schema: serde_json::json!({ + "type": "object", + "properties": properties, + }), + allowed_callers: None, + defer_loading: None, + input_examples: None, + strict: None, + cache_control: None, + } + } + + #[test] + fn inspector_reports_catalog_and_mcp_schema_costs_with_bounded_rows() { + let mut app = test_app(); + let mut catalog = (0..(MAX_SCHEMA_COST_ROWS + 2)) + .map(|index| schema_tool(&format!("tool_{index}"), index + 1)) + .collect::>(); + catalog.push(schema_tool("mcp_local_echo", 3)); + app.session.last_tool_catalog = Some(catalog); + app.mcp_snapshot = Some(McpManagerSnapshot { + config_path: PathBuf::from("/tmp/mcp.json"), + config_exists: true, + reload_required: false, + servers: vec![ + McpServerSnapshot { + name: "local".to_string(), + enabled: true, + required: false, + transport: "stdio".to_string(), + command_or_url: "echo".to_string(), + connect_timeout: 1, + execute_timeout: 1, + read_timeout: 1, + connected: true, + error: None, + capability_metadata: Default::default(), + tools: vec![McpDiscoveredItem { + name: "echo".to_string(), + model_name: "mcp_local_echo".to_string(), + description: Some("echoes input".to_string()), + }], + resources: Vec::new(), + prompts: Vec::new(), + }, + McpServerSnapshot { + name: "empty".to_string(), + enabled: true, + required: false, + transport: "stdio".to_string(), + command_or_url: "empty".to_string(), + connect_timeout: 1, + execute_timeout: 1, + read_timeout: 1, + connected: true, + error: None, + capability_metadata: Default::default(), + tools: Vec::new(), + resources: Vec::new(), + prompts: Vec::new(), + }, + ], + }); + + let text = build_context_inspector_text(&app, Locale::En); + assert!(text.contains("[catalog]"), "catalog total missing: {text}"); + assert!(text.contains("tool_25"), "catalog row missing: {text}"); + assert!( + text.contains("more catalog tools"), + "catalog bound missing: {text}" + ); + assert!( + text.contains("[mcp:local]"), + "MCP server row missing: {text}" + ); + assert!( + text.contains("1/1 announced tools"), + "MCP tool cost missing: {text}" + ); + assert!( + text.contains("[mcp:empty] ~0"), + "empty MCP row missing: {text}" + ); + } + #[test] fn inspector_uses_compact_session_id() { let mut app = test_app(); From e82a70e2390d14a0140f2e9ded362593dcc76478 Mon Sep 17 00:00:00 2001 From: CodeWhale Bot Date: Thu, 27 Aug 2026 01:12:30 -0700 Subject: [PATCH 2/2] fix(tui): give schema costs a distinct localized section Separate the contributed tool/MCP schema-cost report from Recent Tools and add complete locale coverage, so the context inspector does not render two adjacent sections under the same heading. Signed-off-by: CodeWhale Bot --- crates/tui/locales/ca.json | 1 + crates/tui/locales/de.json | 1 + crates/tui/locales/en.json | 1 + crates/tui/locales/es-419.json | 1 + crates/tui/locales/fr.json | 1 + crates/tui/locales/hi.json | 1 + crates/tui/locales/id.json | 1 + crates/tui/locales/ja.json | 1 + crates/tui/locales/ko.json | 1 + crates/tui/locales/pt-BR.json | 1 + crates/tui/locales/ru.json | 1 + crates/tui/locales/uk.json | 1 + crates/tui/locales/vi.json | 1 + crates/tui/locales/zh-Hans.json | 1 + crates/tui/locales/zh-Hant.json | 1 + crates/tui/src/localization.rs | 2 ++ crates/tui/src/tui/context_inspector.rs | 14 ++++++++++++-- 17 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/tui/locales/ca.json b/crates/tui/locales/ca.json index bee1301045..c07e361d79 100644 --- a/crates/tui/locales/ca.json +++ b/crates/tui/locales/ca.json @@ -947,6 +947,7 @@ "CtxInspSystemPrompt": "Estructura del prompt del sistema", "CtxInspReferences": "Referències", "CtxInspRecentTools": "Eines recents", + "CtxInspToolSchemaCosts": "Costos dels esquemes d’eines", "CtxInspModel": "Model", "CtxInspWorkspace": "Espai de treball", "CtxInspSession": "Sessió", diff --git a/crates/tui/locales/de.json b/crates/tui/locales/de.json index 81c2df59f7..5c40a20743 100644 --- a/crates/tui/locales/de.json +++ b/crates/tui/locales/de.json @@ -947,6 +947,7 @@ "CtxInspSystemPrompt": "System-Prompt-Struktur", "CtxInspReferences": "Referenzen", "CtxInspRecentTools": "Letzte Tools", + "CtxInspToolSchemaCosts": "Kosten der Tool-Schemata", "CtxInspModel": "Modell", "CtxInspWorkspace": "Workspace", "CtxInspSession": "Sitzung", diff --git a/crates/tui/locales/en.json b/crates/tui/locales/en.json index 3a7e28a199..09111c1726 100644 --- a/crates/tui/locales/en.json +++ b/crates/tui/locales/en.json @@ -970,6 +970,7 @@ "CtxInspSystemPrompt": "System Prompt Structure", "CtxInspReferences": "References", "CtxInspRecentTools": "Recent Tools", + "CtxInspToolSchemaCosts": "Tool schema costs", "CtxInspModel": "Model", "CtxInspWorkspace": "Workspace", "CtxInspSession": "Session", diff --git a/crates/tui/locales/es-419.json b/crates/tui/locales/es-419.json index 241cd83d17..c2f8426328 100644 --- a/crates/tui/locales/es-419.json +++ b/crates/tui/locales/es-419.json @@ -968,6 +968,7 @@ "CtxInspSystemPrompt": "Estructura del prompt del sistema", "CtxInspReferences": "Referencias", "CtxInspRecentTools": "Herramientas recientes", + "CtxInspToolSchemaCosts": "Costos de esquemas de herramientas", "CtxInspModel": "Modelo", "CtxInspWorkspace": "Espacio de trabajo", "CtxInspSession": "Sesión", diff --git a/crates/tui/locales/fr.json b/crates/tui/locales/fr.json index c59babbedd..795cccfc8e 100644 --- a/crates/tui/locales/fr.json +++ b/crates/tui/locales/fr.json @@ -947,6 +947,7 @@ "CtxInspSystemPrompt": "Structure du prompt système", "CtxInspReferences": "Références", "CtxInspRecentTools": "Outils récents", + "CtxInspToolSchemaCosts": "Coûts des schémas d’outils", "CtxInspModel": "Modèle", "CtxInspWorkspace": "Workspace", "CtxInspSession": "Session", diff --git a/crates/tui/locales/hi.json b/crates/tui/locales/hi.json index c52d04d669..14513b987d 100644 --- a/crates/tui/locales/hi.json +++ b/crates/tui/locales/hi.json @@ -947,6 +947,7 @@ "CtxInspSystemPrompt": "सिस्टम प्रॉम्प्ट संरचना", "CtxInspReferences": "संदर्भ", "CtxInspRecentTools": "हाल के टूल", + "CtxInspToolSchemaCosts": "टूल स्कीमा की लागत", "CtxInspModel": "मॉडल", "CtxInspWorkspace": "वर्कस्पेस", "CtxInspSession": "सत्र", diff --git a/crates/tui/locales/id.json b/crates/tui/locales/id.json index 1cca42d307..b4b15a087a 100644 --- a/crates/tui/locales/id.json +++ b/crates/tui/locales/id.json @@ -947,6 +947,7 @@ "CtxInspSystemPrompt": "Struktur System Prompt", "CtxInspReferences": "Referensi", "CtxInspRecentTools": "Tool Terbaru", + "CtxInspToolSchemaCosts": "Biaya skema tool", "CtxInspModel": "Model", "CtxInspWorkspace": "Workspace", "CtxInspSession": "Sesi", diff --git a/crates/tui/locales/ja.json b/crates/tui/locales/ja.json index 0f24dde957..05b9f15081 100644 --- a/crates/tui/locales/ja.json +++ b/crates/tui/locales/ja.json @@ -968,6 +968,7 @@ "CtxInspSystemPrompt": "システムプロンプト構造", "CtxInspReferences": "参照", "CtxInspRecentTools": "最近のツール", + "CtxInspToolSchemaCosts": "ツールスキーマのコスト", "CtxInspModel": "モデル", "CtxInspWorkspace": "ワークスペース", "CtxInspSession": "セッション", diff --git a/crates/tui/locales/ko.json b/crates/tui/locales/ko.json index 924fdb94c6..cee3b02206 100644 --- a/crates/tui/locales/ko.json +++ b/crates/tui/locales/ko.json @@ -970,6 +970,7 @@ "CtxInspSystemPrompt": "시스템 프롬프트 구조", "CtxInspReferences": "참조", "CtxInspRecentTools": "최근 도구", + "CtxInspToolSchemaCosts": "도구 스키마 비용", "CtxInspModel": "모델", "CtxInspWorkspace": "작업 공간", "CtxInspSession": "세션", diff --git a/crates/tui/locales/pt-BR.json b/crates/tui/locales/pt-BR.json index b0a6f4b9a7..799e6f8cc1 100644 --- a/crates/tui/locales/pt-BR.json +++ b/crates/tui/locales/pt-BR.json @@ -968,6 +968,7 @@ "CtxInspSystemPrompt": "Estrutura do prompt do sistema", "CtxInspReferences": "Referências", "CtxInspRecentTools": "Ferramentas recentes", + "CtxInspToolSchemaCosts": "Custos dos esquemas de ferramentas", "CtxInspModel": "Modelo", "CtxInspWorkspace": "Espaço de trabalho", "CtxInspSession": "Sessão", diff --git a/crates/tui/locales/ru.json b/crates/tui/locales/ru.json index 36232f5664..1e5fb0d8ad 100644 --- a/crates/tui/locales/ru.json +++ b/crates/tui/locales/ru.json @@ -947,6 +947,7 @@ "CtxInspSystemPrompt": "Структура системного промпта", "CtxInspReferences": "Ссылки", "CtxInspRecentTools": "Недавние инструменты", + "CtxInspToolSchemaCosts": "Стоимость схем инструментов", "CtxInspModel": "Модель", "CtxInspWorkspace": "Рабочая область", "CtxInspSession": "Сессия", diff --git a/crates/tui/locales/uk.json b/crates/tui/locales/uk.json index 0f53c65cf5..c8983df2f4 100644 --- a/crates/tui/locales/uk.json +++ b/crates/tui/locales/uk.json @@ -947,6 +947,7 @@ "CtxInspSystemPrompt": "Структура системного промпту", "CtxInspReferences": "Посилання", "CtxInspRecentTools": "Недавні інструменти", + "CtxInspToolSchemaCosts": "Вартість схем інструментів", "CtxInspModel": "Модель", "CtxInspWorkspace": "Робочий простір", "CtxInspSession": "Сесія", diff --git a/crates/tui/locales/vi.json b/crates/tui/locales/vi.json index f25a7ecc3c..87c3ec2def 100644 --- a/crates/tui/locales/vi.json +++ b/crates/tui/locales/vi.json @@ -968,6 +968,7 @@ "CtxInspSystemPrompt": "Cấu trúc lời nhắc hệ thống", "CtxInspReferences": "Tham chiếu", "CtxInspRecentTools": "Công cụ gần đây", + "CtxInspToolSchemaCosts": "Chi phí lược đồ công cụ", "CtxInspModel": "Mô hình", "CtxInspWorkspace": "Không gian làm việc", "CtxInspSession": "Phiên", diff --git a/crates/tui/locales/zh-Hans.json b/crates/tui/locales/zh-Hans.json index 43d82560f7..8025de9bf7 100644 --- a/crates/tui/locales/zh-Hans.json +++ b/crates/tui/locales/zh-Hans.json @@ -968,6 +968,7 @@ "CtxInspSystemPrompt": "系统提示结构", "CtxInspReferences": "引用", "CtxInspRecentTools": "最近使用的工具", + "CtxInspToolSchemaCosts": "工具架构成本", "CtxInspModel": "模型", "CtxInspWorkspace": "工作区", "CtxInspSession": "会话", diff --git a/crates/tui/locales/zh-Hant.json b/crates/tui/locales/zh-Hant.json index a7af854305..e2c49f4a9b 100644 --- a/crates/tui/locales/zh-Hant.json +++ b/crates/tui/locales/zh-Hant.json @@ -640,6 +640,7 @@ "CtxInspOk": "正常", "CtxInspOutputCaptured": "已捕獲輸出", "CtxInspRecentTools": "最近使用的工具", + "CtxInspToolSchemaCosts": "工具結構描述成本", "CtxInspReferences": "引用", "CtxInspRowFree": "剩餘", "CtxInspRowMessages": "訊息", diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index a9ac6959ab..866f040d44 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -1156,6 +1156,7 @@ pub enum MessageId { CtxInspSystemPrompt, CtxInspReferences, CtxInspRecentTools, + CtxInspToolSchemaCosts, CtxInspModel, CtxInspWorkspace, CtxInspSession, @@ -2900,6 +2901,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::CtxInspSystemPrompt, MessageId::CtxInspReferences, MessageId::CtxInspRecentTools, + MessageId::CtxInspToolSchemaCosts, MessageId::CtxInspModel, MessageId::CtxInspWorkspace, MessageId::CtxInspSession, diff --git a/crates/tui/src/tui/context_inspector.rs b/crates/tui/src/tui/context_inspector.rs index 58220c618a..014777abff 100644 --- a/crates/tui/src/tui/context_inspector.rs +++ b/crates/tui/src/tui/context_inspector.rs @@ -571,9 +571,10 @@ fn push_tool_schema_costs(out: &mut String, app: &App, locale: Locale) { return; }; + let _ = writeln!(out); let tokens = tr(locale, MessageId::CtxInspTokens); - let tools_label = tr(locale, MessageId::CtxInspRecentTools); - let _ = writeln!(out, "{} ({})", tools_label, tokens); + let schema_costs_label = tr(locale, MessageId::CtxInspToolSchemaCosts); + let _ = writeln!(out, "{} ({})", schema_costs_label, tokens); let _ = writeln!(out, "------------"); let mut built_in: Vec<(String, usize)> = catalog @@ -1057,6 +1058,15 @@ mod tests { }); let text = build_context_inspector_text(&app, Locale::En); + assert_eq!( + text.matches("Recent Tools").count(), + 1, + "schema costs need a distinct section heading: {text}" + ); + assert!( + text.contains("\n\nTool schema costs (tokens)\n------------"), + "schema costs need a separated localized section: {text}" + ); assert!(text.contains("[catalog]"), "catalog total missing: {text}"); assert!(text.contains("tool_25"), "catalog row missing: {text}"); assert!(