mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 09:01:17 +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;
|
border: 1px solid transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 6px 8px;
|
padding: 6px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition);
|
transition: all var(--transition);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -496,6 +497,13 @@
|
||||||
color: rgba(255, 255, 255, 0.6);
|
color: rgba(255, 255, 255, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-option-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.agent-option-name {
|
.agent-option-name {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
@ -509,6 +517,7 @@
|
||||||
|
|
||||||
.agent-badge.installed {
|
.agent-badge.installed {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-badge.version {
|
.agent-badge.version {
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,11 @@ const SessionSidebar = ({
|
||||||
setShowMenu(false);
|
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.installed && <span className="agent-badge installed">Installed</span>}
|
||||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -201,9 +201,11 @@ const ChatPanel = ({
|
||||||
setShowAgentMenu(false);
|
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.installed && <span className="agent-badge installed">Installed</span>}
|
||||||
{agent.version && <span className="agent-badge version">v{agent.version}</span>}
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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 { useEffect, useState } from "react";
|
||||||
import type { UniversalEvent } from "sandbox-agent";
|
import type { UniversalEvent } from "sandbox-agent";
|
||||||
import { formatJson, formatTime } from "../../utils/format";
|
import { formatJson, formatTime } from "../../utils/format";
|
||||||
|
|
@ -52,7 +52,6 @@ const EventsTab = ({
|
||||||
disabled={events.length === 0}
|
disabled={events.length === 0}
|
||||||
title="Copy all events as JSON"
|
title="Copy all events as JSON"
|
||||||
>
|
>
|
||||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
|
||||||
{copied ? "Copied" : "Copy"}
|
{copied ? "Copied" : "Copy"}
|
||||||
</button>
|
</button>
|
||||||
<button className="button ghost small" onClick={onClear}>
|
<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(())
|
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
|
/// Test that verifies the session can be reopened after ending
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn session_reopen_after_end() {
|
async fn session_reopen_after_end() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
mod session_lifecycle;
|
mod session_lifecycle;
|
||||||
|
mod multi_turn;
|
||||||
mod permissions;
|
mod permissions;
|
||||||
mod questions;
|
mod questions;
|
||||||
mod reasoning;
|
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