feat: add copy button to events tab to copy all events as JSON

This commit is contained in:
Nathan Flurry 2026-01-27 20:31:38 -08:00
parent 6782836030
commit f2c060903f
8 changed files with 156 additions and 102 deletions

View file

@ -1,4 +1,4 @@
import { ChevronDown, ChevronRight } from "lucide-react";
import { ChevronDown, ChevronRight, Copy, Check } from "lucide-react";
import { useEffect, useState } from "react";
import type { UniversalEvent } from "sandbox-agent";
import { formatJson, formatTime } from "../../utils/format";
@ -20,6 +20,17 @@ const EventsTab = ({
error: string | null;
}) => {
const [collapsedEvents, setCollapsedEvents] = useState<Record<string, boolean>>({});
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(JSON.stringify(events, null, 2));
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy events:", err);
}
};
useEffect(() => {
if (events.length === 0) {
@ -35,6 +46,15 @@ const EventsTab = ({
<button className="button ghost small" onClick={onFetch} disabled={loading}>
{loading ? "Loading..." : "Fetch"}
</button>
<button
className="button ghost small"
onClick={handleCopy}
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}>
Clear
</button>

View file

@ -4,7 +4,7 @@ use std::io::{BufRead, BufReader, Write};
use std::net::TcpListener;
use std::path::PathBuf;
use std::process::Stdio;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, Weak};
use std::time::{Duration, Instant};
@ -46,6 +46,7 @@ use sandbox_agent_agent_management::credentials::{
use crate::agent_server_logs::AgentServerLogs;
const MOCK_EVENT_DELAY_MS: u64 = 200;
static USER_MESSAGE_COUNTER: AtomicU64 = AtomicU64::new(1);
#[derive(Debug)]
pub struct AppState {
@ -263,6 +264,7 @@ struct SessionState {
broadcaster: broadcast::Sender<UniversalEvent>,
opencode_stream_started: bool,
codex_sender: Option<mpsc::UnboundedSender<String>>,
session_started_emitted: bool,
}
#[derive(Debug, Clone)]
@ -315,6 +317,7 @@ impl SessionState {
broadcaster,
opencode_stream_started: false,
codex_sender: None,
session_started_emitted: false,
})
}
@ -394,6 +397,18 @@ impl SessionState {
}
fn push_event(&mut self, conversion: EventConversion) -> Option<UniversalEvent> {
if conversion.event_type == UniversalEventType::SessionStarted {
if self.session_started_emitted {
return None;
}
self.session_started_emitted = true;
}
if conversion.event_type == UniversalEventType::SessionEnded
&& agent_supports_resume(self.agent)
&& !conversion.synthetic
{
return None;
}
if conversion.event_type == UniversalEventType::ItemStarted {
if let UniversalEventData::Item(ref data) = conversion.data {
if self.item_started.contains(&data.item.item_id) {
@ -1463,6 +1478,11 @@ impl SessionManager {
self.send_mock_message(session_id, message).await?;
return Ok(());
}
if matches!(session_snapshot.agent, AgentId::Claude | AgentId::Amp) {
let _ = self
.record_conversions(&session_id, user_message_conversions(&message))
.await;
}
if session_snapshot.agent == AgentId::Opencode {
self.ensure_opencode_stream(session_id.clone()).await?;
self.send_opencode_prompt(&session_snapshot, &message)
@ -2067,15 +2087,17 @@ impl SessionManager {
let status = tokio::task::spawn_blocking(move || child.wait()).await;
match status {
Ok(Ok(status)) if status.success() => {
let message = format!("agent exited with status {:?}", status);
self.mark_session_ended(
&session_id,
status.code(),
&message,
SessionEndReason::Completed,
TerminatedBy::Agent,
)
.await;
if !agent_supports_resume(agent) {
let message = format!("agent exited with status {:?}", status);
self.mark_session_ended(
&session_id,
status.code(),
&message,
SessionEndReason::Completed,
TerminatedBy::Agent,
)
.await;
}
}
Ok(Ok(status)) => {
let message = format!("agent exited with status {:?}", status);
@ -5035,6 +5057,46 @@ fn mock_user_message(prefix: &str, text: &str) -> Vec<EventConversion> {
]
}
fn user_message_conversions(text: &str) -> Vec<EventConversion> {
let id = USER_MESSAGE_COUNTER.fetch_add(1, Ordering::Relaxed);
let native_item_id = format!("user_{id}");
let content = vec![ContentPart::Text {
text: text.to_string(),
}];
vec![
EventConversion::new(
UniversalEventType::ItemStarted,
UniversalEventData::Item(ItemEventData {
item: UniversalItem {
item_id: String::new(),
native_item_id: Some(native_item_id.clone()),
parent_id: None,
kind: ItemKind::Message,
role: Some(ItemRole::User),
content: content.clone(),
status: ItemStatus::InProgress,
},
}),
)
.synthetic(),
EventConversion::new(
UniversalEventType::ItemCompleted,
UniversalEventData::Item(ItemEventData {
item: UniversalItem {
item_id: String::new(),
native_item_id: Some(native_item_id),
parent_id: None,
kind: ItemKind::Message,
role: Some(ItemRole::User),
content,
status: ItemStatus::Completed,
},
}),
)
.synthetic(),
]
}
fn mock_assistant_message(native_item_id: String, text: String) -> Vec<EventConversion> {
let content = vec![ContentPart::Text { text }];
vec![

View file

@ -6,23 +6,19 @@ expression: value
seq: 1
session: started
type: session.started
- metadata: true
seq: 2
session: started
type: session.started
- item:
content_types:
- text
kind: message
role: user
status: in_progress
seq: 3
seq: 2
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 4
seq: 3
type: item.delta
- item:
content_types:
@ -30,11 +26,11 @@ expression: value
kind: message
role: user
status: completed
seq: 5
seq: 4
type: item.completed
- permission:
action: command_execution
id: "<redacted>"
status: requested
seq: 6
seq: 5
type: permission.requested

View file

@ -6,23 +6,19 @@ expression: value
seq: 1
session: started
type: session.started
- metadata: true
seq: 2
session: started
type: session.started
- item:
content_types:
- text
kind: message
role: user
status: in_progress
seq: 3
seq: 2
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 4
seq: 3
type: item.delta
- item:
content_types:
@ -30,11 +26,11 @@ expression: value
kind: message
role: user
status: completed
seq: 5
seq: 4
type: item.completed
- question:
id: "<redacted>"
options: 2
status: requested
seq: 6
seq: 5
type: question.requested

View file

@ -6,23 +6,19 @@ expression: value
seq: 1
session: started
type: session.started
- metadata: true
seq: 2
session: started
type: session.started
- item:
content_types:
- text
kind: message
role: user
status: in_progress
seq: 3
seq: 2
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 4
seq: 3
type: item.delta
- item:
content_types:
@ -30,11 +26,11 @@ expression: value
kind: message
role: user
status: completed
seq: 5
seq: 4
type: item.completed
- question:
id: "<redacted>"
options: 2
status: requested
seq: 6
seq: 5
type: question.requested

View file

@ -7,23 +7,19 @@ session_a:
seq: 1
session: started
type: session.started
- metadata: true
seq: 2
session: started
type: session.started
- item:
content_types:
- text
kind: message
role: user
status: in_progress
seq: 3
seq: 2
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 4
seq: 3
type: item.delta
- item:
content_types:
@ -31,7 +27,7 @@ session_a:
kind: message
role: user
status: completed
seq: 5
seq: 4
type: item.completed
- item:
content_types:
@ -39,13 +35,13 @@ session_a:
kind: message
role: assistant
status: in_progress
seq: 6
seq: 5
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 7
seq: 6
type: item.delta
- item:
content_types:
@ -53,30 +49,26 @@ session_a:
kind: message
role: assistant
status: completed
seq: 8
seq: 7
type: item.completed
session_b:
- metadata: true
seq: 1
session: started
type: session.started
- metadata: true
- item:
content_types:
- text
kind: message
role: user
status: in_progress
seq: 2
session: started
type: session.started
- item:
content_types:
- text
kind: message
role: user
status: in_progress
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 3
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 4
type: item.delta
- item:
content_types:
@ -84,7 +76,7 @@ session_b:
kind: message
role: user
status: completed
seq: 5
seq: 4
type: item.completed
- item:
content_types:
@ -92,13 +84,13 @@ session_b:
kind: message
role: assistant
status: in_progress
seq: 6
seq: 5
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 7
seq: 6
type: item.delta
- item:
content_types:
@ -106,5 +98,5 @@ session_b:
kind: message
role: assistant
status: completed
seq: 8
seq: 7
type: item.completed

View file

@ -6,23 +6,19 @@ expression: normalized
seq: 1
session: started
type: session.started
- metadata: true
- item:
content_types:
- text
kind: message
role: user
status: in_progress
seq: 2
session: started
type: session.started
- item:
content_types:
- text
kind: message
role: user
status: in_progress
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 3
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 4
type: item.delta
- item:
content_types:
@ -30,7 +26,7 @@ expression: normalized
kind: message
role: user
status: completed
seq: 5
seq: 4
type: item.completed
- item:
content_types:
@ -38,13 +34,13 @@ expression: normalized
kind: message
role: assistant
status: in_progress
seq: 6
seq: 5
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 7
seq: 6
type: item.delta
- item:
content_types:
@ -52,5 +48,5 @@ expression: normalized
kind: message
role: assistant
status: completed
seq: 8
seq: 7
type: item.completed

View file

@ -6,23 +6,19 @@ expression: normalized
seq: 1
session: started
type: session.started
- metadata: true
- item:
content_types:
- text
kind: message
role: user
status: in_progress
seq: 2
session: started
type: session.started
- item:
content_types:
- text
kind: message
role: user
status: in_progress
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 3
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 4
type: item.delta
- item:
content_types:
@ -30,7 +26,7 @@ expression: normalized
kind: message
role: user
status: completed
seq: 5
seq: 4
type: item.completed
- item:
content_types:
@ -38,13 +34,13 @@ expression: normalized
kind: message
role: assistant
status: in_progress
seq: 6
seq: 5
type: item.started
- delta:
delta: "<redacted>"
item_id: "<redacted>"
native_item_id: "<redacted>"
seq: 7
seq: 6
type: item.delta
- item:
content_types:
@ -52,5 +48,5 @@ expression: normalized
kind: message
role: assistant
status: completed
seq: 8
seq: 7
type: item.completed