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
13 changes: 8 additions & 5 deletions src/lib/detection/cli-patterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,21 @@ export const CODEX_PROMPT_PATTERN = /^›\s*/m;
export const CODEX_SEPARATOR_PATTERN = /^─.*Worked for.*─+$/m;

/**
* Codex CLI selection list footer pattern (Issue #619)
* Codex CLI selection list footer pattern (Issue #619, #622)
* Detects Codex CLI's interactive selection prompts that use arrow key
* navigation (e.g., /model command's model selection step).
* navigation (e.g., /model command's model and reasoning level selection steps).
*
* Matches: "press enter to confirm or esc to cancel"
* Matches:
* - Step 1 (model selection): "Press enter to select reasoning effort, or esc to dismiss."
* - Step 2 (reasoning level): "Press enter to confirm or esc to go back"
* - Legacy: "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
* The distinction is important: "press enter to confirm/select" 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;
export const CODEX_SELECTION_LIST_PATTERN = /press\s+enter\s+to\s+(?:confirm|select)/i;

/**
* Pasted text pattern
Expand Down
41 changes: 23 additions & 18 deletions src/lib/detection/status-detector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,27 @@ export function detectSessionStatus(
};
}

// 0.8. Codex: selection list detection BEFORE prompt detection (Issue #622)
// CODEX_SELECTION_LIST_PATTERN matches "press enter to confirm/select" footer.
// Without this early check, detectPrompt() at priority 1 would detect the numbered
// options (e.g., "› 1. gpt-5.4") as a multiple_choice prompt, preventing
// NavigationButtons from being shown.
// This mirrors the Copilot Priority 0 pattern above.
if (cliToolId === 'codex') {
const codexFullContent = contentLines.join('\n');
if (CODEX_SELECTION_LIST_PATTERN.test(codexFullContent)) {
const codexPromptOptions = buildDetectPromptOptions(cliToolId);
const codexPromptDetection = detectPrompt(stripBoxDrawing(cleanOutput), codexPromptOptions);
return {
status: 'waiting',
confidence: 'high',
reason: STATUS_REASON.CODEX_SELECTION_LIST,
hasActivePrompt: false,
promptDetection: codexPromptDetection,
};
}
}

// 1. Interactive prompt detection (highest priority)
// This includes yes/no prompts, multiple choice, and approval prompts
const promptOptions = buildDetectPromptOptions(cliToolId);
Expand Down Expand Up @@ -411,24 +432,8 @@ 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,
};
}
// A2. (Removed — Codex selection list detection moved to priority 0.8,
// before detectPrompt, to prevent false multiple_choice detection. Issue #622)

// 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.
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/prompt-detector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2816,4 +2816,53 @@ Are you sure you want to continue? (yes/no)
expect(result.isPrompt).toBe(false);
});
});

// Issue #622: Codex /model Step 1 should not be detected as multiple_choice by prompt detector
describe('Issue #622: Codex /model Step 1 prompt detector behavior', () => {
it('should detect Codex /model Step 1 as multiple_choice by prompt detector (intercepted by status-detector)', () => {
// Note: The prompt detector itself MAY detect this as multiple_choice because
// it has numbered options with cursor indicator. The fix is at the status-detector
// level which intercepts BEFORE the prompt detector runs.
// This test documents the prompt detector's behavior for this input.
const output = [
'Select Model and Effort',
'',
'\u203A 1. gpt-5.4 (current) Latest frontier agentic coding model.',
' 2. gpt-5.4-mini Smaller frontier agentic coding model.',
' 3. o3 Advanced reasoning model.',
' 4. o4-mini Fast, affordable reasoning model.',
'',
'Press enter to select reasoning effort, or esc to dismiss.',
].join('\n');

const result = detectPrompt(output);
// The prompt detector may or may not detect this as a prompt.
// The important thing is that status-detector's early Codex selection list
// check runs BEFORE detectPrompt, so this path is never reached in practice.
// We just document the behavior here.
if (result.isPrompt) {
expect(result.promptData?.type).toBe('multiple_choice');
}
});

it('should detect Codex /model Step 2 reasoning level as multiple_choice', () => {
// Step 2 with "press enter to confirm" - also intercepted by status-detector
const output = [
'Select Reasoning Level for gpt-5.4',
'',
' 1. Low Fast responses with lighter reasoning',
'\u203A 2. Medium (default) (current) Balances speed and reasoning depth for everyday tasks',
' 3. High Greater reasoning depth for complex problems',
' 4. Extra high Extra high reasoning depth for complex problems',
'',
'Press enter to confirm or esc to go back',
].join('\n');

const result = detectPrompt(output);
// Similar to Step 1 - status-detector intercepts first
if (result.isPrompt) {
expect(result.promptData?.type).toBe('multiple_choice');
}
});
});
});
78 changes: 71 additions & 7 deletions tests/unit/status-detector-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,19 +310,20 @@ describe('detectSessionStatus - Codex selection_list detection (Issue #619)', ()
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)
it('should prioritize selection list over thinking when both present (Issue #622)', () => {
// When "press enter to confirm" footer and thinking indicator are both present,
// selection list detection (priority 0.8) runs first and takes precedence.
// This is correct because the footer definitively indicates a selection list UI.
const output = buildCodexOutput([
'Select a model',
' o4-mini (current)',
' \u276F o4-mini (current)',
'press enter to confirm or esc to cancel',
' Planning something', // thinking indicator in last lines
'\u2022 Planning something', // thinking indicator in last lines
]);

const result = detectSessionStatus(output, 'codex');
expect(result.status).toBe('running');
expect(result.reason).toBe('thinking_indicator');
expect(result.status).toBe('waiting');
expect(result.reason).toBe(STATUS_REASON.CODEX_SELECTION_LIST);
});

it('should detect numbered prompt (Step 2) via priority 1 detectPrompt, not selection list', () => {
Expand Down Expand Up @@ -369,3 +370,66 @@ describe('detectSessionStatus - Codex selection_list detection (Issue #619)', ()
expect(result.reason).not.toBe(STATUS_REASON.CODEX_SELECTION_LIST);
});
});

describe('detectSessionStatus - Codex /model Step 1 model selection (Issue #622)', () => {
it('should detect Codex /model Step 1 with "press enter to select" as codex_selection_list', () => {
// Step 1: Model selection uses "Press enter to select reasoning effort, or esc to dismiss."
// This must be detected as codex_selection_list, NOT as multiple_choice prompt.
const output = buildCodexOutput([
'Select Model and Effort',
'',
'\u203A 1. gpt-5.4 (current) Latest frontier agentic coding model.',
' 2. gpt-5.4-mini Smaller frontier agentic coding model.',
' 3. o3 Advanced reasoning model.',
' 4. o4-mini Fast, affordable reasoning model.',
'',
'Press enter to select reasoning effort, or esc to dismiss.',
]);

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 detect Codex /model Step 2 with "press enter to confirm" as codex_selection_list', () => {
// Step 2: Reasoning level selection uses "Press enter to confirm or esc to go back"
// This must still be detected as codex_selection_list (regression check).
const output = buildCodexOutput([
'Select Reasoning Level for gpt-5.4',
'',
' 1. Low Fast responses with lighter reasoning',
'\u203A 2. Medium (default) (current) Balances speed and reasoning depth for everyday tasks',
' 3. High Greater reasoning depth for complex problems',
' 4. Extra high Extra high reasoning depth for complex problems',
'',
'Press enter to confirm or esc to go back',
]);

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 NOT affect "press number to confirm" detection (Issue #616 regression)', () => {
// "press number to confirm" should still be detected as multiple_choice prompt,
// not codex_selection_list.
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');
expect(result.reason).not.toBe(STATUS_REASON.CODEX_SELECTION_LIST);
expect(result.status).toBe('waiting');
expect(result.reason).toBe('prompt_detected');
});
});
Loading