diff --git a/app/src-tauri/src/file_viewer/poll.rs b/app/src-tauri/src/file_viewer/poll.rs index 905e172..165e297 100644 --- a/app/src-tauri/src/file_viewer/poll.rs +++ b/app/src-tauri/src/file_viewer/poll.rs @@ -51,8 +51,10 @@ pub async fn poll_file( "poll".to_string(), container_path.to_string(), ]; + // A background poll: an open viewer must not keep an idle AWS workspace + // up. Opening and saving a file are the user's own actions and count. let (stdout, stderr, code) = container - .exec_oneshot_streams_as("claude", cmd, Vec::new()) + .exec_oneshot_streams_background("claude", cmd) .await?; if code != 0 && code != 4 { return Err(format!( diff --git a/app/src-tauri/src/host/agent.rs b/app/src-tauri/src/host/agent.rs index 011838e..ad5f4a5 100644 --- a/app/src-tauri/src/host/agent.rs +++ b/app/src-tauri/src/host/agent.rs @@ -400,6 +400,17 @@ pub async fn exec_oneshot_streams_as( Ok((out.stdout(), out.stderr(), out.exit_code)) } +/// [`exec_oneshot_streams_as`] marked as a background poll (the file +/// viewer's change check), so it never keeps an idle workspace up. +pub async fn exec_oneshot_streams_background( + ep: &AgentEndpoint, + user: &str, + cmd: Vec, +) -> Result<(String, String, i64), String> { + let out = exec_oneshot_raw_marked(ep, user, cmd, Vec::new(), MAX_ONESHOT_OUTPUT, true).await?; + Ok((out.stdout(), out.stderr(), out.exit_code)) +} + // ── detached execs, status, resize ─────────────────────────────────────────── #[derive(Deserialize)] diff --git a/app/src-tauri/src/host/agent/tests.rs b/app/src-tauri/src/host/agent/tests.rs index c0ce2f0..a72517d 100644 --- a/app/src-tauri/src/host/agent/tests.rs +++ b/app/src-tauri/src/host/agent/tests.rs @@ -323,6 +323,22 @@ async fn only_a_background_poll_carries_the_background_marker() { assert_eq!(seen.exec_bodies[0], seen.exec_bodies[1], "the request is otherwise the same"); } +#[tokio::test] +async fn the_file_viewers_poll_is_a_background_request_and_its_open_and_save_are_not() { + let (ep, fake) = fake_agent().await; + let t = target(&ep); + // The 2 s change check, as the viewer runs it. + let _ = crate::file_viewer::poll::poll_file(&t, "/workspace/x").await; + t.exec_oneshot_streams_background("claude", cmd("sh")).await.unwrap(); + // What opening or saving goes through: user actions, which count. + t.exec_oneshot_streams_as("claude", cmd("sh"), Vec::new()).await.unwrap(); + let seen = fake.seen.lock().unwrap(); + assert_eq!(seen.exec_background, [Some("1".to_string()), Some("1".to_string()), None]); + let src = |s: &str| s.contains("exec_oneshot_streams_as("); + assert!(!src(include_str!("../../file_viewer/poll.rs")), "the poll stays a background request"); + assert!(src(include_str!("../../file_viewer/resolve.rs")) && src(include_str!("../../file_viewer/write.rs"))); +} + // ── one-shot execs ─────────────────────────────────────────────────────────── #[tokio::test] diff --git a/app/src-tauri/src/host/mod.rs b/app/src-tauri/src/host/mod.rs index f4a3522..6882030 100644 --- a/app/src-tauri/src/host/mod.rs +++ b/app/src-tauri/src/host/mod.rs @@ -302,6 +302,22 @@ impl ContainerTarget { } } + /// [`Self::exec_oneshot_streams_as`] for a background poll (the file + /// viewer's change check): on the agent it carries the background marker, + /// so it never counts as activity for idle stop. The same call on Docker. + pub async fn exec_oneshot_streams_background( + &self, + user: &str, + cmd: Vec, + ) -> Result<(String, String, i64), String> { + match self { + ContainerTarget::Docker { container_id } => { + docker::exec::exec_oneshot_streams_as(container_id, user, cmd, Vec::new()).await + } + ContainerTarget::Agent(ep) => agent::exec_oneshot_streams_background(ep, user, cmd).await, + } + } + /// Start a command and return without waiting or reading its output. /// Returns the exec id. pub async fn exec_detached(&self, spec: ExecSpec) -> Result {