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
+93 -2
View File
@@ -17,6 +17,7 @@ use std::sync::Arc;
use skald_core::skald::{Skald, UserContext};
use skald_core::session::handler::ApprovalDecision;
use skald_core::approval::ApprovalManager;
use skald_core::mcp::{McpManager, parse_mcp_tool_name};
use skald_core::tools::{ToolRegistry, ToolDescriptionLength, tool_names as tn};
use super::{ApiError, guard::AuthUser, require_context};
@@ -112,11 +113,35 @@ async fn messages_for_source(skald: &Arc<Skald>, ctx: &UserContext, source: &str
.collect();
let mut items: Vec<Value> = Vec::new();
build_items(db, skald.tools(), &ctx.approval, &main_stack, &subagent_map, &mut items).await?;
build_items(db, skald.tools(), skald.mcp(), &ctx.user_mcp, &ctx.approval, &main_stack, &subagent_map, &mut items).await?;
Ok(Json(items))
}
/// Card metadata (friendly display name + semantic icon key) for a persisted tool
/// call, mirroring the live loop's `tool_ui_meta`: the registry seam, with the MCP
/// display-name override layered on (per-user runtime first, then global) for an
/// `mcp__server__tool` name. History resolves against the running runtimes, so the
/// friendly name survives a refresh; a stopped server falls back to the prettified
/// name the seam produced.
fn tool_card_meta(
tools: &ToolRegistry,
global_mcp: &McpManager,
user_mcp: &McpManager,
name: &str,
args: &Value,
) -> (String, String) {
let mut meta = tools.display_meta(name, args);
if let Some((server, tool)) = parse_mcp_tool_name(name) {
if let Some(friendly) = user_mcp.tool_display_name(server, tool)
.or_else(|| global_mcp.tool_display_name(server, tool))
{
meta.display_name = friendly;
}
}
(meta.display_name, meta.icon)
}
// ── POST /api/tools/:tool_call_id/resolve — approve/reject a pending tool ─────
// (source-agnostic; /api/web/tools/... kept as a back-compat alias)
@@ -269,6 +294,56 @@ pub async fn resolve_tool(
}
}
// ── GET /api/tools/:tool_call_id — full execution detail for the detail page ──
/// One tool call's full record — input args, result, and (for a file-write) the
/// before/after snapshot — for the dedicated tool-detail page. Read from the
/// caller's own pool (the `tool_call_id` is local to it), so ownership is implicit.
pub async fn tool_detail(
State(skald): State<Arc<Skald>>,
Extension(auth): Extension<AuthUser>,
Path(p): Path<ResolveToolPath>,
) -> Result<Json<Value>, ApiError> {
let ctx = require_context(&skald, &auth.user_id).await?;
let tc = chat_llm_tools::get(&ctx.pool, p.tool_call_id).await?
.ok_or_else(|| ApiError::not_found(format!("tool_call_id {} not found", p.tool_call_id)))?;
let args: Value = tc.arguments.as_deref()
.and_then(|s| serde_json::from_str(s).ok())
.unwrap_or(Value::Null);
// Normalize the DB status the same way as the history projection: an interrupted
// `running` row surfaces as an error, terminal states pass through.
let (status, result, error) = match tc.status.as_str() {
"done" => ("done", tc.result.clone(), None),
"pending" => ("pending", None, None),
"running" => ("error", None, Some("Interrupted.".to_string())),
"cancelled" => ("cancelled", None, tc.result.clone()),
"rejected" => ("rejected", None, tc.result.clone()),
_ => ("error", None, tc.result.clone()),
};
let (display_name, icon) = tool_card_meta(skald.tools(), skald.mcp(), &ctx.user_mcp, &tc.name, &args);
let label_full = skald.tools().describe_call(&tc.name, &args, ToolDescriptionLength::Full);
let target_path = skald.tools().target_path(&tc.name, &args);
Ok(Json(json!({
"tool_call_id": tc.id,
"name": tc.name,
"display_name": display_name,
"icon": icon,
"label_full": label_full,
"path": target_path,
"arguments": args,
"status": status,
"result": result,
"result_type": tc.result_type,
"error": error,
"preview_old": tc.preview_old,
"preview_new": tc.preview_new,
})))
}
// ── GET /api/sessions — list sessions by source (paginated) ──────────────────
#[derive(Deserialize)]
@@ -473,10 +548,16 @@ fn build_debug_items<'a>(
let label_short = tools.describe_call(&tc.name, &args, ToolDescriptionLength::Short);
let label_full = tools.describe_call(&tc.name, &args, ToolDescriptionLength::Full);
let target_path = tools.target_path(&tc.name, &args);
// Debug view has no MCP runtime handy: use the registry seam
// directly (MCP tools fall back to a prettified name + `mcp`
// icon, no live/manifest title override).
let meta = tools.display_meta(&tc.name, &args);
items.push(json!({
"kind": "tool",
"tool_call_id": tc.id,
"name": tc.name,
"display_name": meta.display_name,
"icon": meta.icon,
"label_short": label_short,
"label_full": label_full,
"path": target_path,
@@ -485,6 +566,8 @@ fn build_debug_items<'a>(
"result": result,
"result_type": tc.result_type,
"error": error,
"preview_old": tc.preview_old,
"preview_new": tc.preview_new,
}));
if let Some(sub_stack) = subagent_map.get(&tc.id) {
@@ -513,9 +596,12 @@ fn build_debug_items<'a>(
// ── Recursive message-tree builder ────────────────────────────────────────────
#[allow(clippy::too_many_arguments)]
fn build_items<'a>(
db: &'a SqlitePool,
tools: &'a ToolRegistry,
global_mcp: &'a McpManager,
user_mcp: &'a McpManager,
approval: &'a ApprovalManager,
stack: &'a SessionStack,
subagent_map: &'a HashMap<i64, SessionStack>,
@@ -601,11 +687,14 @@ fn build_items<'a>(
let label_short = tools.describe_call(&tc.name, &args, ToolDescriptionLength::Short);
let label_full = tools.describe_call(&tc.name, &args, ToolDescriptionLength::Full);
let target_path = tools.target_path(&tc.name, &args);
let (display_name, icon) = tool_card_meta(tools, global_mcp, user_mcp, &tc.name, &args);
items.push(json!({
"kind": "tool",
"tool_call_id": tc.id,
"request_id": request_id,
"name": tc.name,
"display_name": display_name,
"icon": icon,
"label_short": label_short,
"label_full": label_full,
"path": target_path,
@@ -614,6 +703,8 @@ fn build_items<'a>(
"result": result,
"result_type": tc.result_type,
"error": error,
"preview_old": tc.preview_old,
"preview_new": tc.preview_new,
}));
if let Some(sub_stack) = subagent_map.get(&tc.id) {
@@ -624,7 +715,7 @@ fn build_items<'a>(
"depth": sub_stack.depth,
"done": true,
}));
build_items(db, tools, approval, sub_stack, subagent_map, items).await?;
build_items(db, tools, global_mcp, user_mcp, approval, sub_stack, subagent_map, items).await?;
items.push(json!({
"kind": "agent_end",
"agent_id": sub_stack.agent_id,