From f71d36ade3808377a3513bd7bc6f47670532e604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radim=20H=C3=B6fer?= Date: Thu, 8 Oct 2026 13:04:54 +0200 Subject: [PATCH 1/2] fix(agent): show progress while starting a remote agent session agent run --remote created its Agent Proxy session with no spinner, so a slow or failing request left the terminal silent after the startup warnings. Show 'Starting remote agent session...' during the request and 'Remote agent session started (N bindings)' once it opens. --- src/handlers/entry/root.rs | 51 ++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/src/handlers/entry/root.rs b/src/handlers/entry/root.rs index 24049322..ad779ca8 100644 --- a/src/handlers/entry/root.rs +++ b/src/handlers/entry/root.rs @@ -373,6 +373,17 @@ fn audit_binding_sources( .collect() } +/// The line an `agent run --remote` prints once the control plane has opened +/// its session. The binding values stay remote, so it counts bindings, not secrets. +fn remote_session_started_message(binding_count: usize) -> String { + let noun = if binding_count == 1 { + "binding" + } else { + "bindings" + }; + format!("Remote agent session started ({binding_count} {noun})") +} + /// Returns the canonical source variables that must be removed from the child /// environment before binding placeholders are installed. fn remote_source_env_names(bindings: &[crate::api::remote_proxy::RemoteBinding]) -> Vec { @@ -1146,12 +1157,30 @@ pub async fn handle_cli(args: Cli) -> Exit { .unwrap_or(crate::handlers::run::routing::RemoteMode::Credential), ), }; - let session = crate::api::remote_proxy::create_session(&session_request, raw_output) - .await + let mut session_spinner = (!silent).then(|| { + crate::utils::spinner::new_spinner( + "Starting remote agent session...", + crate::utils::spinner::Streams::Stderr, + ) + }); + let session_result = + crate::api::remote_proxy::create_session(&session_request, raw_output) + .await; + if let Some(mut spinner) = session_spinner.take() { + spinner.clear(); + } + let session = session_result // The Agent Proxy setup follows startup warnings. Keep its // formatted API error visually distinct without changing // output spacing for every other CLI command. .map_err(|error| anyhow::anyhow!("\n{error}"))?; + if !silent { + eprintln!( + "{} {}", + "✓".green_if_tty_stderr(), + remote_session_started_message(bindings.len()) + ); + } let token = session.session_token.clone(); let routing = match crate::handlers::run::routing::resolve_session_routing( &session, @@ -2353,9 +2382,9 @@ mod tests { dependency_hooks_enabled, directory_profile_git_warning, ensure_replacement_session_is_compatible, exit_for, infer_remote_agent_type, is_bare_agent_hook, remote_bindings, remote_codex_command_with_mcp_binding_headers, - remote_session_rotation_delay_for, remote_session_transport_identity, - remote_source_env_names, secret_child_name, summarize_audit_events, - uses_local_dependency_hook_broker_mode, + remote_session_rotation_delay_for, remote_session_started_message, + remote_session_transport_identity, remote_source_env_names, secret_child_name, + summarize_audit_events, uses_local_dependency_hook_broker_mode, }; use crate::api::remote_proxy::{RemoteBinding, RemoteBindingSource}; use crate::cmd::root::Cli; @@ -2580,6 +2609,18 @@ mod tests { } } + #[test] + fn remote_session_started_message_counts_bindings() { + assert_eq!( + remote_session_started_message(1), + "Remote agent session started (1 binding)" + ); + assert_eq!( + remote_session_started_message(3), + "Remote agent session started (3 bindings)" + ); + } + #[test] fn remote_bindings_compile_mixed_sources_with_value_templates() { let binding = |from: Option<&str>| AgentBindingProfile { From b59baf7f7f1deea6e1f1fcb306af4a2e28c9f203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radim=20H=C3=B6fer?= Date: Thu, 8 Oct 2026 13:04:54 +0200 Subject: [PATCH 2/2] fix(run): say bindings rather than environment in a local agent run A local agent run loads only its profile's bindings, so its spinner and completion line now read 'Loading bindings...' and 'Bindings loaded (N bindings)'. Plain run keeps 'Loading environment...'. --- src/handlers/run/entry.rs | 43 ++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/handlers/run/entry.rs b/src/handlers/run/entry.rs index 6b8bdc0b..4d5cf53f 100644 --- a/src/handlers/run/entry.rs +++ b/src/handlers/run/entry.rs @@ -965,7 +965,7 @@ pub async fn handle_load_env_run(args: HandleRunArgs) -> anyhow::Result<()> { let mut spinner = if !silent { Some(crate::utils::spinner::new_spinner( - "Loading environment...", + loading_message(proxy_policy.is_some()), Streams::Stderr, )) } else { @@ -1826,20 +1826,25 @@ fn apply_secret_bindings( } } -/// Combines the remote fallback with local overrides. Names outside the -/// profile's requested source set are discarded before a child can receive them. +/// The spinner `run` shows while loading. An agent run loads only its +/// profile's bindings, so it names those rather than a whole environment. +fn loading_message(agent_run: bool) -> &'static str { + if agent_run { + "Loading bindings..." + } else { + "Loading environment..." + } +} + /// The line `run` prints once the secrets are loaded. "Egress-only profile" /// describes an agent profile, so plain `stashbase run` never says it. fn loaded_message(secret_count: usize, agent_run: bool) -> String { - let label = if agent_run && secret_count == 0 { - "Egress-only profile" - } else { - "Environment loaded" - }; - let noun = if secret_count == 1 { - "secret" - } else { - "secrets" + let (label, noun) = match (agent_run, secret_count) { + (true, 0) => ("Egress-only profile", "secrets"), + (true, 1) => ("Bindings loaded", "binding"), + (true, _) => ("Bindings loaded", "bindings"), + (false, 1) => ("Environment loaded", "secret"), + (false, _) => ("Environment loaded", "secrets"), }; format!("{label} ({secret_count} {noun})") } @@ -1858,6 +1863,8 @@ fn needs_remote_fetch(only: &[String], remote_only: &[String], agent_run: bool) } } +/// Combines the remote fallback with local overrides. Names outside the +/// profile's requested source set are discarded before a child can receive them. fn merge_remote_and_local_secrets( remote: Vec, local: Vec, @@ -1898,7 +1905,7 @@ fn missing_secret_labels( #[cfg(test)] mod tests { use super::{ - apply_secret_bindings, load_run_secrets_from_file, loaded_message, + apply_secret_bindings, load_run_secrets_from_file, loaded_message, loading_message, merge_remote_and_local_secrets, missing_secret_labels, needs_remote_fetch, prepare_local_run_secrets, }; @@ -2160,10 +2167,18 @@ mod tests { assert_eq!(missing, ["GH_TOKEN (from GITHUB_TOKEN)"]); } + #[test] + fn an_agent_run_loads_bindings_rather_than_an_environment() { + assert_eq!(loading_message(true), "Loading bindings..."); + assert_eq!(loading_message(false), "Loading environment..."); + } + #[test] fn only_an_agent_run_without_secrets_is_called_egress_only() { assert_eq!(loaded_message(0, true), "Egress-only profile (0 secrets)"); - assert_eq!(loaded_message(1, true), "Environment loaded (1 secret)"); + assert_eq!(loaded_message(1, true), "Bindings loaded (1 binding)"); + assert_eq!(loaded_message(3, true), "Bindings loaded (3 bindings)"); + assert_eq!(loaded_message(1, false), "Environment loaded (1 secret)"); assert_eq!(loaded_message(0, false), "Environment loaded (0 secrets)"); assert_eq!(loaded_message(10, false), "Environment loaded (10 secrets)"); }