From 990b2e912431b4ed470d83016f3164d5e715ae93 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 4 Mar 2026 08:45:42 -0500 Subject: [PATCH] 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 --- crates/arc-agent/src/docker_sandbox.rs | 4 ++ crates/arc-agent/src/local_sandbox.rs | 4 ++ crates/arc-agent/src/sandbox.rs | 9 +++++ crates/arc-agent/src/test_support.rs | 4 ++ crates/arc-workflows/src/cli/mod.rs | 13 ++++++- crates/arc-workflows/src/cli/progress.rs | 42 ++++++++++++++++++++- crates/arc-workflows/src/daytona_sandbox.rs | 7 ++++ 7 files changed, 80 insertions(+), 3 deletions(-) diff --git a/crates/arc-agent/src/docker_sandbox.rs b/crates/arc-agent/src/docker_sandbox.rs index a5fcffa7f..2abae4a0c 100644 --- a/crates/arc-agent/src/docker_sandbox.rs +++ b/crates/arc-agent/src/docker_sandbox.rs @@ -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(()) diff --git a/crates/arc-agent/src/local_sandbox.rs b/crates/arc-agent/src/local_sandbox.rs index ba4ea55f3..9eeb6679e 100644 --- a/crates/arc-agent/src/local_sandbox.rs +++ b/crates/arc-agent/src/local_sandbox.rs @@ -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(), diff --git a/crates/arc-agent/src/sandbox.rs b/crates/arc-agent/src/sandbox.rs index a96ecc71e..cdd8972b6 100644 --- a/crates/arc-agent/src/sandbox.rs +++ b/crates/arc-agent/src/sandbox.rs @@ -102,6 +102,10 @@ pub enum SandboxEvent { Ready { provider: String, duration_ms: u64, + name: Option, + cpu: Option, + memory: Option, + url: Option, }, 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(), diff --git a/crates/arc-agent/src/test_support.rs b/crates/arc-agent/src/test_support.rs index 7f3ac576a..41d97097c 100644 --- a/crates/arc-agent/src/test_support.rs +++ b/crates/arc-agent/src/test_support.rs @@ -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(()) } diff --git a/crates/arc-workflows/src/cli/mod.rs b/crates/arc-workflows/src/cli/mod.rs index 51c67d233..c316b6cb7 100644 --- a/crates/arc-workflows/src/cli/mod.rs +++ b/crates/arc-workflows/src/cli/mod.rs @@ -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()); diff --git a/crates/arc-workflows/src/cli/progress.rs b/crates/arc-workflows/src/cli/progress.rs index 9ec243749..79b4035c8 100644 --- a/crates/arc-workflows/src/cli/progress.rs +++ b/crates/arc-workflows/src/cli/progress.rs @@ -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}"); + } } } } diff --git a/crates/arc-workflows/src/daytona_sandbox.rs b/crates/arc-workflows/src/daytona_sandbox.rs index 392b2f0dd..622553d09 100644 --- a/crates/arc-workflows/src/daytona_sandbox.rs +++ b/crates/arc-workflows/src/daytona_sandbox.rs @@ -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(())