aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-18 23:29:04 +0300
committerGitHub <noreply@github.com>2026-07-18 23:29:04 +0300
commit74db2f3a79e03d752802a9c6a2ee7b55b0ca088d (patch)
tree7fbab28bcc408c189076e145ef8e32a5f3201a7b
parentf11be93e0409f8159eb8c501416f4bd3516c4270 (diff)
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>
-rw-r--r--docs/next/CHANGELOG.md1
-rw-r--r--src/detect/manifest/tests.rs120
-rw-r--r--src/detect/manifests/codex.toml13
-rw-r--r--website/agent-detection/codex.toml13
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
@@ -748,6 +748,126 @@ fn codex_background_terminal_screen_does_not_override_osc_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
// active braille spinner in the OSC title is priority 1050 — OSC wins.
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"
@@ -57,6 +57,15 @@ any = [
]
[[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"
priority = 100
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"
@@ -57,6 +57,15 @@ any = [
]
[[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"
priority = 100