tool card UI redesign: semantic icons, inline diff persistence, tool detail page, MCP-friendly titles
Nightly Build / build (push) Successful in 6m38s

This commit is contained in:
2026-07-21 23:39:41 +01:00
parent 8e891fbced
commit c11702c3d3
44 changed files with 1103 additions and 50 deletions
+2
View File
@@ -26,6 +26,8 @@ pub struct ExecuteCmd;
impl Tool for ExecuteCmd {
fn name(&self) -> &str { crate::tools::tool_names::EXECUTE_CMD }
fn display_name(&self) -> &str { "Run Command" }
fn icon(&self) -> &str { "shell" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Shell }
fn description(&self) -> &str {
@@ -114,6 +114,8 @@ impl EditFile {
impl Tool for EditFile {
fn name(&self) -> &str { "edit_file" }
fn display_name(&self) -> &str { "Edit File" }
fn icon(&self) -> &str { "edit" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Filesystem }
fn description(&self) -> &str {
@@ -16,6 +16,8 @@ impl GrepFiles {
impl Tool for GrepFiles {
fn name(&self) -> &str { "grep_files" }
fn display_name(&self) -> &str { "Search" }
fn icon(&self) -> &str { "search" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Filesystem }
fn description(&self) -> &str {
@@ -48,6 +48,8 @@ fn apply_insert(text: &str, args: &Value, display: &str) -> Result<(String, Stri
impl Tool for InsertAtLine {
fn name(&self) -> &str { "insert_at_line" }
fn display_name(&self) -> &str { "Edit File" }
fn icon(&self) -> &str { "edit" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Filesystem }
fn description(&self) -> &str {
@@ -25,6 +25,8 @@ impl ListFiles {
impl Tool for ListFiles {
fn name(&self) -> &str { "list_files" }
fn display_name(&self) -> &str { "List Files" }
fn icon(&self) -> &str { "list" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Filesystem }
fn description(&self) -> &str {
@@ -46,6 +46,8 @@ fn render_hits(store: &str, hits: &[MemoryHit], out: &mut String) {
impl Tool for MemorySearch {
fn name(&self) -> &str { "memory_search" }
fn display_name(&self) -> &str { "Search Memory" }
fn icon(&self) -> &str { "search" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Introspection }
fn description(&self) -> &str {
@@ -49,6 +49,8 @@ fn number_lines(content: &str, start: usize, end_line: Option<usize>, limit: Opt
impl Tool for ReadFile {
fn name(&self) -> &str { "read_file" }
fn display_name(&self) -> &str { "Read File" }
fn icon(&self) -> &str { "read" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Filesystem }
fn description(&self) -> &str {
@@ -55,6 +55,8 @@ fn apply_replace(content: &str, args: &Value, display: &str) -> Result<(String,
impl Tool for ReplaceLines {
fn name(&self) -> &str { "replace_lines" }
fn display_name(&self) -> &str { "Edit File" }
fn icon(&self) -> &str { "edit" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Filesystem }
fn description(&self) -> &str {
@@ -66,6 +66,8 @@ fn render_search(text: &str, args: &Value, display: &str) -> Result<String> {
impl Tool for SearchFile {
fn name(&self) -> &str { "search_file" }
fn display_name(&self) -> &str { "Search" }
fn icon(&self) -> &str { "search" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Filesystem }
fn description(&self) -> &str {
@@ -21,6 +21,8 @@ impl WriteFile {
impl Tool for WriteFile {
fn name(&self) -> &str { "write_file" }
fn display_name(&self) -> &str { "Edit File" }
fn icon(&self) -> &str { "edit" }
fn category(&self) -> crate::tools::ToolCategory { crate::tools::ToolCategory::Filesystem }
fn description(&self) -> &str {
@@ -47,6 +47,8 @@ pub struct ImageGenerateTool {
impl Tool for ImageGenerateTool {
fn name(&self) -> &str { "image_generate" }
fn display_name(&self) -> &str { "Generate Image" }
fn icon(&self) -> &str { "image" }
fn category(&self) -> ToolCategory { ToolCategory::Config }
fn description(&self) -> &str {
+65
View File
@@ -62,6 +62,33 @@ pub use core_api::tool::{
pub const MAX_LABEL_SHORT: usize = 60;
pub const MAX_LABEL_FULL: usize = 120;
/// UI metadata for one tool call — the friendly card title plus a **semantic** icon
/// key (never a glyph; the frontend maps the key to an icon + accent color). Computed
/// by [`ToolRegistry::display_meta`], the single seam shared by the live WS event and
/// the history projection so the two can't drift.
#[derive(Debug, Clone)]
pub struct ToolUiMeta {
pub display_name: String,
pub icon: String,
}
/// Turns a raw tool id (`snake_case` / `kebab-case`) into a Title-Cased phrase for a
/// UI label when no friendly name is declared — `list_recent_files` → "List Recent
/// Files". The last-resort fallback for MCP and unknown tools.
pub fn prettify_tool_name(name: &str) -> String {
name.split(|c| c == '_' || c == '-')
.filter(|s| !s.is_empty())
.map(|w| {
let mut chars = w.chars();
match chars.next() {
Some(f) => f.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
})
.collect::<Vec<_>>()
.join(" ")
}
/// Registry of all available tools.
pub struct ToolRegistry {
tools: HashMap<String, Arc<dyn Tool>>,
@@ -173,6 +200,44 @@ impl ToolRegistry {
name.to_string()
}
/// Friendly card metadata (display name + semantic icon key) for any tool call,
/// including non-registry tools. Registry tools delegate to
/// [`Tool::display_name`]/[`Tool::icon`]; sub-agent and interface tools are
/// handled inline; an `mcp__server__tool` name gets `icon = "mcp"` and a
/// prettified tool name — the caller (which holds the [`McpProvider`]) may then
/// override `display_name` with the connector's resolved friendly name.
///
/// [`McpProvider`]: crate::mcp::provider::McpProvider
pub fn display_meta(&self, name: &str, args: &Value) -> ToolUiMeta {
if let Some(tool) = self.tools.get(name) {
return ToolUiMeta {
display_name: tool.display_name().to_string(),
icon: tool.icon().to_string(),
};
}
// Sub-agent delegation tools (InterfaceTools, not in the registry).
if name == tool_names::EXECUTE_TASK
|| name == tool_names::EXECUTE_SUBTASK
|| name == "run_subtask"
{
let dn = match args["agent_id"].as_str().map(str::trim).filter(|s| !s.is_empty()) {
Some(a) => format!("Sub-agent: {a}"),
None => "Sub-agent".to_string(),
};
return ToolUiMeta { display_name: dn, icon: "subagent".to_string() };
}
if name == tool_names::SHOW_FILE_TO_USER {
return ToolUiMeta { display_name: "Show File".to_string(), icon: "read".to_string() };
}
// MCP tool `mcp__server__tool`: default to a prettified tool name; the caller
// overrides with the connector's manifest/`title` friendly name when known.
if let Some(rest) = name.strip_prefix("mcp__") {
let tool = rest.split_once("__").map(|(_, t)| t).unwrap_or(rest);
return ToolUiMeta { display_name: prettify_tool_name(tool), icon: "mcp".to_string() };
}
ToolUiMeta { display_name: prettify_tool_name(name), icon: "tool".to_string() }
}
/// Returns the category of a registered tool, or `None` for unknown tools
/// (MCP tools, interface tools, call_agent, etc.).
pub fn category_of(&self, name: &str) -> Option<ToolCategory> {