tool card UI redesign: semantic icons, inline diff persistence, tool detail page, MCP-friendly titles
Nightly Build / build (push) Successful in 6m38s
Nightly Build / build (push) Successful in 6m38s
This commit is contained in:
@@ -201,6 +201,16 @@ struct VerifySpec {
|
||||
#[serde(default)] timeout_secs: Option<u64>,
|
||||
}
|
||||
|
||||
/// One entry of the manifest's optional `tools[]` block: a friendly display name
|
||||
/// for a raw MCP tool. Snapshotted into `mcp_catalog.tool_meta_json` and used as the
|
||||
/// authoritative UI card title (override > live MCP `title` > prettified raw name).
|
||||
/// Icons are not per-tool — a connector's own icon covers all its tools.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
struct ToolMeta {
|
||||
name: String,
|
||||
#[serde(default)] display_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize)]
|
||||
struct Manifest {
|
||||
#[serde(default)] name: Option<String>,
|
||||
@@ -226,6 +236,8 @@ struct Manifest {
|
||||
#[serde(default)] env: Vec<EnvEntry>,
|
||||
/// Optional verify-before-save command.
|
||||
#[serde(default)] verify: Option<VerifySpec>,
|
||||
/// Optional friendly display names for the connector's tools (UI card titles).
|
||||
#[serde(default)] tools: Vec<ToolMeta>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -762,6 +774,10 @@ pub async fn install(
|
||||
.llm_short_description
|
||||
.as_deref()
|
||||
.or(h.entry.user_description.as_deref()),
|
||||
// Snapshot the manifest's friendly tool names for the UI card titles.
|
||||
tool_meta_json: (!h.manifest.tools.is_empty())
|
||||
.then(|| serde_json::to_string(&h.manifest.tools).ok())
|
||||
.flatten(),
|
||||
// Snapshot the feed's version so a later listing can compare it against a
|
||||
// newer feed and surface "update available". Manifest wins over index.
|
||||
version: h.manifest.version.or(h.entry.version),
|
||||
|
||||
@@ -322,6 +322,9 @@ pub async fn catalog_upsert(
|
||||
icon_large_path: None,
|
||||
friendly_name: body.friendly_name.as_deref(),
|
||||
description: body.description.as_deref(),
|
||||
// Friendly tool names are the feed's to set; the manual form leaves them
|
||||
// untouched (COALESCE in `upsert`).
|
||||
tool_meta_json: None,
|
||||
// Versioning is the feed's to set (marketplace install); the manual form
|
||||
// leaves it untouched (COALESCE in `upsert`).
|
||||
version: None,
|
||||
|
||||
@@ -68,6 +68,8 @@ pub fn router() -> Router<Arc<Skald>> {
|
||||
// File attachments: streamed to disk, so the default body-size limit is
|
||||
// disabled on this route only.
|
||||
.route("/{source}/uploads", post(uploads::upload).layer(DefaultBodyLimit::disable()))
|
||||
// Full execution detail for one tool call (the tool-detail page).
|
||||
.route("/tools/{tool_call_id}", get(sessions::tool_detail))
|
||||
// Source-agnostic approval resolve, keyed by globally-unique tool_call_id.
|
||||
.route("/tools/{tool_call_id}/resolve", post(sessions::resolve_tool))
|
||||
// Back-compat alias for older web clients that POST to /web/tools/...
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user