Add sandbox details and hyperlink to progress output

Show sandbox name, CPU, and memory on a second indented detail line
beneath the "Sandbox: provider" line. Hyperlink the provider name to
its dashboard URL using OSC 8 terminal escape sequences (TTY only).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-04 08:45:42 -05:00
parent bde59168c9
commit 990b2e9124
7 changed files with 80 additions and 3 deletions

View file

@ -398,6 +398,10 @@ impl Sandbox for DockerSandbox {
self.emit(SandboxEvent::Ready {
provider: "docker".into(),
duration_ms: init_duration,
name: None,
cpu: None,
memory: None,
url: None,
});
Ok(())

View file

@ -379,6 +379,10 @@ impl Sandbox for LocalSandbox {
Ok(()) => self.emit(SandboxEvent::Ready {
provider: "local".into(),
duration_ms,
name: None,
cpu: None,
memory: None,
url: None,
}),
Err(e) => self.emit(SandboxEvent::InitializeFailed {
provider: "local".into(),

View file

@ -102,6 +102,10 @@ pub enum SandboxEvent {
Ready {
provider: String,
duration_ms: u64,
name: Option<String>,
cpu: Option<f64>,
memory: Option<f64>,
url: Option<String>,
},
InitializeFailed {
provider: String,
@ -170,6 +174,7 @@ impl SandboxEvent {
Self::Ready {
provider,
duration_ms,
..
} => {
info!(provider, duration_ms, "Sandbox ready");
}
@ -405,6 +410,10 @@ mod tests {
SandboxEvent::Ready {
provider: "local".into(),
duration_ms: 50,
name: None,
cpu: None,
memory: None,
url: None,
},
SandboxEvent::InitializeFailed {
provider: "docker".into(),

View file

@ -189,6 +189,10 @@ impl Sandbox for MockSandbox {
self.emit(crate::sandbox::SandboxEvent::Ready {
provider: "mock".into(),
duration_ms: 0,
name: None,
cpu: None,
memory: None,
url: None,
});
Ok(())
}

View file

@ -495,7 +495,14 @@ pub fn format_event_summary(event: &WorkflowRunEvent, styles: &Styles) -> String
use arc_agent::SandboxEvent;
match event {
SandboxEvent::Initializing { provider } => format!("[SANDBOX_INITIALIZING] provider={provider}"),
SandboxEvent::Ready { provider, duration_ms } => format!("[SANDBOX_READY] provider={provider} duration={duration_ms}ms"),
SandboxEvent::Ready { provider, duration_ms, name, cpu, memory, .. } => {
let mut s = format!("[SANDBOX_READY] provider={provider}");
if let Some(n) = name { s.push_str(&format!(" name={n}")); }
if let Some(c) = cpu { s.push_str(&format!(" cpu={c}")); }
if let Some(m) = memory { s.push_str(&format!(" memory={m}")); }
s.push_str(&format!(" duration={duration_ms}ms"));
s
}
SandboxEvent::InitializeFailed { provider, error, duration_ms } => format!("[SANDBOX_INIT_FAILED] provider={provider} error=\"{error}\" duration={duration_ms}ms"),
SandboxEvent::CleanupStarted { provider } => format!("[SANDBOX_CLEANUP_STARTED] provider={provider}"),
SandboxEvent::CleanupCompleted { provider, duration_ms } => format!("[SANDBOX_CLEANUP_COMPLETED] provider={provider} duration={duration_ms}ms"),
@ -1451,6 +1458,10 @@ mod tests {
event: arc_agent::SandboxEvent::Ready {
provider: "docker".into(),
duration_ms: 1500,
name: None,
cpu: None,
memory: None,
url: None,
},
};
let s = format_event_summary(&event, test_styles());

View file

@ -43,6 +43,7 @@ cached_style!(
);
cached_style!(style_tool_done, " {wide_msg} {prefix:.dim}");
cached_style!(style_static_dim, " {wide_msg:.dim}");
cached_style!(style_sandbox_detail, " {wide_msg:.dim}");
cached_style!(style_empty, " ");
// ── Cached glyphs ───────────────────────────────────────────────────────
@ -74,6 +75,20 @@ fn format_duration_ms(ms: u64) -> String {
format_duration_short(Duration::from_millis(ms))
}
/// Wrap `text` in an OSC 8 terminal hyperlink pointing to `url`.
fn terminal_hyperlink(url: &str, text: &str) -> String {
format!("\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\")
}
/// Format a number as an integer if whole, one decimal otherwise.
fn format_number(n: f64) -> String {
if (n - n.round()).abs() < f64::EPSILON {
format!("{}", n as i64)
} else {
format!("{n:.1}")
}
}
// ── Tool call display name ──────────────────────────────────────────────
fn truncate(s: &str, max: usize) -> String {
@ -290,18 +305,41 @@ impl ProgressUI {
SandboxEvent::Ready {
provider,
duration_ms,
name,
cpu,
memory,
url,
} => {
let dur = format_duration_ms(*duration_ms);
let detail = match (name, cpu, memory) {
(Some(n), Some(c), Some(m)) => {
Some(format!("{n} ({} cpu, {} GB)", format_number(*c), format_number(*m)))
}
(Some(n), _, _) => Some(n.clone()),
_ => None,
};
match &self.renderer {
ProgressRenderer::Tty(_) => {
ProgressRenderer::Tty(tty) => {
let display_provider = match url {
Some(u) => terminal_hyperlink(u, provider),
None => provider.clone(),
};
if let Some(bar) = self.sandbox_bar.take() {
bar.set_style(style_header_done());
bar.set_prefix(dur);
bar.finish_with_message(format!("Sandbox: {provider}"));
bar.finish_with_message(format!("Sandbox: {display_provider}"));
}
if let Some(detail_str) = &detail {
let detail_bar = tty.multi.add(ProgressBar::new_spinner());
detail_bar.set_style(style_sandbox_detail());
detail_bar.finish_with_message(detail_str.clone());
}
}
ProgressRenderer::Plain => {
eprintln!(" Sandbox: {provider} (ready in {dur})");
if let Some(detail_str) = &detail {
eprintln!(" {detail_str}");
}
}
}
}

View file

@ -458,6 +458,9 @@ impl Sandbox for DaytonaSandbox {
}
}
let sandbox_name = sandbox.name.clone();
let sandbox_cpu = sandbox.cpu;
let sandbox_memory = sandbox.memory;
self.sandbox
.set(sandbox)
.map_err(|_| "Daytona sandbox already initialized".to_string())?;
@ -467,6 +470,10 @@ impl Sandbox for DaytonaSandbox {
self.emit(SandboxEvent::Ready {
provider: "daytona".into(),
duration_ms: init_duration,
name: Some(sandbox_name),
cpu: Some(sandbox_cpu),
memory: Some(sandbox_memory),
url: Some("https://app.daytona.io/dashboard/sandboxes".into()),
});
Ok(())