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:
parent
d8807b8e8c
commit
ace13cd785
7 changed files with 191 additions and 87 deletions
|
|
@ -135,11 +135,13 @@ async fn serve(
|
|||
match recv {
|
||||
Ok(AgentResponse::Message { from, body }) => {
|
||||
tracing::info!(%from, %body, "inbox");
|
||||
let unread = inbox_unread(socket).await;
|
||||
bus.emit(LiveEvent::TurnStart {
|
||||
from: from.clone(),
|
||||
body: body.clone(),
|
||||
unread,
|
||||
});
|
||||
let prompt = format_wake_prompt(&from, &body);
|
||||
let prompt = format_wake_prompt(&from, &body, unread);
|
||||
let outcome = turn::drive_turn(
|
||||
&prompt,
|
||||
&mcp_config,
|
||||
|
|
@ -168,9 +170,24 @@ async fn serve(
|
|||
|
||||
/// Per-turn user prompt. The role/tools/etc. is in the system prompt
|
||||
/// (`prompts/agent.md` → `claude --system-prompt-file`); this is just the
|
||||
/// wake signal claude reacts to.
|
||||
fn format_wake_prompt(from: &str, body: &str) -> String {
|
||||
format!("Incoming message from `{from}`:\n---\n{body}\n---")
|
||||
/// wake signal claude reacts to. `unread` is the count of *other*
|
||||
/// messages in the inbox right after this one was popped.
|
||||
fn format_wake_prompt(from: &str, body: &str, unread: u64) -> String {
|
||||
let pending = if unread == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n\n({unread} more message(s) pending in your inbox — drain via `mcp__hyperhive__recv` if relevant.)")
|
||||
};
|
||||
format!("Incoming message from `{from}`:\n---\n{body}\n---{pending}")
|
||||
}
|
||||
|
||||
/// Best-effort: ask our own per-agent socket how many messages are still
|
||||
/// pending after the wake-up Recv. Returns 0 if anything goes wrong.
|
||||
async fn inbox_unread(socket: &Path) -> u64 {
|
||||
match client::request::<_, AgentResponse>(socket, &AgentRequest::Status).await {
|
||||
Ok(AgentResponse::Status { unread }) => unread,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(resp: &AgentResponse) -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -151,11 +151,13 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> {
|
|||
// so the wake prompt can label it as such.
|
||||
}
|
||||
tracing::info!(%from, %body, "manager inbox");
|
||||
let unread = inbox_unread(socket).await;
|
||||
bus.emit(LiveEvent::TurnStart {
|
||||
from: from.clone(),
|
||||
body: body.clone(),
|
||||
unread,
|
||||
});
|
||||
let prompt = format_wake_prompt(&from, &body);
|
||||
let prompt = format_wake_prompt(&from, &body, unread);
|
||||
let outcome = turn::drive_turn(
|
||||
&prompt,
|
||||
&mcp_config,
|
||||
|
|
@ -185,7 +187,20 @@ async fn serve(socket: &Path, interval: Duration, bus: Bus) -> Result<()> {
|
|||
|
||||
/// Per-turn user prompt. The role/tools/etc. is in the system prompt
|
||||
/// (`prompts/manager.md` → `claude --system-prompt-file`); this is just
|
||||
/// the wake signal.
|
||||
fn format_wake_prompt(from: &str, body: &str) -> String {
|
||||
format!("Incoming message from `{from}`:\n---\n{body}\n---")
|
||||
/// the wake signal. `unread` is the inbox depth after this message was
|
||||
/// popped.
|
||||
fn format_wake_prompt(from: &str, body: &str, unread: u64) -> String {
|
||||
let pending = if unread == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n\n({unread} more message(s) pending in your inbox — drain via `mcp__hyperhive__recv` if relevant.)")
|
||||
};
|
||||
format!("Incoming message from `{from}`:\n---\n{body}\n---{pending}")
|
||||
}
|
||||
|
||||
async fn inbox_unread(socket: &Path) -> u64 {
|
||||
match client::request::<_, ManagerResponse>(socket, &ManagerRequest::Status).await {
|
||||
Ok(ManagerResponse::Status { unread }) => unread,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,13 @@ const CHANNEL_CAPACITY: usize = 256;
|
|||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum LiveEvent {
|
||||
/// Harness popped a wake-up message and is about to invoke claude.
|
||||
TurnStart { from: String, body: String },
|
||||
/// `unread` is the count of *other* messages still in the inbox at
|
||||
/// that moment — surfaced as a badge in the live panel header.
|
||||
TurnStart {
|
||||
from: String,
|
||||
body: String,
|
||||
unread: u64,
|
||||
},
|
||||
/// One line of claude's `--output-format stream-json` stdout, parsed as
|
||||
/// a generic JSON value (so we don't have to track every claude-code
|
||||
/// event variant). The frontend pretty-prints by `type` field.
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue