aboutsummaryrefslogtreecommitdiff
path: root/src/pane.rs
diff options
context:
space:
mode:
authorJJ Liebig <jonathan.liebig@gmail.com>2026-09-24 02:23:42 +0200
committerGitHub <noreply@github.com>2026-09-24 02:23:42 +0200
commit9c96f7ddb3be2cc575a159d4d1f1d49fb10d7006 (patch)
tree182e89183981160397af301c075ab11316b15850 /src/pane.rs
parent21fd121a63db53ff44b947bbe38c47bbf35e632c (diff)
fix: avoid inferring codex idle from terminal output (#4563)
refs #4507
Diffstat (limited to 'src/pane.rs')
-rw-r--r--src/pane.rs211
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, .. })
+ ));
+ }
}