feat(#2196): move send/ask/answer body to backend _body field

The three message-bearing hyperhive tools (send, ask, answer) previously
had named JS branches in renderRichToolUse that each:
- computed a summary string (recipient / line count)
- rendered the body text via detailsOpenMd (marked + DOMPurify)

This commit moves the body text and summary string to the backend,
reducing the JS dispatch table to a single generic markdown path.

Backend (stream_enrich.rs):
- rich_tool_body: new 'markdown' body_type for send/ask/answer —
  stamps _body with input.body / input.question / input.answer
- fmt_hyperhive_message_tool: new helper formats _summary as
  'send* → to' / 'ask* → to' / 'answer* #id' with ' · NL' when
  the body spans multiple lines; extracted out of fmt_hyperhive_tool
  to keep it under the too_many_lines limit
- doc: updated rich_tool_body docstring to list the new 'markdown' type

Frontend (app.js):
- Remove the three named branches (send/ask/answer) from renderRichToolUse
- Extend the generic _body path: 'markdown' type calls detailsOpenMd
- The ask-form slot logic (operator inline-answer binding) is preserved
  within the markdown branch, now reading the question from c._body
  instead of input.question — DOM mounting remains client-side
This commit is contained in:
iris 2026-07-20 18:20:45 +02:00 committed by mara
commit b25c44f7cb
2 changed files with 102 additions and 53 deletions

View file

@ -270,9 +270,10 @@ fn is_rich_tool(name: &str) -> bool {
/// which renderer to use:
/// - `"diff"` → `api.detailsDiff` (colour-coded `+`/`-` lines)
/// - `"plain"` → `api.details` (plain `<pre>` block)
/// - `"markdown"` → `api.detailsOpenMd` (markdown rendered via marked + `DOMPurify`,
/// default-open; used for message-bearing tools: send, ask, answer)
///
/// Returns `None` for tools whose body is inherently client-side (markdown
/// rendering, DOM forms) or that have no body at all.
/// Returns `None` for tools that have no body at all.
///
/// **`Write` is intentionally absent**: its `content` field can be megabytes
/// and is one-sided (no `old_string` to diff against), making it too large to
@ -317,6 +318,33 @@ fn rich_tool_body(name: &str, input: &Value) -> Option<(String, &'static str)> {
Some((format!("$ {cmd}"), "plain"))
}
}
// Message-bearing tools: body is markdown text rendered by the client.
// The ask form (operator reply slot) is still mounted client-side;
// only the raw body text moves to the backend here.
"mcp__hyperhive__send" => {
let body = input.get("body").and_then(Value::as_str).unwrap_or("");
if body.is_empty() {
None
} else {
Some((body.to_owned(), "markdown"))
}
}
"mcp__hyperhive__ask" => {
let q = input.get("question").and_then(Value::as_str).unwrap_or("");
if q.is_empty() {
None
} else {
Some((q.to_owned(), "markdown"))
}
}
"mcp__hyperhive__answer" => {
let a = input.get("answer").and_then(Value::as_str).unwrap_or("");
if a.is_empty() {
None
} else {
Some((a.to_owned(), "markdown"))
}
}
_ => None,
}
}
@ -443,12 +471,54 @@ fn fmt_builtin_tool(name: &str, short: &str, input: &Value) -> String {
}
}
/// Summary for message-bearing hyperhive tools (send / ask / answer).
///
/// Format: `"{short} → {recipient}"` or `"{short} #{id}"` with ` · NL`
/// appended when the body spans multiple lines.
fn fmt_hyperhive_message_tool(name: &str, short: &str, input: &Value) -> String {
match name {
"mcp__hyperhive__send" => {
let to = sv(input, "to");
let lines = sv(input, "body").lines().count();
if lines > 1 {
format!("{short}{to} · {lines}L")
} else {
format!("{short}{to}")
}
}
"mcp__hyperhive__ask" => {
let to = input
.get("to")
.and_then(Value::as_str)
.unwrap_or("operator");
let lines = sv(input, "question").lines().count();
if lines > 1 {
format!("{short}{to} · {lines}L")
} else {
format!("{short}{to}")
}
}
"mcp__hyperhive__answer" => {
let id = input
.get("id")
.and_then(Value::as_u64)
.map_or_else(|| "?".to_owned(), |n| n.to_string());
let lines = sv(input, "answer").lines().count();
if lines > 1 {
format!("{short} #{id} · {lines}L")
} else {
format!("{short} #{id}")
}
}
_ => 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))
"mcp__hyperhive__send" | "mcp__hyperhive__ask" | "mcp__hyperhive__answer" => {
fmt_hyperhive_message_tool(name, short, input)
}
"mcp__hyperhive__recv" => {
let mut parts = Vec::new();