From dd89efe2bcfc2bc18a2cd2f27db04fe8ab250408 Mon Sep 17 00:00:00 2001 From: kewton Date: Sat, 4 Apr 2026 09:53:00 +0900 Subject: [PATCH] fix(619): detect Codex /model selection list as waiting status Add CODEX_SELECTION_LIST_PATTERN to detect "press enter to confirm" footer in Codex CLI's arrow-key selection prompts (e.g., /model Step 1). This enables NavigationButtons display on mobile for model selection. - Add CODEX_SELECTION_LIST_PATTERN in cli-patterns.ts - Add CODEX_SELECTION_LIST to STATUS_REASON and SELECTION_LIST_REASONS - Insert selection list detection in status-detector.ts section 2.7 (after thinking, before idle prompt) - Add 6 test cases for Codex selection list detection Resolves #619 Co-Authored-By: Claude Sonnet 4.6 --- src/lib/detection/cli-patterns.ts | 14 +++ src/lib/detection/status-detector.ts | 23 ++++- tests/unit/status-detector-selection.test.ts | 101 ++++++++++++++++++- 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/src/lib/detection/cli-patterns.ts b/src/lib/detection/cli-patterns.ts index 41a84976..6ab850a0 100644 --- a/src/lib/detection/cli-patterns.ts +++ b/src/lib/detection/cli-patterns.ts @@ -83,6 +83,20 @@ export const CODEX_PROMPT_PATTERN = /^›\s*/m; */ export const CODEX_SEPARATOR_PATTERN = /^─.*Worked for.*─+$/m; +/** + * Codex CLI selection list footer pattern (Issue #619) + * Detects Codex CLI's interactive selection prompts that use arrow key + * navigation (e.g., /model command's model selection step). + * + * Matches: "press enter to confirm or esc to cancel" + * Does NOT match: "press number to confirm" (handled by detectMultipleChoicePrompt) + * + * The distinction is important: "press enter to confirm" indicates an arrow-key + * selection list (NavigationButtons), while "press number to confirm" indicates + * a numbered prompt (PromptPanel with buttons). + */ +export const CODEX_SELECTION_LIST_PATTERN = /press\s+enter\s+to\s+confirm/i; + /** * Pasted text pattern * diff --git a/src/lib/detection/status-detector.ts b/src/lib/detection/status-detector.ts index 16ab7146..fbbd5368 100644 --- a/src/lib/detection/status-detector.ts +++ b/src/lib/detection/status-detector.ts @@ -24,7 +24,7 @@ * coupling via a minimal DTO/projection type. */ -import { stripAnsi, stripBoxDrawing, detectThinking, getCliToolPatterns, buildDetectPromptOptions, OPENCODE_RESPONSE_COMPLETE, OPENCODE_PROCESSING_INDICATOR, OPENCODE_SELECTION_LIST_PATTERN, CLAUDE_SELECTION_LIST_FOOTER, COPILOT_SELECTION_LIST_PATTERN, CODEX_PROMPT_PATTERN } from './cli-patterns'; +import { stripAnsi, stripBoxDrawing, detectThinking, getCliToolPatterns, buildDetectPromptOptions, OPENCODE_RESPONSE_COMPLETE, OPENCODE_PROCESSING_INDICATOR, OPENCODE_SELECTION_LIST_PATTERN, CLAUDE_SELECTION_LIST_FOOTER, COPILOT_SELECTION_LIST_PATTERN, CODEX_PROMPT_PATTERN, CODEX_SELECTION_LIST_PATTERN } from './cli-patterns'; import { detectPrompt } from './prompt-detector'; import type { PromptDetectionResult } from './prompt-detector'; import type { CLIToolType } from '@/lib/cli-tools/types'; @@ -103,6 +103,7 @@ export const STATUS_REASON = { OPENCODE_SELECTION_LIST: 'opencode_selection_list', CLAUDE_SELECTION_LIST: 'claude_selection_list', COPILOT_SELECTION_LIST: 'copilot_selection_list', + CODEX_SELECTION_LIST: 'codex_selection_list', OPENCODE_RESPONSE_COMPLETE: 'opencode_response_complete', INPUT_PROMPT: 'input_prompt', NO_RECENT_OUTPUT: 'no_recent_output', @@ -120,6 +121,7 @@ export const SELECTION_LIST_REASONS = new Set([ STATUS_REASON.OPENCODE_SELECTION_LIST, STATUS_REASON.CLAUDE_SELECTION_LIST, STATUS_REASON.COPILOT_SELECTION_LIST, + STATUS_REASON.CODEX_SELECTION_LIST, ]); /** @@ -409,6 +411,25 @@ export function detectSessionStatus( }; } + // A2. Check content area for selection list (Issue #619: Codex /model selection list) + // Codex /model Step 1 shows arrow-key selection list with + // "press enter to confirm or esc to cancel" footer. + // Must be checked AFTER thinking (A) but BEFORE idle prompt (B). + // "press number to confirm" (Step 2) is NOT matched — that's handled + // by detectMultipleChoicePrompt at priority 1. + const codexFullContentText = contentLines + .slice(0, lastContentIdx + 1) + .join('\n'); + if (CODEX_SELECTION_LIST_PATTERN.test(codexFullContentText)) { + return { + status: 'waiting', + confidence: 'high', + reason: STATUS_REASON.CODEX_SELECTION_LIST, + hasActivePrompt: false, + promptDetection, + }; + } + // B. Check if the last content line is the idle › prompt. // The last non-empty line above the status bar is the current active line. // When Codex is idle, this is the › prompt (with optional suggestion text). diff --git a/tests/unit/status-detector-selection.test.ts b/tests/unit/status-detector-selection.test.ts index 7b69db32..41237e20 100644 --- a/tests/unit/status-detector-selection.test.ts +++ b/tests/unit/status-detector-selection.test.ts @@ -156,14 +156,15 @@ describe('SELECTION_LIST_REASONS Set', () => { expect(SELECTION_LIST_REASONS).toBeInstanceOf(Set); }); - it('should contain all three selection list reasons', () => { + it('should contain all four selection list reasons', () => { expect(SELECTION_LIST_REASONS.has(STATUS_REASON.OPENCODE_SELECTION_LIST)).toBe(true); expect(SELECTION_LIST_REASONS.has(STATUS_REASON.CLAUDE_SELECTION_LIST)).toBe(true); expect(SELECTION_LIST_REASONS.has(STATUS_REASON.COPILOT_SELECTION_LIST)).toBe(true); + expect(SELECTION_LIST_REASONS.has(STATUS_REASON.CODEX_SELECTION_LIST)).toBe(true); }); - it('should have exactly 3 entries', () => { - expect(SELECTION_LIST_REASONS.size).toBe(3); + it('should have exactly 4 entries', () => { + expect(SELECTION_LIST_REASONS.size).toBe(4); }); it('should not contain unrelated reasons', () => { @@ -274,3 +275,97 @@ describe('detectSessionStatus - Copilot selection_list detection', () => { expect(result.reason).not.toBe(STATUS_REASON.COPILOT_SELECTION_LIST); }); }); + +// Helper: Build Codex TUI output with content area + status bar +// Codex TUI layout: content area (top) | empty padding | status bar (bottom) +function buildCodexOutput(contentLines: string[]): string { + const statusBar = ' o4-mini 50% left · /path/to/project'; + const padding = Array(10).fill(''); + return [...contentLines, ...padding, statusBar].join('\n'); +} + +describe('STATUS_REASON - CODEX_SELECTION_LIST', () => { + it('should export CODEX_SELECTION_LIST constant', () => { + expect(STATUS_REASON.CODEX_SELECTION_LIST).toBe('codex_selection_list'); + }); +}); + +describe('detectSessionStatus - Codex selection_list detection (Issue #619)', () => { + it('should detect Codex /model Step 1 selection list and return waiting status', () => { + const output = buildCodexOutput([ + 'Select a model', + '', + ' ❯ o4-mini (current)', + ' o3', + ' o3-pro', + ' codex-mini-latest', + '', + 'press enter to confirm or esc to cancel', + ]); + + const result = detectSessionStatus(output, 'codex'); + expect(result.status).toBe('waiting'); + expect(result.confidence).toBe('high'); + expect(result.reason).toBe(STATUS_REASON.CODEX_SELECTION_LIST); + expect(result.hasActivePrompt).toBe(false); + }); + + it('should prioritize thinking (A) over selection list', () => { + // If thinking indicator is present alongside "press enter to confirm", + // thinking should take priority (step A before selection list step) + const output = buildCodexOutput([ + 'Select a model', + ' ❯ o4-mini (current)', + 'press enter to confirm or esc to cancel', + '• Planning something', // thinking indicator in last lines + ]); + + const result = detectSessionStatus(output, 'codex'); + expect(result.status).toBe('running'); + expect(result.reason).toBe('thinking_indicator'); + }); + + it('should detect numbered prompt (Step 2) via priority 1 detectPrompt, not selection list', () => { + // Codex /model Step 2 uses "press number to confirm" (NOT "press enter to confirm") + // This is handled by detectMultipleChoicePrompt (priority 1) as a multiple_choice prompt + const output = buildCodexOutput([ + 'Reasoning level', + '', + ' 1. low', + ' 2. medium (default)', + ' 3. high', + '', + 'press number to confirm or esc to cancel', + ]); + + const result = detectSessionStatus(output, 'codex'); + // Should be detected as prompt_detected (priority 1), not codex_selection_list + expect(result.reason).not.toBe(STATUS_REASON.CODEX_SELECTION_LIST); + expect(result.status).toBe('waiting'); + expect(result.reason).toBe('prompt_detected'); + }); + + it('should NOT detect selection list for normal Codex response', () => { + const output = buildCodexOutput([ + 'Here is the implementation:', + '```typescript', + 'function hello() { return "world"; }', + '```', + '• Ran command: npm test', + ]); + + const result = detectSessionStatus(output, 'codex'); + expect(result.reason).not.toBe(STATUS_REASON.CODEX_SELECTION_LIST); + }); + + it('should NOT trigger codex_selection_list for non-codex tools', () => { + // Even if output contains "press enter to confirm", other tools should not match + const output = [ + 'press enter to confirm or esc to cancel', + '> ', + ].join('\n'); + + const result = detectSessionStatus(output, 'claude'); + expect(result.reason).not.toBe(STATUS_REASON.CODEX_SELECTION_LIST); + }); +});