Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions crates/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -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ó",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@
"CtxInspSystemPrompt": "System-Prompt-Struktur",
"CtxInspReferences": "Referenzen",
"CtxInspRecentTools": "Letzte Tools",
"CtxInspToolSchemaCosts": "Kosten der Tool-Schemata",
"CtxInspModel": "Modell",
"CtxInspWorkspace": "Workspace",
"CtxInspSession": "Sitzung",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@
"CtxInspSystemPrompt": "System Prompt Structure",
"CtxInspReferences": "References",
"CtxInspRecentTools": "Recent Tools",
"CtxInspToolSchemaCosts": "Tool schema costs",
"CtxInspModel": "Model",
"CtxInspWorkspace": "Workspace",
"CtxInspSession": "Session",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/es-419.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@
"CtxInspSystemPrompt": "सिस्टम प्रॉम्प्ट संरचना",
"CtxInspReferences": "संदर्भ",
"CtxInspRecentTools": "हाल के टूल",
"CtxInspToolSchemaCosts": "टूल स्कीमा की लागत",
"CtxInspModel": "मॉडल",
"CtxInspWorkspace": "वर्कस्पेस",
"CtxInspSession": "सत्र",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@
"CtxInspSystemPrompt": "Struktur System Prompt",
"CtxInspReferences": "Referensi",
"CtxInspRecentTools": "Tool Terbaru",
"CtxInspToolSchemaCosts": "Biaya skema tool",
"CtxInspModel": "Model",
"CtxInspWorkspace": "Workspace",
"CtxInspSession": "Sesi",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@
"CtxInspSystemPrompt": "システムプロンプト構造",
"CtxInspReferences": "参照",
"CtxInspRecentTools": "最近のツール",
"CtxInspToolSchemaCosts": "ツールスキーマのコスト",
"CtxInspModel": "モデル",
"CtxInspWorkspace": "ワークスペース",
"CtxInspSession": "セッション",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@
"CtxInspSystemPrompt": "시스템 프롬프트 구조",
"CtxInspReferences": "참조",
"CtxInspRecentTools": "최근 도구",
"CtxInspToolSchemaCosts": "도구 스키마 비용",
"CtxInspModel": "모델",
"CtxInspWorkspace": "작업 공간",
"CtxInspSession": "세션",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@
"CtxInspSystemPrompt": "Структура системного промпта",
"CtxInspReferences": "Ссылки",
"CtxInspRecentTools": "Недавние инструменты",
"CtxInspToolSchemaCosts": "Стоимость схем инструментов",
"CtxInspModel": "Модель",
"CtxInspWorkspace": "Рабочая область",
"CtxInspSession": "Сессия",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,7 @@
"CtxInspSystemPrompt": "Структура системного промпту",
"CtxInspReferences": "Посилання",
"CtxInspRecentTools": "Недавні інструменти",
"CtxInspToolSchemaCosts": "Вартість схем інструментів",
"CtxInspModel": "Модель",
"CtxInspWorkspace": "Робочий простір",
"CtxInspSession": "Сесія",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/zh-Hans.json
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@
"CtxInspSystemPrompt": "系统提示结构",
"CtxInspReferences": "引用",
"CtxInspRecentTools": "最近使用的工具",
"CtxInspToolSchemaCosts": "工具架构成本",
"CtxInspModel": "模型",
"CtxInspWorkspace": "工作区",
"CtxInspSession": "会话",
Expand Down
1 change: 1 addition & 0 deletions crates/tui/locales/zh-Hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@
"CtxInspOk": "正常",
"CtxInspOutputCaptured": "已捕獲輸出",
"CtxInspRecentTools": "最近使用的工具",
"CtxInspToolSchemaCosts": "工具結構描述成本",
"CtxInspReferences": "引用",
"CtxInspRowFree": "剩餘",
"CtxInspRowMessages": "訊息",
Expand Down
2 changes: 2 additions & 0 deletions crates/tui/src/localization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,7 @@ pub enum MessageId {
CtxInspSystemPrompt,
CtxInspReferences,
CtxInspRecentTools,
CtxInspToolSchemaCosts,
CtxInspModel,
CtxInspWorkspace,
CtxInspSession,
Expand Down Expand Up @@ -2900,6 +2901,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::CtxInspSystemPrompt,
MessageId::CtxInspReferences,
MessageId::CtxInspRecentTools,
MessageId::CtxInspToolSchemaCosts,
MessageId::CtxInspModel,
MessageId::CtxInspWorkspace,
MessageId::CtxInspSession,
Expand Down
179 changes: 177 additions & 2 deletions crates/tui/src/tui/context_inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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)] = &[
(
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -557,6 +560,70 @@ 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 _ = writeln!(out);
let tokens = tr(locale, MessageId::CtxInspTokens);
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
.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
Expand Down Expand Up @@ -839,7 +906,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::{
Expand Down Expand Up @@ -912,6 +980,113 @@ 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::<serde_json::Map<_, _>>();
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::<Vec<_>>();
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_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!(
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();
Expand Down
Loading