From 7ae4b056a0ca478e584fa282c45b528134cc80c9 Mon Sep 17 00:00:00 2001 From: JJ Liebig Date: Tue, 11 Aug 2026 02:24:02 +0200 Subject: fix: wait for agent prompt readiness (#2537) * fix: wait for agent prompt readiness refs #2410 * fix: return pane updates after tab bar command refs #2410 --------- Co-authored-by: Can Celik --- src/server/headless.rs | 85 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 7 deletions(-) (limited to 'src/server') diff --git a/src/server/headless.rs b/src/server/headless.rs index 3425225c..ff6f367d 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -1963,7 +1963,7 @@ impl HeadlessServer { let suppress_active_tab_notifications = self.active_tab_suppresses_notifications(is_active_tab); - if self.app.state.sound.allows(update.known_agent) { + if !update.suppress_completion && self.app.state.sound.allows(update.known_agent) { if let Some(sound) = crate::app::actions::notification_sound_for_state_change_with_agent_labels( suppress_active_tab_notifications, @@ -2325,7 +2325,11 @@ impl HeadlessServer { // Headless mode disables local sound playback separately from the // sound policy so reloads can keep server-side notification policy live. self.sync_foreground_client_state(); - self.app.handle_internal_event(ev); + let suppress_completion = self + .app + .handle_internal_event_with_pane_updates(ev) + .iter() + .any(|update| update.pane_id == pane_id_val && update.suppress_completion); // Forward sound notification to clients when server-side sound policy allows it. let is_active_tab = self @@ -2344,7 +2348,8 @@ impl HeadlessServer { let next_state = self.pane_effective_state(pane_id_val); let next_agent_label = self.pane_effective_agent_label(pane_id_val); - if self.app.state.toast_config.delay_seconds == 0 + if !suppress_completion + && self.app.state.toast_config.delay_seconds == 0 && self.app.state.sound.allows(agent_val) { if let Some(sound) = @@ -2364,7 +2369,8 @@ impl HeadlessServer { } } - let toast_msg = if self.app.state.toast_config.delay_seconds == 0 + let toast_msg = if !suppress_completion + && self.app.state.toast_config.delay_seconds == 0 && should_forward_toast_to_clients(self.app.state.toast_config.delivery) { if self.app.state.toast.is_some() && self.app.state.toast != toast_before { @@ -2416,7 +2422,11 @@ impl HeadlessServer { let prev_agent_label = self.pane_effective_agent_label(pane_id_val); self.sync_foreground_client_state(); - self.app.handle_internal_event(ev); + let suppress_completion = self + .app + .handle_internal_event_with_pane_updates(ev) + .iter() + .any(|update| update.pane_id == pane_id_val && update.suppress_completion); // Forward sound notification based on the effective transition when // server-side sound policy allows it. @@ -2436,7 +2446,8 @@ impl HeadlessServer { let next_state = self.pane_effective_state(pane_id_val); let next_agent_label = self.pane_effective_agent_label(pane_id_val); - if self.app.state.toast_config.delay_seconds == 0 + if !suppress_completion + && self.app.state.toast_config.delay_seconds == 0 && self.app.state.sound.allows(agent_val) { if let Some(sound) = @@ -2456,7 +2467,8 @@ impl HeadlessServer { } } - let toast_msg = if self.app.state.toast_config.delay_seconds == 0 + let toast_msg = if !suppress_completion + && self.app.state.toast_config.delay_seconds == 0 && should_forward_toast_to_clients(self.app.state.toast_config.delivery) { if self.app.state.toast.is_some() && self.app.state.toast != toast_before { @@ -11127,6 +11139,65 @@ next_tab = "" } } + #[test] + fn startup_idle_does_not_forward_completion() { + let mut server = test_headless_server(); + let workspace = crate::workspace::Workspace::test_new("active"); + let pane_id = workspace.tabs[0].root_pane; + server.app.state.workspaces = vec![workspace]; + server.app.state.ensure_test_terminals(); + server.app.state.active = Some(0); + server.app.state.toast_config.delivery = crate::config::ToastDelivery::System; + server.app.state.toast_config.delay_seconds = 0; + server.app.state.sound.enabled = true; + + assert!( + server.handle_internal_event_with_forwarding(AppEvent::AgentProcessDetected { + pane_id, + agent: crate::detect::Agent::Pi, + observed_at: Instant::now(), + }) + ); + + let (client_tx, client_control_rx, _client_rx) = test_client_writer(); + server.clients.insert( + 1, + ClientConnection::new( + (80, 24), + crate::kitty_graphics::HostCellSize::default(), + crate::terminal_theme::TerminalTheme::default(), + Some(false), + 1, + RenderEncoding::SemanticFrame, + Some(client_tx), + ), + ); + server.foreground_client_id = Some(1); + server.sync_foreground_client_state(); + while client_control_rx + .recv_timeout(Duration::from_millis(20)) + .is_ok() + {} + + assert!( + server.handle_internal_event_with_forwarding(AppEvent::StateChanged { + pane_id, + agent: Some(crate::detect::Agent::Pi), + state: crate::detect::AgentState::Idle, + visible_blocker: false, + visible_working: false, + process_exited: false, + observed_at: Instant::now(), + }) + ); + assert!( + client_control_rx + .recv_timeout(Duration::from_millis(50)) + .is_err(), + "startup readiness should not forward a completion notification" + ); + } + #[test] fn delayed_agent_notification_forwards_after_deadline() { let mut server = test_headless_server(); -- cgit v1.2.3