From 6f6a5ba04d151e0f3e051976adbd3e9e291ef698 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 27 Jan 2026 20:40:02 -0800 Subject: [PATCH] fix: make agent badges subtle, position next to name, widen dropdown --- docs/openapi.json | 2 +- frontend/packages/inspector/index.html | 24 ++------- .../src/components/SessionSidebar.tsx | 8 +-- .../src/components/chat/ChatPanel.tsx | 8 +-- server/packages/sandbox-agent/src/router.rs | 54 +++++++++++++++++++ .../src/agents/claude.rs | 16 +++++- 6 files changed, 78 insertions(+), 34 deletions(-) diff --git a/docs/openapi.json b/docs/openapi.json index 7bd9301..900b945 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "sandbox-agent", - "description": "", + "description": "Universal API for automatic coding agents in sandboxes. Supprots Claude Code, Codex, OpenCode, and Amp.", "contact": { "name": "Rivet Gaming, LLC", "email": "developer@rivet.gg" diff --git a/frontend/packages/inspector/index.html b/frontend/packages/inspector/index.html index 543649d..a7bb27e 100644 --- a/frontend/packages/inspector/index.html +++ b/frontend/packages/inspector/index.html @@ -460,7 +460,7 @@ position: absolute; top: 30px; right: 0; - min-width: 140px; + min-width: 200px; background: var(--surface); border: 1px solid var(--border-2); border-radius: 8px; @@ -484,7 +484,6 @@ transition: all var(--transition); display: flex; align-items: center; - justify-content: space-between; gap: 8px; } @@ -494,25 +493,14 @@ } .sidebar-add-option:hover .agent-badge { - background: rgba(255, 255, 255, 0.2); - color: #fff; + color: rgba(255, 255, 255, 0.6); } .agent-option-name { - flex: 1; - min-width: 0; - } - - .agent-option-badges { - display: flex; - align-items: center; - gap: 4px; - flex-shrink: 0; + white-space: nowrap; } .agent-badge { - padding: 2px 6px; - border-radius: 4px; font-size: 10px; font-weight: 500; white-space: nowrap; @@ -520,12 +508,10 @@ } .agent-badge.installed { - background: rgba(48, 209, 88, 0.15); - color: var(--success); + color: var(--muted); } .agent-badge.version { - background: var(--border-2); color: var(--muted); } @@ -734,7 +720,7 @@ left: 50%; transform: translateX(-50%); margin-top: 4px; - min-width: 160px; + min-width: 200px; background: var(--surface); border: 1px solid var(--border-2); border-radius: 8px; diff --git a/frontend/packages/inspector/src/components/SessionSidebar.tsx b/frontend/packages/inspector/src/components/SessionSidebar.tsx index 525305f..42f2640 100644 --- a/frontend/packages/inspector/src/components/SessionSidebar.tsx +++ b/frontend/packages/inspector/src/components/SessionSidebar.tsx @@ -82,12 +82,8 @@ const SessionSidebar = ({ }} > {agentLabels[agent.id] ?? agent.id} - {agent.installed && ( - - Installed - {agent.version && v{agent.version}} - - )} + {agent.installed && Installed} + {agent.version && v{agent.version}} ))} diff --git a/frontend/packages/inspector/src/components/chat/ChatPanel.tsx b/frontend/packages/inspector/src/components/chat/ChatPanel.tsx index 9733465..2586f63 100644 --- a/frontend/packages/inspector/src/components/chat/ChatPanel.tsx +++ b/frontend/packages/inspector/src/components/chat/ChatPanel.tsx @@ -202,12 +202,8 @@ const ChatPanel = ({ }} > {agentLabels[agent.id] ?? agent.id} - {agent.installed && ( - - Installed - {agent.version && v{agent.version}} - - )} + {agent.installed && Installed} + {agent.version && v{agent.version}} ))} diff --git a/server/packages/sandbox-agent/src/router.rs b/server/packages/sandbox-agent/src/router.rs index 12e4ced..6734a31 100644 --- a/server/packages/sandbox-agent/src/router.rs +++ b/server/packages/sandbox-agent/src/router.rs @@ -265,6 +265,7 @@ struct SessionState { opencode_stream_started: bool, codex_sender: Option>, session_started_emitted: bool, + last_claude_message_id: Option, } #[derive(Debug, Clone)] @@ -318,6 +319,7 @@ impl SessionState { opencode_stream_started: false, codex_sender: None, session_started_emitted: false, + last_claude_message_id: None, }) } @@ -2066,6 +2068,11 @@ impl SessionManager { break; } } + } else if agent == AgentId::Claude { + let conversions = self.parse_claude_line(&line, &session_id).await; + if !conversions.is_empty() { + let _ = self.record_conversions(&session_id, conversions).await; + } } else { let conversions = parse_agent_line(agent, &line, &session_id); if !conversions.is_empty() { @@ -2176,6 +2183,53 @@ impl SessionManager { Ok(session.record_conversions(conversions)) } + async fn parse_claude_line(&self, line: &str, session_id: &str) -> Vec { + let trimmed = line.trim(); + if trimmed.is_empty() { + return Vec::new(); + } + let mut value: Value = match serde_json::from_str(trimmed) { + Ok(value) => value, + Err(err) => { + return vec![agent_unparsed( + "claude", + &err.to_string(), + Value::String(trimmed.to_string()), + )]; + } + }; + let event_type = value.get("type").and_then(Value::as_str).unwrap_or(""); + if event_type == "assistant" { + if let Some(id) = value + .get("message") + .and_then(|message| message.get("id")) + .and_then(Value::as_str) + { + let mut sessions = self.sessions.lock().await; + if let Some(session) = Self::session_mut(&mut sessions, session_id) { + session.last_claude_message_id = Some(id.to_string()); + } + } + } else if event_type == "result" + && value.get("message_id").is_none() + && value.get("messageId").is_none() + { + let last_id = { + let sessions = self.sessions.lock().await; + Self::session_ref(&sessions, session_id) + .and_then(|session| session.last_claude_message_id.clone()) + }; + if let Some(id) = last_id { + if let Some(map) = value.as_object_mut() { + map.insert("message_id".to_string(), Value::String(id)); + } + } + } + + convert_claude::event_to_universal_with_session(&value, session_id.to_string()) + .unwrap_or_else(|err| vec![agent_unparsed("claude", &err, value)]) + } + async fn record_error( &self, session_id: &str, diff --git a/server/packages/universal-agent-schema/src/agents/claude.rs b/server/packages/universal-agent-schema/src/agents/claude.rs index 8d16c75..0d085b4 100644 --- a/server/packages/universal-agent-schema/src/agents/claude.rs +++ b/server/packages/universal-agent-schema/src/agents/claude.rs @@ -31,6 +31,7 @@ pub fn event_to_universal_with_session( let event_type = event.get("type").and_then(Value::as_str).unwrap_or(""); let mut conversions = match event_type { "system" => vec![system_event_to_universal(event)], + "user" => Vec::new(), "assistant" => assistant_event_to_universal(event, &session_id), "tool_use" => tool_use_event_to_universal(event, &session_id), "tool_result" => tool_result_event_to_universal(event), @@ -63,7 +64,7 @@ fn assistant_event_to_universal(event: &Value, session_id: &str) -> Vec Vec { fn result_event_to_universal(event: &Value, session_id: &str) -> Vec { // The `result` event completes the message started by `assistant`. // Use the same native_item_id so they link to the same universal item. - let native_message_id = format!("{session_id}_message"); + let native_message_id = claude_message_id(event, session_id); let result_text = event .get("result") .and_then(Value::as_str) @@ -251,6 +252,17 @@ fn result_event_to_universal(event: &Value, session_id: &str) -> Vec String { + event + .get("message") + .and_then(|message| message.get("id")) + .and_then(Value::as_str) + .or_else(|| event.get("message_id").and_then(Value::as_str)) + .or_else(|| event.get("messageId").and_then(Value::as_str)) + .map(|id| id.to_string()) + .unwrap_or_else(|| format!("{session_id}_message")) +} + fn item_events(item: UniversalItem, synthetic_start: bool) -> Vec { let mut events = Vec::new(); if synthetic_start {