frontend/agent: drop JS dispatch tables, read backend _icon/_summary/_category
Phase 2 of hyperhive#2196. The backend now pre-computes enrichment fields on every SSE event (stream_enrich.rs); the frontend reads them directly instead of running its own dispatch logic. Removed (~330 lines of JS): - fmtArgsGeneric / TOOL_ICONS / toolIcon / fmtRoom / fmtUser / fmtToolUse renderStream changes: - system events: dispatch on v._category (drop/thinking_tok/note/details) + v._summary / v._body instead of per-subtype if-chains; status tick still overrides label client-side when stateName === 'compacting' since elapsed time is a wall-clock value the backend cannot know at emit time - tool_use: use c._category === 'rich' for rich-renderer routing, c._icon / c._summary for flat rows renderRichToolUse: toolIcon(name) -> c._icon (from backend enrichment) stream_enrich.rs: also stamp _category: 'drop' on top-level type=result / type=rate_limit_event so the frontend can use a single _category check instead of separate type-based early returns
This commit is contained in:
parent
ae3f011eb3
commit
0f5367f948
3 changed files with 196 additions and 437 deletions
|
|
@ -36,6 +36,13 @@ use serde_json::{Value, json};
|
|||
/// already-enriched values if the DB is ever pre-populated by a future phase).
|
||||
pub fn enrich(v: &mut Value) {
|
||||
match v.get("type").and_then(Value::as_str).unwrap_or("") {
|
||||
// Top-level result/rate_limit_event are drop-category noise — stamp
|
||||
// the same category the frontend uses to silently discard them.
|
||||
"result" | "rate_limit_event" => {
|
||||
if let Some(obj) = v.as_object_mut() {
|
||||
obj.entry("_category").or_insert_with(|| json!("drop"));
|
||||
}
|
||||
}
|
||||
"system" => enrich_system(v),
|
||||
"assistant" => enrich_assistant(v),
|
||||
_ => {}
|
||||
|
|
@ -246,6 +253,7 @@ fn is_rich_tool(name: &str) -> bool {
|
|||
name,
|
||||
"Write"
|
||||
| "Edit"
|
||||
| "mcp__bash__run"
|
||||
| "mcp__hyperhive__send"
|
||||
| "mcp__hyperhive__ask"
|
||||
| "mcp__hyperhive__answer"
|
||||
|
|
@ -300,10 +308,9 @@ fn tool_icon_fallback(name: &str) -> &'static str {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tool_use summary formatter
|
||||
// tool_use summary formatter — dispatch to per-family sub-functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn fmt_tool_use(name: &str, input: &Value) -> String {
|
||||
// Short name: strip the MCP server prefix for display.
|
||||
let short = if let Some(rest) = name.strip_prefix("mcp__hyperhive__") {
|
||||
|
|
@ -316,6 +323,20 @@ fn fmt_tool_use(name: &str, input: &Value) -> String {
|
|||
name.to_owned()
|
||||
};
|
||||
|
||||
if name.starts_with("mcp__hyperhive__") {
|
||||
fmt_hyperhive_tool(name, &short, input)
|
||||
} else if name.starts_with("mcp__bash__") {
|
||||
fmt_bash_tool(name, &short, input)
|
||||
} else if name.starts_with("mcp__matrix__") {
|
||||
fmt_matrix_tool(name, &short, input)
|
||||
} else {
|
||||
fmt_builtin_tool(name, &short, input)
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in claude tools: Read/Write/Edit/Glob/Grep/Bash/TodoWrite and any
|
||||
/// unknown tool that doesn't carry a known MCP server prefix.
|
||||
fn fmt_builtin_tool(name: &str, short: &str, input: &Value) -> String {
|
||||
match name {
|
||||
"Read" | "Write" | "Edit" => format!("{short} {}", sv(input, "file_path")),
|
||||
"Glob" | "Grep" => format!("{short} {}", sv(input, "pattern")),
|
||||
|
|
@ -338,6 +359,13 @@ fn fmt_tool_use(name: &str, input: &Value) -> String {
|
|||
.map_or(0, Vec::len);
|
||||
format!("{short} ({n} items)")
|
||||
}
|
||||
_ => fmt_args_generic(short, input),
|
||||
}
|
||||
}
|
||||
|
||||
/// `mcp__hyperhive__*` tools.
|
||||
fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String {
|
||||
match name {
|
||||
"mcp__hyperhive__send" => {
|
||||
let body = trim_str(&sv(input, "body"), 80);
|
||||
format!("{short} → {}: {}", sv(input, "to"), json_str(&body))
|
||||
|
|
@ -395,42 +423,111 @@ fn fmt_tool_use(name: &str, input: &Value) -> String {
|
|||
format!("{short} {}", parts.join(" · "))
|
||||
}
|
||||
}
|
||||
"mcp__hyperhive__remind" => {
|
||||
let when = if let Some(s) = input.get("delay_seconds").and_then(Value::as_u64) {
|
||||
if s < 60 {
|
||||
format!("+{s}s")
|
||||
} else if s < 3_600 {
|
||||
format!("+{}m", s / 60)
|
||||
} else {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let h_f = s as f64 / 3_600.0;
|
||||
format!("+{h_f:.1}h")
|
||||
}
|
||||
} else if let Some(ts) = input.get("at_unix_timestamp").and_then(Value::as_u64) {
|
||||
let h = (ts % 86_400) / 3_600;
|
||||
let m = (ts % 3_600) / 60;
|
||||
format!("at {h:02}:{m:02}Z")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let msg_raw = input
|
||||
.get("message")
|
||||
.or_else(|| input.get("file_path"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let msg = trim_str(&msg_raw.replace(char::is_whitespace, " "), 60);
|
||||
let when_part = if when.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {when}")
|
||||
};
|
||||
let msg_part = if msg.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" \"{msg}\"")
|
||||
};
|
||||
format!("{short}{when_part}{msg_part}")
|
||||
"mcp__hyperhive__remind" => fmt_hyperhive_remind(short, input),
|
||||
"mcp__hyperhive__request_update_meta_inputs"
|
||||
| "mcp__hyperhive__list_schedules"
|
||||
| "mcp__hyperhive__cancel_schedule"
|
||||
| "mcp__hyperhive__fire_schedule_now"
|
||||
| "mcp__hyperhive__edit_schedule"
|
||||
| "mcp__hyperhive__request_schedule_prompt" => {
|
||||
fmt_hyperhive_schedule_tool(name, short, input)
|
||||
}
|
||||
"mcp__hyperhive__set_status" => {
|
||||
format!("{short} \"{}\"", trim_str(&sv(input, "text"), 60))
|
||||
}
|
||||
"mcp__hyperhive__get_loose_ends" => {
|
||||
let agent = input
|
||||
.get("agent")
|
||||
.and_then(Value::as_str)
|
||||
.map_or_else(|| "()".to_owned(), |a| format!(" [{a}]"));
|
||||
format!("{short}{agent}")
|
||||
}
|
||||
"mcp__hyperhive__get_agent_meta" => {
|
||||
let name_part = input
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.map_or_else(|| "()".to_owned(), |n| format!(" {n}"));
|
||||
format!("{short}{name_part}")
|
||||
}
|
||||
"mcp__hyperhive__cancel_loose_end" => {
|
||||
let id = input
|
||||
.get("id")
|
||||
.and_then(Value::as_u64)
|
||||
.map_or_else(|| "?".to_owned(), |n| n.to_string());
|
||||
format!("{short} {} #{id}", sv(input, "kind"))
|
||||
}
|
||||
_ => fmt_args_generic(short, input),
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_hyperhive_remind(short: &str, input: &Value) -> String {
|
||||
let when = if let Some(s) = input.get("delay_seconds").and_then(Value::as_u64) {
|
||||
if s < 60 {
|
||||
format!("+{s}s")
|
||||
} else if s < 3_600 {
|
||||
format!("+{}m", s / 60)
|
||||
} else {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let h_f = s as f64 / 3_600.0;
|
||||
format!("+{h_f:.1}h")
|
||||
}
|
||||
} else if let Some(ts) = input.get("at_unix_timestamp").and_then(Value::as_u64) {
|
||||
let h = (ts % 86_400) / 3_600;
|
||||
let m = (ts % 3_600) / 60;
|
||||
format!("at {h:02}:{m:02}Z")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let msg_raw = input
|
||||
.get("message")
|
||||
.or_else(|| input.get("file_path"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let msg = trim_str(&msg_raw.replace(char::is_whitespace, " "), 60);
|
||||
let when_part = if when.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {when}")
|
||||
};
|
||||
let msg_part = if msg.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" \"{msg}\"")
|
||||
};
|
||||
format!("{short}{when_part}{msg_part}")
|
||||
}
|
||||
|
||||
fn fmt_hyperhive_edit_schedule(short: &str, input: &Value) -> String {
|
||||
let id = input
|
||||
.get("id")
|
||||
.and_then(Value::as_u64)
|
||||
.map_or_else(|| "?".to_owned(), |n| n.to_string());
|
||||
let mut parts = vec![format!("#{id}")];
|
||||
if input.get("body").is_some() {
|
||||
parts.push("body".to_owned());
|
||||
}
|
||||
if input.get("interval_seconds").is_some() {
|
||||
parts.push("interval".to_owned());
|
||||
}
|
||||
if input.get("next_fire_at_unix").is_some() {
|
||||
parts.push("next".to_owned());
|
||||
}
|
||||
if let Some(a) = input.get("targets_add").and_then(Value::as_array)
|
||||
&& !a.is_empty()
|
||||
{
|
||||
parts.push(format!("+{} tgt", a.len()));
|
||||
}
|
||||
if let Some(r) = input.get("targets_remove").and_then(Value::as_array)
|
||||
&& !r.is_empty()
|
||||
{
|
||||
parts.push(format!("-{} tgt", r.len()));
|
||||
}
|
||||
format!("{short} {}", parts.join(" · "))
|
||||
}
|
||||
|
||||
/// Schedule-management hyperhive tools (list/cancel/fire/edit/request).
|
||||
fn fmt_hyperhive_schedule_tool(name: &str, short: &str, input: &Value) -> String {
|
||||
match name {
|
||||
"mcp__hyperhive__request_update_meta_inputs" => {
|
||||
let ins = match input.get("inputs").and_then(Value::as_array) {
|
||||
Some(arr) if !arr.is_empty() => {
|
||||
|
|
@ -464,33 +561,7 @@ fn fmt_tool_use(name: &str, input: &Value) -> String {
|
|||
.map_or_else(|| "?".to_owned(), |n| n.to_string());
|
||||
format!("{short} #{id}")
|
||||
}
|
||||
"mcp__hyperhive__edit_schedule" => {
|
||||
let id = input
|
||||
.get("id")
|
||||
.and_then(Value::as_u64)
|
||||
.map_or_else(|| "?".to_owned(), |n| n.to_string());
|
||||
let mut parts = vec![format!("#{id}")];
|
||||
if input.get("body").is_some() {
|
||||
parts.push("body".to_owned());
|
||||
}
|
||||
if input.get("interval_seconds").is_some() {
|
||||
parts.push("interval".to_owned());
|
||||
}
|
||||
if input.get("next_fire_at_unix").is_some() {
|
||||
parts.push("next".to_owned());
|
||||
}
|
||||
if let Some(a) = input.get("targets_add").and_then(Value::as_array)
|
||||
&& !a.is_empty()
|
||||
{
|
||||
parts.push(format!("+{} tgt", a.len()));
|
||||
}
|
||||
if let Some(r) = input.get("targets_remove").and_then(Value::as_array)
|
||||
&& !r.is_empty()
|
||||
{
|
||||
parts.push(format!("-{} tgt", r.len()));
|
||||
}
|
||||
format!("{short} {}", parts.join(" · "))
|
||||
}
|
||||
"mcp__hyperhive__edit_schedule" => fmt_hyperhive_edit_schedule(short, input),
|
||||
"mcp__hyperhive__request_schedule_prompt" => {
|
||||
let tgts = match input.get("targets").and_then(Value::as_array) {
|
||||
Some(arr) => arr
|
||||
|
|
@ -518,6 +589,13 @@ fn fmt_tool_use(name: &str, input: &Value) -> String {
|
|||
.unwrap_or_default();
|
||||
format!("{short} → {tgts} at {when}{recur}")
|
||||
}
|
||||
_ => fmt_args_generic(short, input),
|
||||
}
|
||||
}
|
||||
|
||||
/// `mcp__bash__*` tools.
|
||||
fn fmt_bash_tool(name: &str, short: &str, input: &Value) -> String {
|
||||
match name {
|
||||
"mcp__bash__run" => {
|
||||
let cmd = sv(input, "cmd");
|
||||
let first = cmd.lines().next().unwrap_or("").trim().to_owned();
|
||||
|
|
@ -539,30 +617,13 @@ fn fmt_tool_use(name: &str, input: &Value) -> String {
|
|||
};
|
||||
format!("{short} {}{force}", sv(input, "id"))
|
||||
}
|
||||
"mcp__hyperhive__set_status" => {
|
||||
format!("{short} \"{}\"", trim_str(&sv(input, "text"), 60))
|
||||
}
|
||||
"mcp__hyperhive__get_loose_ends" => {
|
||||
let agent = input
|
||||
.get("agent")
|
||||
.and_then(Value::as_str)
|
||||
.map_or_else(|| "()".to_owned(), |a| format!(" [{a}]"));
|
||||
format!("{short}{agent}")
|
||||
}
|
||||
"mcp__hyperhive__get_agent_meta" => {
|
||||
let name_part = input
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.map_or_else(|| "()".to_owned(), |n| format!(" {n}"));
|
||||
format!("{short}{name_part}")
|
||||
}
|
||||
"mcp__hyperhive__cancel_loose_end" => {
|
||||
let id = input
|
||||
.get("id")
|
||||
.and_then(Value::as_u64)
|
||||
.map_or_else(|| "?".to_owned(), |n| n.to_string());
|
||||
format!("{short} {} #{id}", sv(input, "kind"))
|
||||
}
|
||||
_ => fmt_args_generic(short, input),
|
||||
}
|
||||
}
|
||||
|
||||
/// `mcp__matrix__*` tools.
|
||||
fn fmt_matrix_tool(name: &str, short: &str, input: &Value) -> String {
|
||||
match name {
|
||||
"mcp__matrix__read_room" => {
|
||||
let limit = input
|
||||
.get("limit")
|
||||
|
|
@ -584,20 +645,18 @@ fn fmt_tool_use(name: &str, input: &Value) -> String {
|
|||
fmt_user(&sv(input, "user_id")),
|
||||
json_str(&trim_str(&sv(input, "body"), 50))
|
||||
),
|
||||
"mcp__matrix__send_reaction" => {
|
||||
format!(
|
||||
"{short} {} {}",
|
||||
fmt_room(&sv(input, "room")),
|
||||
sv(input, "key")
|
||||
)
|
||||
}
|
||||
"mcp__matrix__send_reaction" => format!(
|
||||
"{short} {} {}",
|
||||
fmt_room(&sv(input, "room")),
|
||||
sv(input, "key")
|
||||
),
|
||||
"mcp__matrix__open_dm" => format!("{short} {}", fmt_user(&sv(input, "user_id"))),
|
||||
"mcp__matrix__invite_user" => format!(
|
||||
"{short} {} → {}",
|
||||
fmt_user(&sv(input, "user_id")),
|
||||
fmt_room(&sv(input, "room"))
|
||||
),
|
||||
_ => fmt_args_generic(&short, input),
|
||||
_ => fmt_args_generic(short, input),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -671,9 +730,10 @@ fn fmt_tok(n: u64) -> String {
|
|||
/// Shorten a matrix room id/alias for display.
|
||||
fn fmt_room(r: &str) -> String {
|
||||
if r.starts_with('!') {
|
||||
// Room id: keep only the local part before the colon (up to 8 chars).
|
||||
let end = r.find(':').unwrap_or(r.len()).min(9);
|
||||
r[..end].to_owned()
|
||||
// Room id: keep only the local part before the colon (up to 9 chars).
|
||||
// Use chars().take() so we never slice on a non-ASCII byte boundary.
|
||||
let colon = r.find(':').unwrap_or(r.len());
|
||||
r.chars().take(colon.min(9)).collect()
|
||||
} else if r.starts_with('#') {
|
||||
// Alias: keep `#name` part before the server.
|
||||
r.split(':').next().unwrap_or(r).to_owned()
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ pub(super) async fn events_stream(
|
|||
// (`_icon`, `_summary`, `_category`) so the frontend doesn't need to
|
||||
// duplicate the dispatch logic. The DB stores raw events; enrichment
|
||||
// is applied here so both the live tail and the history endpoint
|
||||
// deliver the same shape (see `events_history` below).
|
||||
// deliver the same shape (see `events_history` above).
|
||||
if let crate::events::LiveEvent::Stream(ref mut v) = ev.event {
|
||||
crate::stream_enrich::enrich(v);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue