agent ui: terminal-themed live panel; pretty tool calls; collapsed results

- tool_use renders per-tool (Read /path, Bash $ cmd, send → operator: ...)
- tool_result with >120 chars collapses into <details>; short ones inline
- session_init / result / rate_limit dropped from the panel
- thinking content shown inline if present, fallback indicator otherwise
- TurnStart carries unread count → header badge "· 3 unread"
- per-tool [status] line dropped from envelope; lives in wake prompt + UI
- send form moved below the live panel
- live panel themed as a terminal (crust bg, inset shadow, monospace)
This commit is contained in:
müde 2026-05-15 18:20:58 +02:00
parent d8807b8e8c
commit ace13cd785
7 changed files with 191 additions and 87 deletions

View file

@ -87,34 +87,18 @@ pub fn format_recv(resp: Result<SocketReply, anyhow::Error>) -> String {
}
}
/// Format helper for the status peek used in the status line.
pub fn format_status(resp: Result<SocketReply, anyhow::Error>) -> String {
match resp {
Ok(SocketReply::Status(unread)) => format!("{unread} unread message(s) in inbox"),
Ok(other) => format!("status: unexpected response {other:?}"),
Err(e) => format!("status: transport error: {e:#}"),
}
}
/// Common envelope around every MCP tool handler: pre-log → run → append
/// a status line → post-log. Free function so both `AgentServer` and
/// `ManagerServer` use the same shape; the per-server `status_line`
/// closure is what differs (different `Status` wire types).
pub async fn run_tool_envelope<F, S>(tool: &'static str, args: String, status: S, body: F) -> String
/// Common envelope around every MCP tool handler: pre-log → run →
/// post-log. The inbox-status hint used to be appended to every tool
/// result; that lives in the wake prompt + UI header now, so tool
/// results stay clean.
pub async fn run_tool_envelope<F>(tool: &'static str, args: String, body: F) -> String
where
F: Future<Output = String>,
S: Future<Output = String>,
{
tracing::info!(tool, %args, "tool: request");
let result = body.await;
let status_text = status.await;
let full = if status_text.is_empty() {
result
} else {
format!("{result}\n\n[status] {status_text}")
};
tracing::info!(tool, result = %full, "tool: result");
full
tracing::info!(tool, result = %result, "tool: result");
result
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
@ -141,19 +125,6 @@ impl AgentServer {
pub fn new(socket: PathBuf) -> Self {
Self { socket }
}
/// Non-mutating peek used in the status line. Falls back to a vague
/// note rather than failing the whole tool call when the socket
/// hiccups.
async fn status_line(&self) -> String {
let resp = client::request::<_, hive_sh4re::AgentResponse>(
&self.socket,
&hive_sh4re::AgentRequest::Status,
)
.await
.map(SocketReply::from);
format_status(resp)
}
}
#[tool_router]
@ -165,7 +136,7 @@ impl AgentServer {
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
let log = format!("{args:?}");
let to = args.to.clone();
run_tool_envelope("send", log, self.status_line(), async move {
run_tool_envelope("send", log, async move {
let resp = client::request::<_, hive_sh4re::AgentResponse>(
&self.socket,
&hive_sh4re::AgentRequest::Send {
@ -185,7 +156,7 @@ impl AgentServer {
or an empty marker if nothing is waiting."
)]
async fn recv(&self, Parameters(_args): Parameters<RecvArgs>) -> String {
run_tool_envelope("recv", String::new(), self.status_line(), async move {
run_tool_envelope("recv", String::new(), async move {
let resp = client::request::<_, hive_sh4re::AgentResponse>(
&self.socket,
&hive_sh4re::AgentRequest::Recv,
@ -258,16 +229,6 @@ impl ManagerServer {
Self { socket }
}
async fn status_line(&self) -> String {
let resp = client::request::<_, hive_sh4re::ManagerResponse>(
&self.socket,
&hive_sh4re::ManagerRequest::Status,
)
.await
.map(SocketReply::from);
format_status(resp)
}
/// Helper: issue any `ManagerRequest`, convert the reply through
/// `SocketReply`. Manager tools that just need an `Ok` ack share this.
async fn dispatch(
@ -289,7 +250,7 @@ impl ManagerServer {
async fn send(&self, Parameters(args): Parameters<SendArgs>) -> String {
let log = format!("{args:?}");
let to = args.to.clone();
run_tool_envelope("send", log, self.status_line(), async move {
run_tool_envelope("send", log, async move {
let resp = self
.dispatch(hive_sh4re::ManagerRequest::Send {
to: args.to,
@ -306,7 +267,7 @@ impl ManagerServer {
empty."
)]
async fn recv(&self, Parameters(_args): Parameters<RecvArgs>) -> String {
run_tool_envelope("recv", String::new(), self.status_line(), async move {
run_tool_envelope("recv", String::new(), async move {
let resp = self.dispatch(hive_sh4re::ManagerRequest::Recv).await;
format_recv(resp)
})
@ -320,7 +281,7 @@ impl ManagerServer {
async fn request_spawn(&self, Parameters(args): Parameters<RequestSpawnArgs>) -> String {
let log = format!("{args:?}");
let name = args.name.clone();
run_tool_envelope("request_spawn", log, self.status_line(), async move {
run_tool_envelope("request_spawn", log, async move {
let resp = self
.dispatch(hive_sh4re::ManagerRequest::RequestSpawn { name: args.name })
.await;
@ -340,7 +301,7 @@ impl ManagerServer {
async fn kill(&self, Parameters(args): Parameters<KillArgs>) -> String {
let log = format!("{args:?}");
let name = args.name.clone();
run_tool_envelope("kill", log, self.status_line(), async move {
run_tool_envelope("kill", log, async move {
let resp = self
.dispatch(hive_sh4re::ManagerRequest::Kill { name: args.name })
.await;
@ -364,7 +325,6 @@ impl ManagerServer {
run_tool_envelope(
"request_apply_commit",
log,
self.status_line(),
async move {
let resp = self
.dispatch(hive_sh4re::ManagerRequest::RequestApplyCommit {