diff options
| author | JJ Liebig <jonathan.liebig@gmail.com> | 2026-09-24 02:23:42 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-24 02:23:42 +0200 |
| commit | 9c96f7ddb3be2cc575a159d4d1f1d49fb10d7006 (patch) | |
| tree | 182e89183981160397af301c075ab11316b15850 /src/pane.rs | |
| parent | 21fd121a63db53ff44b947bbe38c47bbf35e632c (diff) | |
fix: avoid inferring codex idle from terminal output (#4563)
refs #4507
Diffstat (limited to 'src/pane.rs')
| -rw-r--r-- | src/pane.rs | 211 |
1 files changed, 180 insertions, 31 deletions
diff --git a/src/pane.rs b/src/pane.rs index 9bc35eca..bfeec638 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -34,7 +34,7 @@ mod terminal; mod xtgettcap; use self::agent_detection::{ - decide_detection_screen_read, decide_screen_detection_publish, + codex_prompt_ready, decide_detection_screen_read, decide_screen_detection_publish, detection_update_for_publish_with_osc, mark_detection_content_changed, observe_detection_content_change, DetectionPublishDecision, DetectionScreenReadDecision, DetectionScreenReadInput, PendingIdleConfirmation, ScreenDetectionPublishInput, @@ -290,6 +290,31 @@ async fn publish_agent_process_detected_event( } } +async fn publish_codex_prompt_observation( + state_events: &mpsc::Sender<AppEvent>, + pane_id: PaneId, + agent: Option<Agent>, + content: &str, + detection: Option<&crate::detect::AgentDetection>, + process_exited: bool, + last_ready: &mut bool, +) { + let ready = agent == Some(Agent::Codex) + && !process_exited + && detection.is_some_and(|detection| detection.state == AgentState::Unknown) + && codex_prompt_ready(content); + if ready == *last_ready { + return; + } + *last_ready = ready; + if let Err(err) = state_events + .send(AppEvent::CodexPromptObserved { pane_id, ready }) + .await + { + warn!(pane = pane_id.raw(), %err, "failed to deliver Codex prompt observation"); + } +} + #[derive(Debug, Clone, Copy)] struct AgentDetectionPublishUpdate { state: AgentState, @@ -771,6 +796,7 @@ fn spawn_basic_detection_task( let mut last_screen_scan_detection_content_seq = None; let mut agent_startup_grace_until = None; let mut pending_idle = PendingIdleConfirmation::default(); + let mut last_codex_prompt_ready = false; loop { let sleep_duration = if pending_idle.active() { @@ -781,6 +807,10 @@ fn spawn_basic_detection_task( tokio::select! { _ = tokio::time::sleep(sleep_duration) => {} _ = detect_reset.notified() => { + publish_codex_prompt_observation( + &state_events, pane_id, Some(Agent::Codex), "", None, false, + &mut last_codex_prompt_ready, + ).await; agent_presence = AgentDetectionPresence::from_agent(None); state = AgentState::Unknown; last_visible_idle = false; @@ -888,6 +918,7 @@ fn spawn_basic_detection_task( == ForegroundShellAgentAction::ReportReplacementProcess; if agent_changed { pending_idle.clear(); + last_codex_prompt_ready = false; last_screen_scan_detection_content_seq = None; // A replacement agent must not inherit OSC evidence // from the previous process; a first acquisition keeps @@ -961,10 +992,29 @@ fn spawn_basic_detection_task( last_screen_scan_detection_content_seq = current_detection_content_seq; let content_changed = content != last_detection_text; last_detection_text.clone_from(&content); - if !process_exited && crate::detect::should_skip_state_update(agent, &content) { + let osc_title = terminal.agent_osc_title(); + let osc_progress = terminal.agent_osc_progress(); + let screen_detection = detection_update_for_publish_with_osc( + agent, + &content, + &osc_title, + &osc_progress, + process_exited, + ); + publish_codex_prompt_observation( + &state_events, + pane_id, + agent, + &content, + screen_detection.as_ref(), + process_exited, + &mut last_codex_prompt_ready, + ) + .await; + let Some(screen_detection) = screen_detection else { pending_idle.clear(); continue; - } + }; sync_content_change_acquisition( agent_presence.current_agent(), suppressed_agent, @@ -974,19 +1024,6 @@ fn spawn_basic_detection_task( &mut acquisition_started_at, &mut last_content_change_at, ); - - let osc_title = terminal.agent_osc_title(); - let osc_progress = terminal.agent_osc_progress(); - let Some(screen_detection) = detection_update_for_publish_with_osc( - agent, - &content, - &osc_title, - &osc_progress, - process_exited, - ) else { - pending_idle.clear(); - continue; - }; match decide_screen_detection_publish( ScreenDetectionPublishInput { screen_detection, @@ -2660,6 +2697,7 @@ impl PaneRuntime { let mut last_screen_scan_detection_content_seq = None; let mut agent_startup_grace_until = None; let mut pending_idle = PendingIdleConfirmation::default(); + let mut last_codex_prompt_ready = false; tokio::time::sleep(Duration::from_millis(50)).await; @@ -2680,6 +2718,10 @@ impl PaneRuntime { tokio::select! { _ = tokio::time::sleep(tick) => {} _ = detect_reset.notified() => { + publish_codex_prompt_observation( + &state_events, pane_id, Some(Agent::Codex), "", None, false, + &mut last_codex_prompt_ready, + ).await; agent_presence = AgentDetectionPresence::from_agent(None); state = AgentState::Unknown; last_visible_idle = false; @@ -2823,6 +2865,7 @@ impl PaneRuntime { == ForegroundShellAgentAction::ReportReplacementProcess { pending_idle.clear(); + last_codex_prompt_ready = false; last_screen_scan_detection_content_seq = None; // A replacement agent must not inherit OSC // evidence from the previous process; a first @@ -2930,10 +2973,29 @@ impl PaneRuntime { last_screen_scan_detection_content_seq = current_detection_content_seq; let content_changed = content != last_detection_text; last_detection_text.clone_from(&content); - if detect::should_skip_state_update(agent, &content) { + let osc_title = terminal.agent_osc_title(); + let osc_progress = terminal.agent_osc_progress(); + let screen_detection = detection_update_for_publish_with_osc( + agent, + &content, + &osc_title, + &osc_progress, + process_exited, + ); + publish_codex_prompt_observation( + &state_events, + pane_id, + agent, + &content, + screen_detection.as_ref(), + process_exited, + &mut last_codex_prompt_ready, + ) + .await; + let Some(screen_detection) = screen_detection else { pending_idle.clear(); continue; - } + }; sync_content_change_acquisition( agent_presence.current_agent(), suppressed_agent, @@ -2943,19 +3005,6 @@ impl PaneRuntime { &mut acquisition_started_at, &mut last_content_change_at, ); - - let osc_title = terminal.agent_osc_title(); - let osc_progress = terminal.agent_osc_progress(); - let Some(screen_detection) = detection_update_for_publish_with_osc( - agent, - &content, - &osc_title, - &osc_progress, - process_exited, - ) else { - pending_idle.clear(); - continue; - }; match decide_screen_detection_publish( ScreenDetectionPublishInput { screen_detection, @@ -5804,4 +5853,104 @@ mod tests { } if delivered_pane == pane_id )); } + + #[tokio::test] + async fn codex_prompt_observation_revokes_on_working_or_skipped_screen() { + let (tx, mut rx) = mpsc::channel(4); + let pane_id = PaneId::from_raw(42); + let detection = crate::detect::AgentDetection { + state: AgentState::Unknown, + skip_state_update: false, + visible_idle: false, + visible_blocker: false, + visible_working: false, + }; + let mut last_ready = false; + let prompt = "› Ask Codex to do anything"; + publish_codex_prompt_observation( + &tx, + pane_id, + Some(Agent::Codex), + prompt, + Some(&detection), + false, + &mut last_ready, + ) + .await; + assert!(matches!( + rx.recv().await, + Some(AppEvent::CodexPromptObserved { ready: true, .. }) + )); + publish_codex_prompt_observation( + &tx, + pane_id, + Some(Agent::Codex), + prompt, + Some(&detection), + false, + &mut last_ready, + ) + .await; + assert!(rx.try_recv().is_err()); + let working = crate::detect::AgentDetection { + state: AgentState::Working, + ..detection + }; + publish_codex_prompt_observation( + &tx, + pane_id, + Some(Agent::Codex), + prompt, + Some(&working), + false, + &mut last_ready, + ) + .await; + assert!(matches!( + rx.recv().await, + Some(AppEvent::CodexPromptObserved { ready: false, .. }) + )); + publish_codex_prompt_observation( + &tx, + pane_id, + Some(Agent::Codex), + prompt, + Some(&detection), + false, + &mut last_ready, + ) + .await; + assert!(matches!( + rx.recv().await, + Some(AppEvent::CodexPromptObserved { ready: true, .. }) + )); + publish_codex_prompt_observation( + &tx, + pane_id, + Some(Agent::Codex), + prompt, + None, + false, + &mut last_ready, + ) + .await; + assert!(matches!( + rx.recv().await, + Some(AppEvent::CodexPromptObserved { ready: false, .. }) + )); + publish_codex_prompt_observation( + &tx, + pane_id, + Some(Agent::Codex), + prompt, + Some(&detection), + false, + &mut last_ready, + ) + .await; + assert!(matches!( + rx.recv().await, + Some(AppEvent::CodexPromptObserved { ready: true, .. }) + )); + } } |
