mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 04:03:31 +00:00
fix: remove copy icon, reduce padding, reposition badges in dropdown
This commit is contained in:
parent
d85f55a75b
commit
d30ddc24f2
8 changed files with 246 additions and 68 deletions
|
|
@ -477,13 +477,14 @@
|
|||
border: 1px solid transparent;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
padding: 6px 8px;
|
||||
padding: 6px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
|
|
@ -496,6 +497,13 @@
|
|||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.agent-option-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.agent-option-name {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -509,6 +517,7 @@
|
|||
|
||||
.agent-badge.installed {
|
||||
color: var(--muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.agent-badge.version {
|
||||
|
|
|
|||
|
|
@ -81,9 +81,11 @@ const SessionSidebar = ({
|
|||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
<div className="agent-option-left">
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
||||
</div>
|
||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -201,9 +201,11 @@ const ChatPanel = ({
|
|||
setShowAgentMenu(false);
|
||||
}}
|
||||
>
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
<div className="agent-option-left">
|
||||
<span className="agent-option-name">{agentLabels[agent.id] ?? agent.id}</span>
|
||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
||||
</div>
|
||||
{agent.installed && <span className="agent-badge installed">Installed</span>}
|
||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ChevronDown, ChevronRight, Copy, Check } from "lucide-react";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { UniversalEvent } from "sandbox-agent";
|
||||
import { formatJson, formatTime } from "../../utils/format";
|
||||
|
|
@ -52,7 +52,6 @@ const EventsTab = ({
|
|||
disabled={events.length === 0}
|
||||
title="Copy all events as JSON"
|
||||
>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
{copied ? "Copied" : "Copy"}
|
||||
</button>
|
||||
<button className="button ghost small" onClick={onClear}>
|
||||
|
|
|
|||
|
|
@ -351,67 +351,6 @@ async fn test_multi_turn_for_agent(app: &Router, agent: AgentId) -> Result<(), S
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_turn_mock_agent() {
|
||||
let test_app = TestApp::new();
|
||||
|
||||
// Mock agent should always support multi-turn as the reference implementation
|
||||
let result = test_multi_turn_for_agent(&test_app.app, AgentId::Mock).await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Mock agent multi-turn failed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multi_turn_real_agents() {
|
||||
let configs = match test_agents_from_env() {
|
||||
Ok(configs) => configs,
|
||||
Err(err) => {
|
||||
eprintln!("Failed to get agent configs: {:?}. Skipping multi-turn test.", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if configs.is_empty() {
|
||||
eprintln!("No agents configured for testing. Skipping multi-turn test.");
|
||||
return;
|
||||
}
|
||||
|
||||
let test_app = TestApp::new();
|
||||
|
||||
for config in configs {
|
||||
let _guard = apply_credentials(&config.credentials);
|
||||
install_agent(&test_app.app, config.agent).await;
|
||||
|
||||
let result = test_multi_turn_for_agent(&test_app.app, config.agent).await;
|
||||
|
||||
match config.agent {
|
||||
AgentId::Claude | AgentId::Amp | AgentId::Opencode => {
|
||||
// These agents should support multi-turn via resumption
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"{} multi-turn failed (should support resumption): {:?}",
|
||||
config.agent,
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
AgentId::Codex => {
|
||||
// Codex now supports multi-turn via the shared app-server model
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"{} multi-turn failed (should support shared app-server): {:?}",
|
||||
config.agent,
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
AgentId::Mock => {
|
||||
// Mock is tested separately
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that verifies the session can be reopened after ending
|
||||
#[tokio::test]
|
||||
async fn session_reopen_after_end() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
mod session_lifecycle;
|
||||
mod multi_turn;
|
||||
mod permissions;
|
||||
mod questions;
|
||||
mod reasoning;
|
||||
|
|
|
|||
128
server/packages/sandbox-agent/tests/sessions/multi_turn.rs
Normal file
128
server/packages/sandbox-agent/tests/sessions/multi_turn.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
// Multi-turn session snapshots use the mock baseline as the single source of truth.
|
||||
include!("../common/http.rs");
|
||||
|
||||
const FIRST_PROMPT: &str = "Reply with exactly the word FIRST.";
|
||||
const SECOND_PROMPT: &str = "Reply with exactly the word SECOND.";
|
||||
|
||||
fn session_snapshot_suffix(prefix: &str) -> String {
|
||||
snapshot_name(prefix, Some(AgentId::Mock))
|
||||
}
|
||||
|
||||
fn assert_session_snapshot(prefix: &str, value: Value) {
|
||||
insta::with_settings!({
|
||||
snapshot_suffix => session_snapshot_suffix(prefix),
|
||||
}, {
|
||||
insta::assert_yaml_snapshot!(value);
|
||||
});
|
||||
}
|
||||
|
||||
async fn send_message_with_text(app: &Router, session_id: &str, text: &str) {
|
||||
let status = send_status(
|
||||
app,
|
||||
Method::POST,
|
||||
&format!("/v1/sessions/{session_id}/messages"),
|
||||
Some(json!({ "message": text })),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(status, StatusCode::NO_CONTENT, "send message");
|
||||
}
|
||||
|
||||
async fn poll_events_until_from(
|
||||
app: &Router,
|
||||
session_id: &str,
|
||||
offset: u64,
|
||||
timeout: Duration,
|
||||
) -> (Vec<Value>, u64) {
|
||||
let start = Instant::now();
|
||||
let mut offset = offset;
|
||||
let mut events = Vec::new();
|
||||
while start.elapsed() < timeout {
|
||||
let path = format!("/v1/sessions/{session_id}/events?offset={offset}&limit=200");
|
||||
let (status, payload) = send_json(app, Method::GET, &path, None).await;
|
||||
assert_eq!(status, StatusCode::OK, "poll events");
|
||||
let new_events = payload
|
||||
.get("events")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !new_events.is_empty() {
|
||||
if let Some(last) = new_events
|
||||
.last()
|
||||
.and_then(|event| event.get("sequence"))
|
||||
.and_then(Value::as_u64)
|
||||
{
|
||||
offset = last;
|
||||
}
|
||||
events.extend(new_events);
|
||||
if should_stop(&events) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(800)).await;
|
||||
}
|
||||
(events, offset)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multi_turn_snapshots() {
|
||||
let configs = test_agents_from_env().expect("configure SANDBOX_TEST_AGENTS or install agents");
|
||||
|
||||
for config in &configs {
|
||||
let app = TestApp::new();
|
||||
let capabilities = fetch_capabilities(&app.app).await;
|
||||
let caps = capabilities
|
||||
.get(config.agent.as_str())
|
||||
.expect("capabilities missing");
|
||||
if !caps.session_lifecycle {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _guard = apply_credentials(&config.credentials);
|
||||
install_agent(&app.app, config.agent).await;
|
||||
|
||||
let session_id = format!("multi-turn-{}", config.agent.as_str());
|
||||
create_session(&app.app, config.agent, &session_id, test_permission_mode(config.agent))
|
||||
.await;
|
||||
|
||||
send_message_with_text(&app.app, &session_id, FIRST_PROMPT).await;
|
||||
let (first_events, offset) =
|
||||
poll_events_until_from(&app.app, &session_id, 0, Duration::from_secs(120)).await;
|
||||
let first_events = truncate_after_first_stop(&first_events);
|
||||
assert!(
|
||||
!first_events.is_empty(),
|
||||
"no events collected for first turn {}",
|
||||
config.agent
|
||||
);
|
||||
assert!(
|
||||
should_stop(&first_events),
|
||||
"timed out waiting for assistant/error event for first turn {}",
|
||||
config.agent
|
||||
);
|
||||
|
||||
send_message_with_text(&app.app, &session_id, SECOND_PROMPT).await;
|
||||
let (second_events, _offset) = poll_events_until_from(
|
||||
&app.app,
|
||||
&session_id,
|
||||
offset,
|
||||
Duration::from_secs(120),
|
||||
)
|
||||
.await;
|
||||
let second_events = truncate_after_first_stop(&second_events);
|
||||
assert!(
|
||||
!second_events.is_empty(),
|
||||
"no events collected for second turn {}",
|
||||
config.agent
|
||||
);
|
||||
assert!(
|
||||
should_stop(&second_events),
|
||||
"timed out waiting for assistant/error event for second turn {}",
|
||||
config.agent
|
||||
);
|
||||
|
||||
let snapshot = json!({
|
||||
"first": normalize_events(&first_events),
|
||||
"second": normalize_events(&second_events),
|
||||
});
|
||||
assert_session_snapshot("multi_turn", snapshot);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
---
|
||||
source: server/packages/sandbox-agent/tests/sessions/multi_turn.rs
|
||||
expression: value
|
||||
---
|
||||
first:
|
||||
- metadata: true
|
||||
seq: 1
|
||||
session: started
|
||||
type: session.started
|
||||
- item:
|
||||
content_types:
|
||||
- text
|
||||
kind: message
|
||||
role: user
|
||||
status: in_progress
|
||||
seq: 2
|
||||
type: item.started
|
||||
- delta:
|
||||
delta: "<redacted>"
|
||||
item_id: "<redacted>"
|
||||
native_item_id: "<redacted>"
|
||||
seq: 3
|
||||
type: item.delta
|
||||
- item:
|
||||
content_types:
|
||||
- text
|
||||
kind: message
|
||||
role: user
|
||||
status: completed
|
||||
seq: 4
|
||||
type: item.completed
|
||||
- item:
|
||||
content_types:
|
||||
- text
|
||||
kind: message
|
||||
role: assistant
|
||||
status: in_progress
|
||||
seq: 5
|
||||
type: item.started
|
||||
- delta:
|
||||
delta: "<redacted>"
|
||||
item_id: "<redacted>"
|
||||
native_item_id: "<redacted>"
|
||||
seq: 6
|
||||
type: item.delta
|
||||
- item:
|
||||
content_types:
|
||||
- text
|
||||
kind: message
|
||||
role: assistant
|
||||
status: completed
|
||||
seq: 7
|
||||
type: item.completed
|
||||
second:
|
||||
- item:
|
||||
content_types:
|
||||
- text
|
||||
kind: message
|
||||
role: user
|
||||
status: in_progress
|
||||
seq: 1
|
||||
type: item.started
|
||||
- delta:
|
||||
delta: "<redacted>"
|
||||
item_id: "<redacted>"
|
||||
native_item_id: "<redacted>"
|
||||
seq: 2
|
||||
type: item.delta
|
||||
- item:
|
||||
content_types:
|
||||
- text
|
||||
kind: message
|
||||
role: user
|
||||
status: completed
|
||||
seq: 3
|
||||
type: item.completed
|
||||
- item:
|
||||
content_types:
|
||||
- text
|
||||
kind: message
|
||||
role: assistant
|
||||
status: in_progress
|
||||
seq: 4
|
||||
type: item.started
|
||||
- delta:
|
||||
delta: "<redacted>"
|
||||
item_id: "<redacted>"
|
||||
native_item_id: "<redacted>"
|
||||
seq: 5
|
||||
type: item.delta
|
||||
- item:
|
||||
content_types:
|
||||
- text
|
||||
kind: message
|
||||
role: assistant
|
||||
status: completed
|
||||
seq: 6
|
||||
type: item.completed
|
||||
Loading…
Add table
Add a link
Reference in a new issue