Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions src/cli_agent_orchestrator/providers/codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,13 @@
# which is shared across v0.111 and v0.136 status bars.
TUI_FOOTER_PATTERN = r"(?:\?\s+for shortcuts|context left|\d+%\s+left|·\s+[~/])"
# Codex TUI progress spinner: "• Working (0s • esc to interrupt)",
# "• Thinking (2s ...)", "• Starting script creation (10s • esc to interrupt)".
# The prefix text varies but the "(Ns • esc to interrupt)" format is consistent.
# "• Working (1m 00s ...)", "• Working (1h 00m 00s ...)", or dynamic
# prefixes such as "• Starting script creation (10s • esc to interrupt)".
# Codex expands the elapsed value at the minute and hour boundaries.
# Appears inline with --no-alt-screen when the agent is actively processing.
# Must be checked before COMPLETED to avoid false positives (the • matches
# ASSISTANT_PREFIX_PATTERN and the TUI footer › matches idle prompt).
TUI_PROGRESS_PATTERN = r"•.*\(\d+s\s*•\s*esc to interrupt\)"
TUI_PROGRESS_PATTERN = r"•[^\n]*\((?:(?:\d+h\s+)?\d+m\s+)?\d+s\s*•\s*esc to interrupt\)"

# Workspace trust/approval prompt shown when Codex opens a new directory.
# Two known variants:
Expand Down Expand Up @@ -333,6 +334,13 @@ class ProviderError(Exception):
class CodexProvider(BaseProvider):
"""Provider for Codex CLI tool integration."""

# Codex redraws its inline TUI in place. The append-only pipe-pane stream
# therefore retains transient progress frames (notably MCP startup) after
# they have been erased from the visible terminal. Route status detection
# through StatusMonitor's pyte-composited viewport so get_status() sees only
# the live frame rather than stale redraw history.
supports_screen_detection = True

def __init__(
self,
terminal_id: str,
Expand Down Expand Up @@ -893,6 +901,20 @@ def get_status(self, output: str) -> TerminalStatus:
# assume the CLI is still producing output.
return TerminalStatus.PROCESSING

def get_status_from_screen(self, screen_lines: list[str]) -> TerminalStatus:
"""Detect status from the current pyte-composited Codex viewport.

Codex's existing detector is line-oriented and already understands its
trust/update dialogs, progress spinner, idle composer, and completed
response markers. Remove pyte's blank padding rows and reuse that
detector against the rendered screen; cursor-erased startup frames are
absent here, which prevents a stale spinner from pinning PROCESSING.
"""
rows = [line.rstrip() for line in screen_lines if line.strip()]
if not rows:
return TerminalStatus.UNKNOWN
return self.get_status("\n".join(rows))
Comment thread
TiantianFlow marked this conversation as resolved.

def extract_last_message_from_script(self, script_output: str) -> str:
"""Extract Codex's final response from terminal output.

Expand Down
80 changes: 80 additions & 0 deletions test/providers/test_codex_provider_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,86 @@ def test_get_status_completed_tui_with_status_bar(self):
assert status == TerminalStatus.COMPLETED


class TestCodexRenderedScreenStatusDetection:
"""Regression coverage for in-place Codex TUI redraws.

``tmux pipe-pane`` is append-only: text erased from the visible terminal
remains in CAO's raw rolling buffer. MCP startup uses the same spinner
shape as a live agent turn, so raw parsing can remain PROCESSING forever
after the visible screen has returned to the idle composer.
"""

def test_provider_opts_into_rendered_screen_detection(self):
provider = CodexProvider("test1234", "test-session", "window-0")

assert provider.supports_screen_detection is True

def test_blank_rendered_screen_is_unknown(self):
provider = CodexProvider("test1234", "test-session", "window-0")

assert provider.get_status_from_screen(["", " "]) == TerminalStatus.UNKNOWN

def test_overwritten_mcp_startup_spinner_does_not_pin_processing(self):
import pyte

raw = (
"\x1b[1;1H• Starting MCP servers (0/3): cao-mcp-server"
" (0s • esc to interrupt)"
"\x1b[3;1H› Improve documentation in @filename"
"\x1b[5;1H gpt-5.6-terra high · /tmp/project"
# Codex clears the transient activity row once MCP startup settles.
"\x1b[1;1H\x1b[2K"
)
screen = pyte.Screen(200, 20)
pyte.Stream(screen).feed(raw)
provider = CodexProvider("test1234", "test-session", "window-0")

# Demonstrate the old failure mode: stripping cursor controls from the
# append-only stream leaves the erased spinner behind.
assert provider.get_status(raw) == TerminalStatus.PROCESSING
# The composited viewport contains only the live idle composer.
assert provider.get_status_from_screen(list(screen.display)) == TerminalStatus.IDLE

def test_live_mcp_startup_spinner_is_processing(self):
screen_lines = [
"• Starting MCP servers (1/3): cao-mcp-server (0s • esc to interrupt)",
"",
"› Improve documentation in @filename",
"",
" gpt-5.6-terra high · /tmp/project",
]
provider = CodexProvider("test1234", "test-session", "window-0")

assert provider.get_status_from_screen(screen_lines) == TerminalStatus.PROCESSING

@pytest.mark.parametrize("elapsed", ["1m 00s", "1h 00m 00s"])
def test_minute_plus_live_progress_is_processing(self, elapsed):
screen_lines = [
"› Implement the requested feature",
f"• Working ({elapsed} • esc to interrupt)",
"",
"› Improve documentation in @filename",
"",
" gpt-5.6-terra high · /tmp/project",
]
provider = CodexProvider("test1234", "test-session", "window-0")

assert provider.get_status_from_screen(screen_lines) == TerminalStatus.PROCESSING

def test_completed_turn_on_rendered_screen_is_completed(self):
screen_lines = [
"› Reply with the readiness token",
"• CAO_CODEX_READY",
"",
"› Improve documentation in @filename",
"",
" gpt-5.6-terra high · /tmp/project",
]
provider = CodexProvider("test1234", "test-session", "window-0")

assert provider.get_status_from_screen(screen_lines) == TerminalStatus.COMPLETED


class TestCodexBulletFormatStatusDetection:
"""Tests for Codex's real interactive output format using › prompt and • bullets."""

Expand Down
Loading