From 74db2f3a79e03d752802a9c6a2ee7b55b0ca088d Mon Sep 17 00:00:00 2001 From: akbash Date: Sat, 18 Jul 2026 23:29:04 +0300 Subject: [PATCH] fix: detect codex working with static titles (#1574) * fix: detect codex working with static titles refs #1563 * fix: ignore interrupted codex footer refs #1563 --------- Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com> --- docs/next/CHANGELOG.md | 1 + src/detect/manifest/tests.rs | 120 +++++++++++++++++++++++++++++ src/detect/manifests/codex.toml | 13 +++- website/agent-detection/codex.toml | 13 +++- 4 files changed, 143 insertions(+), 4 deletions(-) diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 65bb683f..cbe0500d 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixed - `herdr config check` now reports unknown config keys with their full paths instead of treating ignored typos as valid configuration. (#1573) +- Codex panes with customized static terminal titles now fall back to the live working footer instead of remaining idle, while OSC activity remains preferred. (#1563) - Named agent prompts now honor live bracketed-paste mode before sending Enter, preserving OpenCode text such as `A != B` instead of triggering shell mode. (#1525) - New panes, tabs, layouts, and workspaces using `new_cwd = "follow"` now inherit the foreground process-group leader's working directory instead of an unrelated helper process directory. (#1472) - Noninteractive update, plugin, integration, sound, custom-command, and Git subprocesses no longer flash console windows on Windows. (#1468) diff --git a/src/detect/manifest/tests.rs b/src/detect/manifest/tests.rs index ec1873f7..11c92f49 100644 --- a/src/detect/manifest/tests.rs +++ b/src/detect/manifest/tests.rs @@ -747,6 +747,126 @@ fn codex_background_terminal_screen_does_not_override_osc_idle() { assert!(result.visible_idle); } +#[test] +fn codex_screen_working_fallback_handles_static_osc_title() { + let screen = "• I’ll run it and wait for completion.\n\n\ + ◦ Working (1m 16s • esc to interrupt) · 1 background…\n\n\ + › Use /skills to list available skills\n\n\ + gpt-5.6-sol default · /work\n"; + let result = osc_explain(Agent::Codex, screen, "project", ""); + + assert_eq!(result.state, AgentState::Working); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("screen_working_fallback") + ); + assert!(result.visible_working); +} + +#[test] +fn codex_osc_working_remains_preferred_over_screen_fallback() { + let screen = "• Working (4s • esc to interrupt)\n\n\ + › Use /skills to list available skills\n\n\ + gpt-5.6-sol default · /work\n"; + let result = osc_explain(Agent::Codex, screen, "⠸ project", ""); + + assert_eq!(result.state, AgentState::Working); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_working") + ); + assert!(result.visible_working); +} + +#[test] +fn codex_screen_blocker_outranks_working_fallback() { + let screen = "• Working (4s • esc to interrupt)\n\ + › 1. Yes, proceed\n\ + Press enter to confirm or esc to cancel\n"; + let result = osc_explain(Agent::Codex, screen, "project", ""); + + assert_eq!(result.state, AgentState::Blocked); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("live_strong_blocker") + ); + assert!(result.visible_blocker); + assert!(!result.visible_working); +} + +#[test] +fn codex_weak_blocker_outranks_working_fallback() { + let screen = "• Working (4s • esc to interrupt)\n\ + do you want to continue? [y/n]\n\ + › Use /skills to list available skills\n"; + let result = osc_explain(Agent::Codex, screen, "project", ""); + + assert_eq!(result.state, AgentState::Blocked); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("weak_blocker") + ); + assert!(!result.visible_working); +} + +#[test] +fn codex_transcript_viewer_outranks_working_fallback() { + let screen = "• Working (4s • esc to interrupt)\n\ + › transcript\n\ + ↑/↓ to scroll · pgup/pgdn to move · home/end to jump · q to quit · esc to edit prev\n"; + let result = osc_explain(Agent::Codex, screen, "project", ""); + + assert_eq!(result.state, AgentState::Unknown); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("transcript_viewer") + ); + assert!(result.skip_state_update); + assert!(!result.visible_working); +} + +#[test] +fn codex_screen_working_fallback_ignores_stale_and_prompt_text() { + let screens = [ + "◦ Working (1m 16s • esc to interrupt)\n\ + ■ Conversation interrupted\n\ + › Use /skills to list available skills\n\ + gpt-5.6-sol default · /work\n", + "› Explain the text ◦ Working (1m 16s • esc to interrupt)\n\ + gpt-5.6-sol default · /work\n", + " ◦ Working (1m 16s • esc to interrupt)\n\ + › Use /skills to list available skills\n\ + gpt-5.6-sol default · /work\n", + ]; + + for screen in screens { + let result = osc_explain(Agent::Codex, screen, "project", ""); + assert_eq!(result.state, AgentState::Idle); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_idle") + ); + assert!(result.visible_idle); + assert!(!result.visible_working); + } +} + +#[test] +fn codex_screen_working_fallback_ignores_interrupted_short_terminal() { + let screen = "◦ Working (1m 16s • esc to interrupt)\n\ + ■ Conversation interrupted\n\ + ›\n"; + let result = osc_explain(Agent::Codex, screen, "project", ""); + + assert_eq!(result.state, AgentState::Idle); + assert_eq!( + result.matched_rule.as_ref().map(|r| r.id.as_str()), + Some("osc_title_idle") + ); + assert!(result.visible_idle); + assert!(!result.visible_working); +} + #[test] fn codex_osc_working_beats_weak_blocker_screen() { // A stale [y/n] on screen triggers weak_blocker at priority 600, but an diff --git a/src/detect/manifests/codex.toml b/src/detect/manifests/codex.toml index e6851c86..a4a4ca9f 100644 --- a/src/detect/manifests/codex.toml +++ b/src/detect/manifests/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.07.13.1" +version = "2026.07.18.1" min_engine_version = 2 -updated_at = "2026-07-13T00:00:00Z" +updated_at = "2026-07-18T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -56,6 +56,15 @@ any = [ { contains = ["would you like to"], any = [{ contains = ["yes"] }, { contains = ["❯"] }] }, ] +[[rules]] +id = "screen_working_fallback" +state = "working" +priority = 500 +region = "bottom_non_empty_lines(3)" +visible_working = true +line_regex = ['^[•◦]\s+Working \([^)]*esc to interrupt\)(?: · .*)?$'] +not = [{ contains = ["■ Conversation interrupted"] }] + [[rules]] id = "osc_title_idle" state = "idle" diff --git a/website/agent-detection/codex.toml b/website/agent-detection/codex.toml index e6851c86..a4a4ca9f 100644 --- a/website/agent-detection/codex.toml +++ b/website/agent-detection/codex.toml @@ -1,7 +1,7 @@ id = "codex" -version = "2026.07.13.1" +version = "2026.07.18.1" min_engine_version = 2 -updated_at = "2026-07-13T00:00:00Z" +updated_at = "2026-07-18T00:00:00Z" [[rules]] id = "osc_title_blocked" @@ -56,6 +56,15 @@ any = [ { contains = ["would you like to"], any = [{ contains = ["yes"] }, { contains = ["❯"] }] }, ] +[[rules]] +id = "screen_working_fallback" +state = "working" +priority = 500 +region = "bottom_non_empty_lines(3)" +visible_working = true +line_regex = ['^[•◦]\s+Working \([^)]*esc to interrupt\)(?: · .*)?$'] +not = [{ contains = ["■ Conversation interrupted"] }] + [[rules]] id = "osc_title_idle" state = "idle"