diff --git a/src/config/cli-registry/schema.ts b/src/config/cli-registry/schema.ts index 8bdcc281..056abab6 100644 --- a/src/config/cli-registry/schema.ts +++ b/src/config/cli-registry/schema.ts @@ -368,6 +368,22 @@ const capabilitiesSchema = z 'awaitingLine must be a regex compileVersionRegex() accepts: at most 200 characters, no nested quantifiers' ) .optional(), + // Same guard again: run over the foot of a pane capture every time a session + // settles and on a timer while a dialog is up, and ~/.codeman/clis.json can set it. + blockedLine: z + .string() + .min(1) + .refine( + (src) => compileVersionRegex(src) !== null, + 'blockedLine must be a regex compileVersionRegex() accepts: at most 200 characters, no nested quantifiers' + ) + .optional(), + // Wider than `watchingLines` because a dialog's options wrap at narrow widths (codex + // needs 20 at 36 columns). That is safe only for a pattern that ends on the dialog's + // own footer: the rows above it, which the agent may write, can then supply a + // marker but never the footer. Still bounded, since it is also how much text the + // pattern runs over. + blockedLines: z.number().int().min(1).max(24).optional(), }) .strict() // A window with nothing to search is a typo, not a configuration. Refused at LOAD @@ -378,6 +394,10 @@ const capabilitiesSchema = z (v) => v.watchingLines === undefined || v.watchingLine !== undefined, 'watchingLines has nothing to bound without a watchingLine' ) + .refine( + (v) => v.blockedLines === undefined || v.blockedLine !== undefined, + 'blockedLines has nothing to bound without a blockedLine' + ) .optional(), model: z .object({ source: z.enum(['flag', 'claude-settings-file', 'none']), param: z.string().optional() }) diff --git a/src/config/cli-registry/stock.ts b/src/config/cli-registry/stock.ts index 3075e36f..55933c7c 100644 --- a/src/config/cli-registry/stock.ts +++ b/src/config/cli-registry/stock.ts @@ -677,10 +677,11 @@ const CODEX: CliEntry = { // which the agent writes (and while a prompt is typed, the last two). Matching the complete row raises the bar — an assistant // message has to end with this exact line, to the character — but nothing here makes // forging it impossible, so do not read the Claude comment above as applying here. - // What contains it is that codex declares `hooks: 'none'`: no hook event from a codex - // session ever reaches `notePrompt()`, so there is no idle item to pre-acknowledge - // and a forged label costs a wrong badge and nothing else. A CLI that gains hook - // signals must not keep a pattern this soft. + // What contains it is that codex declares `hooks: 'none'`: no IDLE prompt from a codex + // session ever reaches `notePrompt()` (its screen-read dialogs below open PERMISSION + // items, which a watching label never acknowledges), so there is no idle item to + // pre-acknowledge and a forged label costs a wrong badge and nothing else. A CLI that + // gains idle signals must not keep a pattern this soft. // ⚠️ Background TERMINALS are the only background work codex advertises on screen. // A sub-agent started without waiting outlives the turn just as a terminal does — // measured 2026-09-22, the sandboxed process was still running — and the pane shows @@ -689,11 +690,43 @@ const CODEX: CliEntry = { // composer. So a codex session waiting on a sub-agent reads as plainly idle here. // Nothing is misfiled by that (codex raises no idle prompts), and there is no row to // match until codex pins one. + // Codex is the one CLI Codeman launches with its approvals on by default, and it has no + // hook to say it stopped for one, so its dialogs are read off the screen. Measured on a + // live codex-cli 0.162.0 pane on 2026-10-14 (`-a on-request -s read-only`, widths 200, + // 80, 60, 44 and 36): every dialog it blocks on REPLACES the composer and its footer + // with a numbered option list, the selected one marked `›`, over a key-hint row: + // command approval (a network `› 1. Yes, proceed (y)` ... + // escalation too), edit approval + // ` Press enter to confirm or esc to cancel` + // question (request_user_input) ` › 1. Blue Write the color blue.` ... + // ` tab to add notes | enter to submit answer | esc to interrupt` + // (its notes field open) ` tab or esc to clear notes | enter to submit answer` + // workspace trust, at startup `› 1. Trust and continue` + // ` enter continue · esc quit` + // The pattern needs both: a `› N.` option row, then the key-hint row, which must END + // the window. Below about 60 columns that hint wraps onto rows of its own + // (` Press enter to confirm or esc to` / ` cancel`, or `tab to add notes` / + // `enter to submit answer` / `esc to interrupt` one per row), so the hint may run on + // over rows that start with two spaces and a lowercase word or arrow, and must end on + // one of its own closing words. The transcript can quote every one of those strings + // (measured: an answer that printed them verbatim) and still not match, because while + // no dialog is up codex keeps its composer row (`› ...`, column 0, which ends the + // run) and its footer below anything the agent prints. The longest option list + // measured, a question at 36 columns, put the `›` row 16 non-blank rows up; 20 leaves + // room for longer option text, and a dialog whose marker scrolls out of the window is + // simply not seen (the behaviour before this field). Not matched on purpose: + // `Implement this plan?` after a plan turn, whose `enter select · esc back` footer is + // the same as `/model`, `/permissions` and `/review`, menus the user opened at the + // keyboard; the plan turn has ended there, and the session reads idle as before. + // ⚠️ The question dialog's hint contains `esc to interrupt`, the working line, so + // such a pane read as WORKING before this field: a blocked screen now wins over it. workDetect: { promptGlyph: '›', workingLine: '[Ee]sc to interrupt', watchingLine: String.raw`^\s{0,4}(\d+ background terminals?) running · /ps to view · /stop to close$`, watchingLines: 4, + blockedLine: String.raw`(?:^|\n) {0,4}› \d\. [\s\S]*\n {2}(?:Press enter to confirm|tab (?:to add|or esc to clear) notes|enter continue)(?:[^\n]|\n {2}(?=[a-z←])){0,120}(?:cancel|interrupt|quit|answer|all)$`, + blockedLines: 20, }, // The footer under the composer, measured on a live 0.147.0 pane: // ` gpt-5.6-terra default · ~/codeman-cases/th-scratch` (model, reasoning effort, diff --git a/src/config/cli-registry/types.ts b/src/config/cli-registry/types.ts index 51eab76e..9df6754e 100644 --- a/src/config/cli-registry/types.ts +++ b/src/config/cli-registry/types.ts @@ -406,6 +406,32 @@ export interface CliCapabilities { * tested. See `isAwaitingWorkers()` in `session-activity.ts`. */ awaitingLine?: string; + /** + * Source of a regex matching the dialog this CLI draws when it stops and waits for the + * user to answer something (an approval, a question, a trust prompt): Codeman reads + * such a pane as BLOCKED, with the same red "needs you" alert, push, webhook and + * `blocked` wait signal a Claude `permission_prompt` hook raises. It is the source of + * that state for a CLI with no hook of its own (`hooks: 'none'`); a CLI whose hooks + * report it never consults the screen for it. + * + * Tested against the last `blockedLines` non-blank rows of a pane capture, joined + * with `\n`, so the pattern can require several markers at once and anchor on the + * end of the window (no `m` flag: `$` is the last row). It must anchor on chrome + * only that CLI draws, at the FOOT of the screen, where a dialog replaces the + * composer: the text above is the agent's own, and it can print anything. A forged + * match may cost a false alert and nothing else, because Codeman never answers such a + * dialog. See `isBlockedScreen()` in `session-activity.ts`. + * + * A CLI that omits it is never blocked from the screen, which is what every CLI was + * before the field existed. + */ + blockedLine?: string; + /** + * How many non-blank rows at the foot of the screen `blockedLine` sees. A dialog's + * options wrap at narrow widths, which is what pushes its markers up the screen, so + * this is sized for the narrowest pane the CLI was measured at. Default 1. + */ + blockedLines?: number; }; /** * How many columns this CLI indents its transcript body by, so a copy taken from its diff --git a/src/session-activity.ts b/src/session-activity.ts index 599e9662..97f728a3 100644 --- a/src/session-activity.ts +++ b/src/session-activity.ts @@ -159,6 +159,66 @@ export function watchingLabel( return null; } +/** + * How many non-blank rows at the foot of a pane capture `isBlockedScreen()` reads for a + * CLI that declares no number of its own (`capabilities.workDetect.blockedLines`). + */ +export const BLOCKED_TAIL_LINES = 1; + +/** Hard cap on that window, whatever a registry entry asks for (the schema's own max). */ +export const BLOCKED_MAX_TAIL_LINES = 24; + +/** + * How often a session that is showing a blocking dialog re-reads its screen to learn the + * dialog went away. The raise needs no timer, because a dialog is drawn at the END of a + * burst of output and the idle confirmation reads the screen once the pane goes quiet. The + * clear does: answering resumes the turn, and a running turn keeps the pane busy, so the + * settle path would not look again until the whole turn ended, and the red alert would + * outlive the dialog by minutes. One `capture-pane` per interval, and only while a dialog + * is up. + */ +export const BLOCKED_RECHECK_MS = 2000; + +/** + * Consecutive readable captures without the dialog before the blocked state clears. One + * read can land mid-repaint (a resize redraws the whole frame), and a clear followed by a + * re-raise would send the alert, the push and the webhook a second time for the same + * dialog. Two reads at BLOCKED_RECHECK_MS clear 2 to 4 s after the answer. + */ +export const BLOCKED_CLEAR_READS = 2; + +/** + * Whether the foot of a pane shows a dialog this CLI blocks on (an approval, a question, + * a trust prompt), read with the CLI's `capabilities.workDetect.blockedLine`. + * + * The last `tailLines` non-blank rows are joined with `\n` and tested once, so the pattern + * can demand several markers and anchor its footer on the end of the window. Blank rows + * are dropped first, like `watchingLabel()`, because a TUI pads its dialog with them. + * + * ⚠️ The text is pane-derived and the agent writes most of the pane, so a pattern must + * end on chrome only the CLI draws (a dialog's key-hint footer replaces the composer and + * its footer, which nothing the agent prints can sit below). Even so, a match is only + * ever an ALERT: nothing keys a keystroke off it. + * + * @param tailLines how many non-blank rows from the bottom the pattern sees, defaulting to + * `BLOCKED_TAIL_LINES` and capped at `BLOCKED_MAX_TAIL_LINES` + */ +export function isBlockedScreen( + paneText: string | null | undefined, + pattern: RegExp, + tailLines: number = BLOCKED_TAIL_LINES +): boolean { + if (!paneText) return false; + const rows = stripAnsi(paneText) + .split('\n') + .map((row) => row.trimEnd()) + .filter((row) => row !== ''); + const window = rows.slice(-Math.max(1, Math.min(tailLines, BLOCKED_MAX_TAIL_LINES))).join('\n'); + // Same reasoning as watchingLabel(): a caller's `g` flag must not make this flap. + pattern.lastIndex = 0; + return pattern.test(window); +} + /** * How many rows above the composer the turn's closing row may sit. Between the two Claude * draws only its composer border and, sometimes, a right-aligned hint diff --git a/src/session.ts b/src/session.ts index 5a2bb677..9ed2b9e5 100644 --- a/src/session.ts +++ b/src/session.ts @@ -89,7 +89,11 @@ import { isPaneQuiet, watchingLabel, isAwaitingWorkers, + isBlockedScreen, WATCHING_TAIL_LINES, + BLOCKED_TAIL_LINES, + BLOCKED_RECHECK_MS, + BLOCKED_CLEAR_READS, IDLE_RECHECK_MS, PANE_PROBE_MIN_INTERVAL_MS, PANE_PROBE_RECHECK_MS, @@ -596,6 +600,21 @@ export class Session extends EventEmitter { private _watchingWindow = WATCHING_TAIL_LINES; /** Lazily compiled `capabilities.workDetect.awaitingLine`. See _awaitingLinePattern(). */ private _awaitingLineRe: RegExp | null | undefined = undefined; + /** Lazily compiled `capabilities.workDetect.blockedLine`. See _blockedLinePattern(). */ + private _blockedLineRe: RegExp | null | undefined = undefined; + /** Resolved with the pattern above: how many rows at the foot of the screen it sees. */ + private _blockedWindow = BLOCKED_TAIL_LINES; + /** + * The pane is showing a dialog the CLI blocks on, read off the screen with + * `capabilities.workDetect.blockedLine` (codex: an approval, a question, the trust + * prompt). Live state, not persisted: a restarted server re-reads it from the attach + * repaint the same way the watching label comes back. See `_noteBlockedReading()`. + */ + private _screenBlocked = false; + /** Consecutive readable captures without the dialog (cleared at BLOCKED_CLEAR_READS). */ + private _blockedGoneReads = 0; + /** Re-reads the screen while a dialog is up, so the alert clears with it. */ + private _blockedRecheckTimer: NodeJS.Timeout | null = null; /** * The newest model the running CLI reported for itself (its statusline, or its own * footer read off the probe's capture), or null when none has. Feeds `displayModel` @@ -1429,6 +1448,16 @@ export class Session extends EventEmitter { return this._watching; } + /** + * True while the pane shows a dialog its CLI is blocked on, as read off the screen + * (`capabilities.workDetect.blockedLine`). Always false for a CLI that declares none, + * Claude included: its dialogs arrive as hook events instead. The `blocked` wait reads + * it so a caller that asks after the dialog appeared is answered at once. + */ + get screenBlocked(): boolean { + return this._screenBlocked; + } + /** * Prompt-cache state of the main conversation as Claude Code last reported it on the * statusline (v2.1.251+), fed by `POST /api/status-telemetry`. Null until the first @@ -3081,6 +3110,10 @@ export class Session extends EventEmitter { this._expensiveProcessTimer = null; } this._pendingCleanData = ''; + // Nothing reads this pane any more, so a dialog on it can no longer be seen to + // close: drop the state (and its timer) while the listeners are still attached. + // A re-attach re-reads the screen and raises it again if the dialog is still up. + this._clearScreenBlocked(); // If using mux, mark the session as detached but don't kill it if (this._muxSession && this._mux) { this._mux.setAttached(this.id, false); @@ -3265,10 +3298,19 @@ export class Session extends EventEmitter { if (now - this._lastPaneProbeAt < PANE_PROBE_MIN_INTERVAL_MS) return this._lastPaneProbeWorking; this._lastPaneProbeAt = now; const text = this._mux.capturePaneText?.(this._muxSession.muxName) ?? null; + // A dialog the CLI is blocked on is not work, whatever else is on screen: codex's + // question dialog carries `esc to interrupt` in its own key hints, which is codex's + // working line, so without this a session waiting on the user's answer read as busy. + // Decided from THIS capture, never from `_screenBlocked`, which lags a vanished dialog + // by design (see `_noteBlockedReading`) and would otherwise call a resumed turn idle. + const blockedOnScreen = text === null ? null : this._paneShowsBlocked(text); + this._noteBlockedReading(blockedOnScreen, text); // A turn that ended by handing off to workers the CLI waits for is work too: the // composer is up and the pane is quiet, but the next turn starts without the user. this._lastPaneProbeWorking = - text === null ? null : this._workingLinePattern().test(text) || this._paneAwaitsWorkers(text); + text === null + ? null + : !blockedOnScreen && (this._workingLinePattern().test(text) || this._paneAwaitsWorkers(text)); this._readWatching(text); this._readScreenModel(text); return this._lastPaneProbeWorking; @@ -3462,6 +3504,95 @@ export class Session extends EventEmitter { return isAwaitingWorkers(paneText, this._awaitingLineRe, glyph); } + /** + * The regex matching the dialog this CLI blocks on, or null for a CLI whose registry + * entry declares none (every CLI but codex today, Claude included: its dialogs arrive + * as hook events). Compiled once per session through `compileVersionRegex()`, the same + * guard the schema applies, so a pattern it refuses yields null rather than a throw. + */ + private _blockedLinePattern(): RegExp | null { + if (this._blockedLineRe === undefined) { + const detect = getCli(this.mode)?.capabilities.workDetect; + this._blockedLineRe = detect?.blockedLine ? compileVersionRegex(detect.blockedLine) : null; + this._blockedWindow = detect?.blockedLines ?? BLOCKED_TAIL_LINES; + } + return this._blockedLineRe; + } + + /** Whether this capture shows a dialog the CLI is blocked on. See `isBlockedScreen()`. */ + private _paneShowsBlocked(paneText: string): boolean { + const pattern = this._blockedLinePattern(); + return pattern ? isBlockedScreen(paneText, pattern, this._blockedWindow) : false; + } + + /** + * Fold one screen reading into the blocked state, announcing each transition with + * `blockedChanged` (the web server turns it into the alert and the `blocked` signal). + * + * The two directions are deliberately unequal: + * - RAISE on one reading. A match needs the dialog's own footer at the foot of the + * screen, which is only there when the dialog is fully drawn. + * - CLEAR on BLOCKED_CLEAR_READS readings in a row. A single capture can land inside a + * full-frame repaint, and clearing on it would re-raise a moment later and send the + * alert, push and webhook twice for one dialog. + * A capture that could not be read (`null`) is no evidence either way and changes + * nothing, so a tmux hiccup can neither raise nor drop an alert. That is the opposite + * of `_readWatching`, and for the same reason: each degrades toward the alert. + * + * While blocked, `_blockedRecheckTimer` keeps reading, since answering resumes the turn + * and a busy pane never reaches the settle path that raised it. + * + * @param frame the capture the reading came from; rides the raise as the alert's context + */ + private _noteBlockedReading(onScreen: boolean | null, frame: string | null): void { + if (onScreen === null || !this._blockedLinePattern()) return; + if (onScreen) { + this._blockedGoneReads = 0; + if (!this._screenBlocked) { + this._screenBlocked = true; + this.emit('blockedChanged', true, frame); + } + this._armBlockedRecheck(); + return; + } + if (!this._screenBlocked) return; + if (++this._blockedGoneReads < BLOCKED_CLEAR_READS) { + this._armBlockedRecheck(); + return; + } + this._clearScreenBlocked(); + } + + /** (Re)arm the while-blocked screen read. One-shot and self-rearming, like the trust scan. */ + private _armBlockedRecheck(): void { + if (this._blockedRecheckTimer) clearTimeout(this._blockedRecheckTimer); + this._blockedRecheckTimer = setTimeout(() => { + this._blockedRecheckTimer = null; + // No pane left to read (detached): nothing can change until a re-attach, whose + // `_resetBuffers()` clears the state and whose repaint reads the screen afresh. + if (this._isStopped || !this._screenBlocked || !this._mux?.capturePaneText || !this._muxSession) return; + const text = this._mux.capturePaneText(this._muxSession.muxName) ?? null; + // Unreadable this time: keep the state, keep looking (_noteBlockedReading ignores a null). + if (text === null) { + this._armBlockedRecheck(); + return; + } + this._noteBlockedReading(this._paneShowsBlocked(text), text); + }, BLOCKED_RECHECK_MS); + } + + /** Drop the blocked state (and its timer), announcing it when it was set. */ + private _clearScreenBlocked(): void { + this._blockedGoneReads = 0; + if (this._blockedRecheckTimer) { + clearTimeout(this._blockedRecheckTimer); + this._blockedRecheckTimer = null; + } + if (!this._screenBlocked) return; + this._screenBlocked = false; + this.emit('blockedChanged', false, null); + } + /** * The regex matching this CLI's "a turn is running" status line. * @@ -4004,6 +4135,9 @@ export class Session extends EventEmitter { // into a program that never enabled tracking. this._cliMouseModes.clear(); this._syncCliMouseTracking(); + // Nor does it start inside a dialog. Announced, so a pending alert for the old + // pane's dialog is resolved rather than left behind it. + this._clearScreenBlocked(); this._markActivity(true); } @@ -4016,6 +4150,11 @@ export class Session extends EventEmitter { clearTimeout(this._trustDialogTimer); this._trustDialogTimer = null; } + // Clear the while-blocked screen read (the state itself is left to its owner) + if (this._blockedRecheckTimer) { + clearTimeout(this._blockedRecheckTimer); + this._blockedRecheckTimer = null; + } // Clear activity timeout to prevent memory leak if (this.activityTimeout) { diff --git a/test/cli-registry-schema.test.ts b/test/cli-registry-schema.test.ts index 156d315b..97855ee9 100644 --- a/test/cli-registry-schema.test.ts +++ b/test/cli-registry-schema.test.ts @@ -183,6 +183,63 @@ describe('workDetect.workingLine is guarded like every other config regex', () = expect(compileVersionRegex(src), `${entry.id} declares an awaitingLine the guard refuses`).not.toBeNull(); } }); + + it('holds the optional blockedLine to the same guard', () => { + // Run over the foot of a capture at every settle and on a timer while a dialog is + // up, and a clis.json override can set it. + for (const blockedLine of ['(a+)+b', 'a'.repeat(201), '([unclosed', '']) { + expectRejected( + (e) => { + (e.capabilities as Record).workDetect = { + promptGlyph: '>', + workingLine: 'working', + blockedLine, + }; + }, + `blockedLine ${JSON.stringify(blockedLine.slice(0, 20))} must be refused at LOAD time` + ); + } + }); + + it('bounds the blockedLine window and refuses one with no pattern', () => { + for (const blockedLines of [0, 25, 2.5]) { + expectRejected((e) => { + (e.capabilities as Record).workDetect = { + promptGlyph: '>', + workingLine: 'working', + blockedLine: 'Press enter to confirm$', + blockedLines, + }; + }, 'a config file must not be able to widen the search to the whole pane'); + } + expectRejected((e) => { + (e.capabilities as Record).workDetect = { + promptGlyph: '>', + workingLine: 'working', + blockedLines: 4, + }; + }, 'a window with nothing to search is a typo whose failure is otherwise silent'); + }); + + it('accepts a blockedLine with its window, and every shipped one', () => { + const entry = baseEntry(); + (entry.capabilities as Record).workDetect = { + promptGlyph: '>', + workingLine: 'working', + blockedLine: 'Press enter to confirm$', + blockedLines: 24, + }; + expect(CliEntrySchema.safeParse(entry).success).toBe(true); + let shipped = 0; + for (const stock of STOCK_CLIS) { + const src = stock.capabilities.workDetect?.blockedLine; + if (!src) continue; + shipped++; + expect(compileVersionRegex(src), `${stock.id} declares a blockedLine the guard refuses`).not.toBeNull(); + expect(CliEntrySchema.safeParse(JSON.parse(JSON.stringify(stock))).success).toBe(true); + } + expect(shipped).toBeGreaterThan(0); + }); }); describe('no shell text can reach the command line', () => { diff --git a/test/codex-blocked-detection.test.ts b/test/codex-blocked-detection.test.ts new file mode 100644 index 00000000..6b73a900 --- /dev/null +++ b/test/codex-blocked-detection.test.ts @@ -0,0 +1,352 @@ +/** + * A Codex session waiting on one of its own dialogs is BLOCKED, read off the screen. + * + * Codex is the one CLI Codeman launches with its approvals on by default, and it has no + * hook to say it stopped for one (`hooks: 'none'`), so a session parked on "Would you like + * to run the following command?" used to read as plainly idle, and one parked on a + * question (whose key hints carry `esc to interrupt`, codex's working line) as WORKING. + * The registry's `capabilities.workDetect.blockedLine` now names the dialog's chrome, the + * session reads it at every settle and on a timer while it is up, and the server raises + * exactly what a Claude `permission_prompt` hook raises, alert-only. + * + * The fixtures in `test/fixtures/codex-dialogs/` are live `tmux capture-pane -p` frames + * (no -J, what `capturePaneText()` reads) of codex-cli 0.162.0 on 2026-10-14, run with + * `-a on-request -s read-only` at 200, 80, 60, 44 and 36 columns; only trailing + * whitespace was trimmed and the scratch path replaced by `/tmp/codex-scratch/repo`. + */ +import { describe, expect, it, vi, afterEach } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { Session } from '../src/session.js'; +import { getCli } from '../src/config/cli-registry/index.js'; +import { compileVersionRegex } from '../src/config/cli-registry/patterns.js'; +import { + isBlockedScreen, + BLOCKED_TAIL_LINES, + BLOCKED_MAX_TAIL_LINES, + BLOCKED_RECHECK_MS, + BLOCKED_CLEAR_READS, + IDLE_SILENCE_MS, +} from '../src/session-activity.js'; +import type { SessionMode } from '../src/types.js'; + +const DETECT = getCli('codex')!.capabilities.workDetect!; +const CODEX_BLOCKED = compileVersionRegex(DETECT.blockedLine!)!; +const CODEX_WINDOW = DETECT.blockedLines!; +const CODEX_WORKING = compileVersionRegex(DETECT.workingLine)!; + +const FIXTURES = new URL('./fixtures/codex-dialogs/', import.meta.url); +const fixture = (name: string): string => readFileSync(new URL(name, FIXTURES), 'utf8'); +const blocked = (screen: string): boolean => isBlockedScreen(screen, CODEX_BLOCKED, CODEX_WINDOW); + +/** Every dialog codex was measured blocking on. */ +const DIALOGS = [ + 'exec-approval.txt', + 'exec-approval-w36.txt', // the key hint wraps: `Press enter to confirm or esc to` / `cancel` + 'exec-approval-w44.txt', + 'exec-approval-long-command.txt', // a 14-row heredoc preview between the header and the options + 'exec-approval-60x14.txt', // a 14-row pane: codex folds the preview to `[… 21 lines]` + 'edit-approval.txt', + 'network-escalation-approval.txt', + 'question.txt', + 'question-option2-selected.txt', + 'question-1of2.txt', // `| ←/→ to navigate questions |` in the hint + 'question-2of2-submit-all.txt', // `enter to submit all` on the last question + 'question-w36.txt', // the hint one segment per row, the `›` row 16 rows up + 'question-w60.txt', + 'question-notes.txt', // the notes field open: `tab or esc to clear notes | enter to submit answer` + 'question-notes-typed.txt', + 'trust-dialog.txt', // drawn at the TOP of an otherwise blank screen at startup + 'trust-dialog-w40.txt', +]; + +/** Screens with no dialog codex blocks on, several of them quoting the dialogs' words. */ +const NOT_DIALOGS = [ + 'idle-start.txt', + 'composer-typed.txt', + 'working.txt', + 'after-approve.txt', + 'after-deny.txt', + 'after-question.txt', // the transcript keeps `• Questions 1/1 answered` and the question + 'slash-popup.txt', // `› /plan switch to Plan mode`: a `›` row with nothing after it + 'transcript-quotes-dialog.txt', // an answer that printed the dialog's lines verbatim + 'composer-typed-dialog-text.txt', // the user TYPED them into the composer + 'composer-typed-dialog-text-w60.txt', // ... and at 60 columns, where the text wraps + 'model-popup.txt', // menus the user opened at the keyboard: `enter select · esc back` + 'permissions-popup.txt', // (with the quoted dialog from the earlier answer still above it) + 'review-popup.txt', + 'implement-plan.txt', // deliberately not blocked: a plan turn that ended, same footer as /model +]; + +describe('codex blockedLine on live captures', () => { + it('is a pattern the registry guard accepts, with a window inside the cap', () => { + expect(DETECT.blockedLine!.length).toBeLessThanOrEqual(200); + expect(CODEX_BLOCKED).not.toBeNull(); + expect(CODEX_WINDOW).toBeGreaterThan(BLOCKED_TAIL_LINES); + expect(CODEX_WINDOW).toBeLessThanOrEqual(BLOCKED_MAX_TAIL_LINES); + }); + + it.each(DIALOGS)('reads %s as blocked', (name) => { + expect(blocked(fixture(name))).toBe(true); + }); + + it.each(NOT_DIALOGS)('does not read %s as blocked', (name) => { + expect(blocked(fixture(name))).toBe(false); + }); + + it('needs the whole window for the narrowest question, and no more than it declares', () => { + // At 36 columns the selected option sits 16 non-blank rows above the end; a window + // that stops short of it reads nothing, which is the pre-field behaviour, never a + // false alert. + const narrow = fixture('question-w36.txt'); + expect(isBlockedScreen(narrow, CODEX_BLOCKED, 16)).toBe(true); + expect(isBlockedScreen(narrow, CODEX_BLOCKED, 15)).toBe(false); + }); + + it('sees the question dialogs that the working line used to claim', () => { + // Why a blocked screen has to WIN over the working line in the probe: the question's + // own key hints end in `esc to interrupt`. + for (const name of ['question.txt', 'question-w36.txt', 'question-1of2.txt']) { + expect(CODEX_WORKING.test(fixture(name)), name).toBe(true); + expect(blocked(fixture(name)), name).toBe(true); + } + }); +}); + +describe('codex blockedLine against a screen the agent writes', () => { + // The foot of codex's screen while no dialog is up: composer, footer (whose model and + // path vary), hint row. + const composer = (typed = 'Ask Codex to do anything', footer = ' GPT-6-Luna default · ~/cases/x'): string[] => [ + `› ${typed}`, + '', + footer, + ' ? for shortcuts', + ]; + const forgedDialog = [ + '• Here is what the dialog looks like:', + '› 1. Yes, proceed (y)', + ' 2. No, and tell Codex what to do differently (esc)', + '', + ' Press enter to confirm or esc to cancel', + ]; + + it('refuses a forged dialog in the transcript, whatever follows it', () => { + expect(blocked([...forgedDialog, '', ...composer()].join('\n'))).toBe(false); + // While a prompt is typed the hint row goes away, so the forged hint sits only two + // rows above the end: the composer row (column 0, `›`) still breaks the run. + expect(blocked([...forgedDialog, ...composer('next thing').slice(0, 3)].join('\n'))).toBe(false); + // Even with a lowercase model and a short path, the footer row would have to end in + // one of the hint's own closing words. + expect(blocked([...forgedDialog, ...composer('x', ' gpt-5.6 default · ~/x').slice(0, 3)].join('\n'))).toBe(false); + }); + + it('refuses the working view with the dialog quoted right above the status line', () => { + const screen = [...forgedDialog, '', '• Working (3s • esc to interrupt)', '', ...composer()].join('\n'); + expect(blocked(screen)).toBe(false); + }); + + it('refuses a hint row without the option row above it, and an option row without the hint', () => { + expect( + blocked( + [' Would you like to run the following command?', ' Press enter to confirm or esc to cancel'].join('\n') + ) + ).toBe(false); + expect(blocked(['› 1. Yes, proceed (y)', ' 2. No (esc)'].join('\n'))).toBe(false); + }); + + it('stays linear on a hostile window', () => { + // Many option rows and long rows, ending in something that is almost the hint: the + // pattern must give up quickly, since it runs on a timer while a dialog is up. + const row = `› 1. ${'x'.repeat(300)}`; + const screen = [ + ...Array(BLOCKED_MAX_TAIL_LINES - 1).fill(row), + ` Press enter to confirm ${'or '.repeat(60)}`, + ].join('\n'); + const started = performance.now(); + for (let i = 0; i < 50; i++) blocked(screen); + expect(performance.now() - started).toBeLessThan(250); + expect(blocked(screen)).toBe(false); + }); + + it('caps the window at BLOCKED_MAX_TAIL_LINES whatever the caller asks', () => { + const dialog = fixture('exec-approval.txt'); + const padded = `${dialog}\n${Array(BLOCKED_MAX_TAIL_LINES).fill(' filler').join('\n')}`; + expect(isBlockedScreen(padded, CODEX_BLOCKED, 10_000)).toBe(false); + }); + + it('answers false for an empty or unreadable capture', () => { + expect(isBlockedScreen('', CODEX_BLOCKED, CODEX_WINDOW)).toBe(false); + expect(isBlockedScreen(null, CODEX_BLOCKED, CODEX_WINDOW)).toBe(false); + }); +}); + +// ─── The session's blocked state ──────────────────────────────────────────── + +/** Codex's own composer repaint: carries the glyph that arms the idle confirmation. */ +const CODEX_REPAINT = '\x1b[31;1H\x1b[38;5;246m›\xa0\x1b[39m\x1b[0m'; + +type SessionInternals = { + _handleTerminalOutput(data: string): void; + _detectInteractiveActivity(data: string): void; +}; + +function feed(session: Session, data: string): void { + const internals = session as unknown as SessionInternals; + internals._handleTerminalOutput(data); + internals._detectInteractiveActivity(data); +} + +/** A session whose mux answers captures from `screen.text` (null = unreadable). */ +function withFakePane(screen: { text: string | null }, mode: SessionMode = 'codex'): Session { + const mux = { + isAvailable: () => true, + capturePaneText: () => screen.text, + } as unknown as NonNullable[0]>['mux']; + return new Session({ + workingDir: '/tmp', + mode, + mux, + muxSession: { muxName: 'codeman-test', sessionId: 'test', createdAt: Date.now() }, + } as ConstructorParameters[0]); +} + +/** Output, then quiet: the moment the idle confirmation reads the screen. */ +function settle(session: Session): void { + for (let i = 0; i < 3; i++) { + feed(session, CODEX_REPAINT); + vi.advanceTimersByTime(1000); + } + vi.advanceTimersByTime(IDLE_SILENCE_MS + 2000); +} + +function recordChanges(session: Session): Array<{ blocked: boolean; frame: string | null }> { + const changes: Array<{ blocked: boolean; frame: string | null }> = []; + session.on('blockedChanged', (b: boolean, frame: string | null) => changes.push({ blocked: b, frame })); + return changes; +} + +describe('Session.screenBlocked', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it('raises once when the pane settles on an approval, and reads the session idle', () => { + vi.useFakeTimers(); + const screen = { text: fixture('exec-approval.txt') }; + const session = withFakePane(screen); + const changes = recordChanges(session); + + settle(session); + expect(session.screenBlocked).toBe(true); + expect(session.status).toBe('idle'); + expect(changes).toHaveLength(1); + expect(changes[0].blocked).toBe(true); + // The frame it was read from rides along, as the approval card's context. + expect(changes[0].frame).toContain('Would you like to run the following command?'); + + // The dialog staying up is one dialog: no second raise from the timer or a repaint. + vi.advanceTimersByTime(BLOCKED_RECHECK_MS * 5); + settle(session); + expect(changes).toHaveLength(1); + }); + + it('calls a question dialog blocked and idle, not working', () => { + // Its key hints carry `esc to interrupt`, which is codex's working line: before the + // field the probe called this pane busy for as long as the question stayed up. + vi.useFakeTimers(); + const session = withFakePane({ text: fixture('question.txt') }); + settle(session); + expect(session.screenBlocked).toBe(true); + expect(session.status).toBe('idle'); + }); + + it('clears after the dialog has been gone for BLOCKED_CLEAR_READS reads, without waiting for the turn', () => { + vi.useFakeTimers(); + const screen = { text: fixture('exec-approval.txt') as string | null }; + const session = withFakePane(screen); + const changes = recordChanges(session); + settle(session); + expect(session.screenBlocked).toBe(true); + + // Answered: codex resumes the turn, and nothing settles while it runs. The timer is + // what notices, and one read alone is not enough. + screen.text = fixture('working.txt'); + vi.advanceTimersByTime(BLOCKED_RECHECK_MS); + expect(session.screenBlocked).toBe(true); + vi.advanceTimersByTime(BLOCKED_RECHECK_MS * (BLOCKED_CLEAR_READS - 1)); + expect(session.screenBlocked).toBe(false); + expect(changes.map((c) => c.blocked)).toEqual([true, false]); + + // Nothing keeps reading a screen with no dialog on it. + vi.advanceTimersByTime(BLOCKED_RECHECK_MS * 10); + expect(changes).toHaveLength(2); + }); + + it('does not flap on one read that misses a dialog still on screen', () => { + // A capture inside a full-frame repaint can come back without the footer. Clearing on + // it would raise again a moment later: a second alert, push and webhook for one dialog. + vi.useFakeTimers(); + const dialog = fixture('exec-approval.txt'); + const screen = { text: dialog as string | null }; + const session = withFakePane(screen); + const changes = recordChanges(session); + settle(session); + + screen.text = fixture('working.txt'); + vi.advanceTimersByTime(BLOCKED_RECHECK_MS); + screen.text = dialog; + vi.advanceTimersByTime(BLOCKED_RECHECK_MS * 3); + expect(session.screenBlocked).toBe(true); + expect(changes).toHaveLength(1); + }); + + it('keeps its state through an unreadable capture, in either direction', () => { + vi.useFakeTimers(); + const screen = { text: fixture('exec-approval.txt') as string | null }; + const session = withFakePane(screen); + settle(session); + screen.text = null; + vi.advanceTimersByTime(BLOCKED_RECHECK_MS * 5); + expect(session.screenBlocked).toBe(true); + + // ...and it is still watching: once the screen reads again without the dialog, it clears. + screen.text = fixture('after-approve.txt'); + vi.advanceTimersByTime(BLOCKED_RECHECK_MS * BLOCKED_CLEAR_READS); + expect(session.screenBlocked).toBe(false); + }); + + it('raises again for the next dialog after a clear', () => { + vi.useFakeTimers(); + const screen = { text: fixture('exec-approval.txt') as string | null }; + const session = withFakePane(screen); + const changes = recordChanges(session); + settle(session); + screen.text = fixture('after-approve.txt'); + vi.advanceTimersByTime(BLOCKED_RECHECK_MS * BLOCKED_CLEAR_READS); + screen.text = fixture('edit-approval.txt'); + settle(session); + expect(changes.map((c) => c.blocked)).toEqual([true, false, true]); + }); + + it('never reads a dialog off the screen of a CLI that declares no blockedLine', () => { + // Claude's dialogs arrive as hooks; gemini declares nothing. Same screen, no state. + vi.useFakeTimers(); + for (const mode of ['claude', 'gemini'] as const) { + const session = withFakePane({ text: fixture('exec-approval.txt') }, mode); + const changes = recordChanges(session); + settle(session); + expect(session.screenBlocked, mode).toBe(false); + expect(changes, mode).toHaveLength(0); + } + }); + + it('drops the state when a new pane starts, announcing it', () => { + vi.useFakeTimers(); + const session = withFakePane({ text: fixture('trust-dialog.txt') }); + const changes = recordChanges(session); + settle(session); + expect(session.screenBlocked).toBe(true); + (session as unknown as { _resetBuffers(): void })._resetBuffers(); + expect(session.screenBlocked).toBe(false); + expect(changes.map((c) => c.blocked)).toEqual([true, false]); + }); +}); diff --git a/test/fixtures/codex-dialogs/after-approve.txt b/test/fixtures/codex-dialogs/after-approve.txt new file mode 100644 index 00000000..1ed7bfcb --- /dev/null +++ b/test/fixtures/codex-dialogs/after-approve.txt @@ -0,0 +1,50 @@ +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.162.0 -> 0.162.1 │ +│ Run npm install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + + +› Run the shell command: touch created.txt (it needs write access, ask me for approval to run it outside the sandbox) + + +• touch created.txt needs write access in the workspace. I’ll request approval to run that exact command outside the read only sandbox. + +✔ You approved codex to run touch created.txt this time + +• Ran touch created.txt + └ (no output) + +• Created created.txt successfully. + + Worked for 17s • 4:23 AM + + + + + + + + + + + + + + + + + + + + +› Ask Codex to do anything + + GPT-6-Luna default · /tmp/codex-scratch/repo · Request approval to create file + ? for shortcuts diff --git a/test/fixtures/codex-dialogs/after-deny.txt b/test/fixtures/codex-dialogs/after-deny.txt new file mode 100644 index 00000000..f2d00094 --- /dev/null +++ b/test/fixtures/codex-dialogs/after-deny.txt @@ -0,0 +1,50 @@ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + + +› Run the shell command: touch created.txt (it needs write access, ask me for approval to run it outside the sandbox) + + +• touch created.txt needs write access in the workspace. I’ll request approval to run that exact command outside the read only sandbox. + +✔ You approved codex to run touch created.txt this time + +• Ran touch created.txt + └ (no output) + +• Created created.txt successfully. + + Worked for 17s • 4:23 AM + + +› Edit calc.py to add a function sub(a, b) that returns a - b. Use your file editing tool, not a shell command. + + +• I’ll inspect calc.py, then use the file editing tool to add the function. + +• Explored + └ Read calc.py + + Show details + +• Edited calc.py (+4 -0) + 3 + + 4 + + 5 +def sub(a, b): + + Show details + +■ Conversation interrupted - use /feedback if something went wrong + +✘ Failed to apply patch + └ (error details unavailable) + + Show details + + +› Ask Codex to do anything + + GPT-6-Luna default · /tmp/codex-scratch/repo · Request approval to create file + ? for shortcuts diff --git a/test/fixtures/codex-dialogs/after-question.txt b/test/fixtures/codex-dialogs/after-question.txt new file mode 100644 index 00000000..12153d67 --- /dev/null +++ b/test/fixtures/codex-dialogs/after-question.txt @@ -0,0 +1,50 @@ + + +› Use curl to fetch https://example.com and tell me the HTTP status code. + + +• I’ll use curl to fetch the URL and report the response status. + +• Failed (exit 6) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ curl: (6) Could not resolve host: example.com + 000 + +• curl couldn’t resolve example.com in the sandbox, so it returned status 000. I need network access to fetch it. + + Worked for 3s • 4:24 AM + + +› Request approval to rerun that curl with network access (escalated permissions). + + +• I’ll request approval to rerun the same curl command with network access. + +✗ You canceled the request to run curl -sS -o /dev/null -w '%{http_code}' https://example.com + +• Failed (exit 1) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want to add a greeting file. Before planning, ask me which language the greeting should be in, using your question tool with a few options. + + +• Questions 1/1 answered + • Which language should the greeting file use? + answer: English (Recommended) + +• Explored + └ List rg --files -g AGENTS.md -g '*greet*' -g 'README*' -g Makefile -g pyproject.toml -g package.json -g calc.py + + Show details + +• Working (5s • esc to interrupt) + └ Tip: Use /feedback to send logs to the maintainers when something looks off. + + +› Ask Codex to do anything + + GPT-6-Luna medium · /tmp/codex-scratch/repo · Request approval to create file Plan mode + ? for shortcuts diff --git a/test/fixtures/codex-dialogs/composer-typed-dialog-text-w60.txt b/test/fixtures/codex-dialogs/composer-typed-dialog-text-w60.txt new file mode 100644 index 00000000..b44cad8b --- /dev/null +++ b/test/fixtures/codex-dialogs/composer-typed-dialog-text-w60.txt @@ -0,0 +1,23 @@ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want a file named animal.txt. Use your question tool to + ask me which animal to write in it, with 3 options. Ask + only that one question. + + +■ Conversation interrupted - use /feedback if something went +wrong + + +› › 1. Yes, proceed (y) Press enter to confirm or esc to + cancel tab to add notes | enter to submit answer | esc + to interrupt + + GPT-6-Luna medium · /tmp/codex-scratch/repo Plan mode diff --git a/test/fixtures/codex-dialogs/composer-typed-dialog-text.txt b/test/fixtures/codex-dialogs/composer-typed-dialog-text.txt new file mode 100644 index 00000000..d6912737 --- /dev/null +++ b/test/fixtures/codex-dialogs/composer-typed-dialog-text.txt @@ -0,0 +1,49 @@ +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.162.0 -> 0.162.1 │ +│ Run npm install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want a file named animal.txt. Use your question tool to ask me which animal to write in it, with 3 options. Ask only that one question. + + +■ Conversation interrupted - use /feedback if something went wrong + + + + + + + + + + + + + + + + + + + + + + + + + + + +› › 1. Yes, proceed (y) Press enter to confirm or esc to cancel tab to add notes | enter to submit answer | esc to interrupt + + GPT-6-Luna medium · /tmp/codex-scratch/repo · Ask for animal choice Plan mode (shift+tab to cycle) diff --git a/test/fixtures/codex-dialogs/composer-typed.txt b/test/fixtures/codex-dialogs/composer-typed.txt new file mode 100644 index 00000000..f16a6bad --- /dev/null +++ b/test/fixtures/codex-dialogs/composer-typed.txt @@ -0,0 +1,49 @@ +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.162.0 -> 0.162.1 │ +│ Run npm install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +› Run the shell command: touch created.txt (it needs write access, ask me for approval to run it outside the sandbox) + + GPT-6-Luna default · /tmp/codex-scratch/repo diff --git a/test/fixtures/codex-dialogs/edit-approval.txt b/test/fixtures/codex-dialogs/edit-approval.txt new file mode 100644 index 00000000..c166f8e7 --- /dev/null +++ b/test/fixtures/codex-dialogs/edit-approval.txt @@ -0,0 +1,50 @@ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + + +› Run the shell command: touch created.txt (it needs write access, ask me for approval to run it outside the sandbox) + + +• touch created.txt needs write access in the workspace. I’ll request approval to run that exact command outside the read only sandbox. + +✔ You approved codex to run touch created.txt this time + +• Ran touch created.txt + └ (no output) + +• Created created.txt successfully. + + Worked for 17s • 4:23 AM + + +› Edit calc.py to add a function sub(a, b) that returns a - b. Use your file editing tool, not a shell command. + + +• I’ll inspect calc.py, then use the file editing tool to add the function. + +• Explored + └ Read calc.py + + Show details + +• Edited calc.py (+4 -0) + 3 + + 4 + + 5 +def sub(a, b): + + Show details + + + Would you like to make the following edits? + + Description: Apply proposed file edits + Destination: /tmp/codex-scratch/repo/calc.py + + +› 1. Yes, proceed (y) + 2. Yes, and don't ask again for these files (a) + 3. No, and tell Codex what to do differently (esc) + + Press enter to confirm or esc to cancel diff --git a/test/fixtures/codex-dialogs/exec-approval-60x14.txt b/test/fixtures/codex-dialogs/exec-approval-60x14.txt new file mode 100644 index 00000000..78a316d4 --- /dev/null +++ b/test/fixtures/codex-dialogs/exec-approval-60x14.txt @@ -0,0 +1,14 @@ + + Would you like to run the following command? + + Environment: local + + Reason: May I run the requested quoted heredoc with + write access to create lines.txt? + + $ cat > lines.txt <<'EOF' + line 1 + [… 21 lines] ^a view all +› 1. Yes, proceed (y) + 2. No, and tell Codex what to do differently (esc) + Press enter to confirm or esc to cancel diff --git a/test/fixtures/codex-dialogs/exec-approval-long-command.txt b/test/fixtures/codex-dialogs/exec-approval-long-command.txt new file mode 100644 index 00000000..9eccf4f9 --- /dev/null +++ b/test/fixtures/codex-dialogs/exec-approval-long-command.txt @@ -0,0 +1,50 @@ + +› Reply with exactly these five lines as plain text, nothing else, no tools: 'Would you like to run the following command?' then '› 1. Yes, proceed (y)' then ' 2. No, and tell Codex what to do + differently (esc)' then 'tab to add notes | enter to submit answer | esc to interrupt' then ' Press enter to confirm or esc to cancel' + + +• Would you like to run the following command? + › 1. Yes, proceed (y) + 2. No, and tell Codex what to do differently (esc) + tab to add notes | enter to submit answer | esc to interrupt + Press enter to confirm or esc to cancel + + Worked for 1s • 4:31 AM + + +› Run this exact multi-line shell command with escalated permissions (ask me for approval): a bash heredoc that writes 12 lines 'line 1' through 'line 12' into lines.txt, using cat > lines.txt with a + quoted heredoc, each line written out literally. + + +• I’ll request approval to run the quoted heredoc that writes the 12 literal lines to lines.txt. + +• Running cat > lines.txt <<'EOF' … + + Show details + + + Would you like to run the following command? + + Environment: local + + Reason: May I run the requested quoted heredoc with write access to create lines.txt? + + $ cat > lines.txt <<'EOF' + line 1 + line 2 + line 3 + line 4 + line 5 + line 6 + line 7 + line 8 + line 9 + line 10 + line 11 + line 12 + EOF + + +› 1. Yes, proceed (y) + 2. No, and tell Codex what to do differently (esc) + + Press enter to confirm or esc to cancel diff --git a/test/fixtures/codex-dialogs/exec-approval-w36.txt b/test/fixtures/codex-dialogs/exec-approval-w36.txt new file mode 100644 index 00000000..d2ba3e4e --- /dev/null +++ b/test/fixtures/codex-dialogs/exec-approval-w36.txt @@ -0,0 +1,30 @@ +› Run: touch second.txt (needs app… + access outside the read only + sandbox. I’ll request approval to + run that exact command. + +• Running touch second.txt + + + Would you like to run the + following command? + + Environment: local + + Reason: May I run `touch + second.txt` outside the read + only sandbox to create the + requested file? + + $ touch second.txt + + +› 1. Yes, proceed (y) + 2. Yes, and don't ask again for + commands that start with `touch + second.txt` (p) + 3. No, and tell Codex what to do + differently (esc) + + Press enter to confirm or esc to + cancel diff --git a/test/fixtures/codex-dialogs/exec-approval-w44.txt b/test/fixtures/codex-dialogs/exec-approval-w44.txt new file mode 100644 index 00000000..8ad0e839 --- /dev/null +++ b/test/fixtures/codex-dialogs/exec-approval-w44.txt @@ -0,0 +1,30 @@ + outside the sandbox) + + +• Creating second.txt requires write access + outside the read only sandbox. I’ll + request approval to run that exact + command. + +• Running touch second.txt + + + Would you like to run the following + command? + + Environment: local + + Reason: May I run `touch second.txt` + outside the read only sandbox to create + the requested file? + + $ touch second.txt + + +› 1. Yes, proceed (y) + 2. Yes, and don't ask again for commands + that start with `touch second.txt` (p) + 3. No, and tell Codex what to do + differently (esc) + + Press enter to confirm or esc to cancel diff --git a/test/fixtures/codex-dialogs/exec-approval.txt b/test/fixtures/codex-dialogs/exec-approval.txt new file mode 100644 index 00000000..85ac4baa --- /dev/null +++ b/test/fixtures/codex-dialogs/exec-approval.txt @@ -0,0 +1,50 @@ +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.162.0 -> 0.162.1 │ +│ Run npm install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + + +› Run the shell command: touch created.txt (it needs write access, ask me for approval to run it outside the sandbox) + + +• touch created.txt needs write access in the workspace. I’ll request approval to run that exact command outside the read only sandbox. + +• Running touch created.txt + + + + + + + + + + + + + + + + + + Would you like to run the following command? + + Environment: local + + Reason: May I run `touch created.txt` outside the read only sandbox to create the requested file? + + $ touch created.txt + + +› 1. Yes, proceed (y) + 2. Yes, and don't ask again for commands that start with `touch created.txt` (p) + 3. No, and tell Codex what to do differently (esc) + + Press enter to confirm or esc to cancel diff --git a/test/fixtures/codex-dialogs/idle-start.txt b/test/fixtures/codex-dialogs/idle-start.txt new file mode 100644 index 00000000..63109680 --- /dev/null +++ b/test/fixtures/codex-dialogs/idle-start.txt @@ -0,0 +1,50 @@ +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.162.0 -> 0.162.1 │ +│ Run npm install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + + + + + + ⣀⣤⣤⣤⣀⣀ + ⣠⣶⣿⣿⣿⣿⣿⣿⣿⣿⣦⣤⣤⣤⣤⣤⣤⡀ + ⢀⣾⣿⣿⠿⠋⠉⠉⢉⣭⣿⣿⣿⣿⣿⠿⣿⣿⣿⣿⣷⣄ + ⣀⣾⣿⣿⠃ ⣀⣴⣾⣿⣿⡿⠟⠋ ⣀⡀ ⠈⠙⢿⣿⣿⣧ + ⣀⣶⣿⣿⣿⣿⡇ ⢸⣿⣿⡿⠛⠉⢀⣠⣴⣿⣿⣿⣷⣦⣀⠈⢻⣿⣿⡇ + ⣰⣿⣿⡿⢿⣿⣿⡇ ⢸⣿⣿⢀⣤⣶⣿⣿⣿⠟⠛⠻⣿⣿⣿⣿⣮⣿⣿⡷ + ⢰⣿⣿⡟⠁⢸⣿⣿⡇ ⢸⣿⣿⣿⣿⠿⠿⣿⣿⣿⣦⣄⡀⠈⠛⠿⣿⣿⣿⣧⡀ + ⣾⣿⣿⠃ ⢸⣿⣿⡇ ⢸⣿⣿⠋⠁ ⠈⠙⢻⣿⣿⣿⣶⣤⡀ ⠹⢿⣿⣷⡄ + ⢸⣿⣿⣇ ⠸⣿⣿⣷⣦⣼⣿⣿ ⢸⣿⣿⠻⢿⣿⣿⡇ ⠘⣿⣿⣿ + ⠈⢿⣿⣿⣦⡀⠈⠛⠿⣿⣿⣿⣿⣄⡀ ⢀⣠⣼⣿⣿ ⣿⣿⡇ ⣿⣿⣿⠇ + ⢻⣿⣿⣿⣷⣦⣀⠈⠙⠻⣿⣿⣿⣶⣶⣿⣿⣿⣿⣿ ⣿⣿⡇ ⣠⣿⣿⣿ + ⢸⣿⣿⡿⢿⣿⣿⣿⣦⣠⣴⣿⣿⣿⡿⠟⠋⢸⣿⣿ ⣿⣿⣷⣴⣿⣿⣿⠃ + ⠘⣿⣿⣷ ⠈⠛⢿⣿⣿⣿⠿⠋⠉ ⣠⣴⣿⣿⣿ ⣿⣿⣿⣿⣿⠟⠁ + ⠻⣿⣿⣿⣤⣀ ⠈⠉ ⢀⣤⣶⣿⣿⣿⠿⠟⠁ ⣰⣿⣿⡟⠋⠁ + ⠈⠿⣿⣿⣿⣿⣶⣾⣿⣿⣿⣿⠟⠋⠁ ⣠⣼⣿⣿⡟ + ⠙⠛⠛⠛⠻⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⡿⠏ + ⠈⠙⠛⠿⠿⠿⠿⠛⠉ + + + + + + + + + + + + + +› Ask Codex to do anything + + GPT-6-Luna default · /tmp/codex-scratch/repo + ? for shortcuts diff --git a/test/fixtures/codex-dialogs/implement-plan.txt b/test/fixtures/codex-dialogs/implement-plan.txt new file mode 100644 index 00000000..e2d76713 --- /dev/null +++ b/test/fixtures/codex-dialogs/implement-plan.txt @@ -0,0 +1,50 @@ +› I want to add a greeting file. Before planning, ask me which language the greeting should be in, using your question tool with a few options. +• Questions 1/1 answered + • Which language should the greeting file use? + answer: English (Recommended) + +• Questions 2/2 answered + • What should the greeting file be named? + answer: greeting.txt (Recommended) + • What greeting should it contain? + answer: Hello, world! (Recommended) + +• Explored + └ List rg --files -g AGENTS.md -g '*greet*' -g 'README*' -g Makefile -g pyproject.toml -g package.json -g calc.py + + Show details + +• Proposed Plan + + + # Add an English Greeting File + + ## Summary + + Create greeting.txt in the repository root containing Hello, world!. + + ## Changes + + • Add the greeting as plain text, followed by a newline. + • No public APIs or existing files need to change. + + ## Acceptance Check + + • Confirm greeting.txt exists at the repository root and contains exactly Hello, world! on its first line. + + ## Assumptions + + • Use the selected filename and greeting. + • Keep the file as plain text. + + + Worked for 1m 47s • 4:27 AM + + + Implement this plan? + + +› 1. Yes, implement this plan Switch to Default and start coding + 2. Yes, clear context and implement Start a fresh thread (current context: 2% used) + 3. No, stay in Plan mode Continue planning with the model + + enter select · esc back diff --git a/test/fixtures/codex-dialogs/model-popup.txt b/test/fixtures/codex-dialogs/model-popup.txt new file mode 100644 index 00000000..3353d114 --- /dev/null +++ b/test/fixtures/codex-dialogs/model-popup.txt @@ -0,0 +1,50 @@ +› I want to add a greeting file. Before planning, ask me which language the greeting should be in, using your question tool with a few options. +• Questions 1/1 answered + • Which language should the greeting file use? + answer: English (Recommended) + +• Questions 2/2 answered + • What should the greeting file be named? + answer: greeting.txt (Recommended) + • What greeting should it contain? + answer: Hello, world! (Recommended) + +• Explored + └ List rg --files -g AGENTS.md -g '*greet*' -g 'README*' -g Makefile -g pyproject.toml -g package.json -g calc.py + + Show details + +• Proposed Plan + + + # Add an English Greeting File + + ## Summary + + Create greeting.txt in the repository root containing Hello, world!. + + ## Changes + + • Add the greeting as plain text, followed by a newline. + • No public APIs or existing files need to change. + + ## Acceptance Check + + • Confirm greeting.txt exists at the repository root and contains exactly Hello, world! on its first line. + + ## Assumptions + + • Use the selected filename and greeting. + • Keep the file as plain text. + + + Worked for 1m 47s • 4:27 AM + + + Select Model and Effort + + +› 1. GPT-6-Luna (current) Fast and affordable model for easier tasks. + 2. GPT-5.6-Terra Older balanced model for straightforward work. + 3. GPT-5.6-Luna Older fast and efficient model. + + enter select · esc back diff --git a/test/fixtures/codex-dialogs/network-escalation-approval.txt b/test/fixtures/codex-dialogs/network-escalation-approval.txt new file mode 100644 index 00000000..16256303 --- /dev/null +++ b/test/fixtures/codex-dialogs/network-escalation-approval.txt @@ -0,0 +1,50 @@ +› Edit calc.py to add a function sub(a, b) that returns a - b. Use your file editing tool, not a shell command. +• Edited calc.py (+4 -0) + 3 + + 4 + + 5 +def sub(a, b): + + Show details + +■ Conversation interrupted - use /feedback if something went wrong + +✘ Failed to apply patch + └ (error details unavailable) + + Show details + + +› Use curl to fetch https://example.com and tell me the HTTP status code. + + +• I’ll use curl to fetch the URL and report the response status. + +• Failed (exit 6) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ curl: (6) Could not resolve host: example.com + 000 + +• curl couldn’t resolve example.com in the sandbox, so it returned status 000. I need network access to fetch it. + + Worked for 3s • 4:24 AM + + +› Request approval to rerun that curl with network access (escalated permissions). + + +• I’ll request approval to rerun the same curl command with network access. + +• Running curl -sS -o /dev/null -w '%{http_code}' https://example.com + + + Would you like to run the following command? + + Environment: local + + Reason: May I rerun curl with network access to fetch https://example.com and report its HTTP status? + + $ curl -sS -o /dev/null -w '%{http_code}' https://example.com + + +› 1. Yes, proceed (y) + 2. Yes, and don't ask again for commands that start with `curl -sS -o /dev/null -w '%{http_code}' https://example.com` (p) + 3. No, and tell Codex what to do differently (esc) + + Press enter to confirm or esc to cancel diff --git a/test/fixtures/codex-dialogs/permissions-popup.txt b/test/fixtures/codex-dialogs/permissions-popup.txt new file mode 100644 index 00000000..22ba2b59 --- /dev/null +++ b/test/fixtures/codex-dialogs/permissions-popup.txt @@ -0,0 +1,50 @@ + +› Run: touch second.txt (needs approval outside the sandbox) + + +• Creating second.txt requires write access outside the read only sandbox. I’ll request approval to run that exact command. + +✗ You canceled the request to run touch second.txt + +• Failed (exit 1) touch second.txt + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want a new file named color.txt. Ask me with your question tool which color to write in it (offer 3 options). Ask only that one question. + + +• Explored · 1 failed + └ List rg --files -g color.txt -g AGENTS.md (exit 1) + + Show details + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna default for Default mode. + + +› Reply with exactly these five lines as plain text, nothing else, no tools: 'Would you like to run the following command?' then '› 1. Yes, proceed (y)' then ' 2. No, and tell Codex what to do + differently (esc)' then 'tab to add notes | enter to submit answer | esc to interrupt' then ' Press enter to confirm or esc to cancel' + + +• Would you like to run the following command? + › 1. Yes, proceed (y) + 2. No, and tell Codex what to do differently (esc) + tab to add notes | enter to submit answer | esc to interrupt + Press enter to confirm or esc to cancel + + Worked for 1s • 4:31 AM + + + Update Model Permissions + + + 1. Ask for approval Read and edit workspace files and run commands, with approval required for internet access or edits outside the workspace + 2. Approve for me Only ask for actions detected as potentially unsafe + 3. Full Access Use with caution: Codex can edit files outside this workspace and access the internet without approval +› 4. Read Only (current) Read workspace files, with approval required for edits or internet access + + enter select · esc back diff --git a/test/fixtures/codex-dialogs/question-1of2.txt b/test/fixtures/codex-dialogs/question-1of2.txt new file mode 100644 index 00000000..d5237451 --- /dev/null +++ b/test/fixtures/codex-dialogs/question-1of2.txt @@ -0,0 +1,49 @@ + + +• I’ll use curl to fetch the URL and report the response status. + +• Failed (exit 6) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ curl: (6) Could not resolve host: example.com + 000 + +• curl couldn’t resolve example.com in the sandbox, so it returned status 000. I need network access to fetch it. + + Worked for 3s • 4:24 AM + + +› Request approval to rerun that curl with network access (escalated permissions). + + +• I’ll request approval to rerun the same curl command with network access. + +✗ You canceled the request to run curl -sS -o /dev/null -w '%{http_code}' https://example.com + +• Failed (exit 1) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want to add a greeting file. Before planning, ask me which language the greeting should be in, using your question tool with a few options. + + +• Questions 1/1 answered + • Which language should the greeting file use? + answer: English (Recommended) + +• Explored + └ List rg --files -g AGENTS.md -g '*greet*' -g 'README*' -g Makefile -g pyproject.toml -g package.json -g calc.py + + Show details + + + Question 1/2 (2 unanswered) + What should the greeting file be named? + + › 1. greeting.txt (Recommended) Use a clear, conventional text filename. + 2. README greeting Add the greeting as a separate README-style Markdown file. + 3. hello.txt Use a short, friendly filename. + 4. None of the above Optionally, add details in notes (tab) + + tab to add notes | enter to submit answer | ←/→ to navigate questions | esc to interrupt diff --git a/test/fixtures/codex-dialogs/question-2of2-submit-all.txt b/test/fixtures/codex-dialogs/question-2of2-submit-all.txt new file mode 100644 index 00000000..c5f6e8d1 --- /dev/null +++ b/test/fixtures/codex-dialogs/question-2of2-submit-all.txt @@ -0,0 +1,49 @@ + + +• I’ll use curl to fetch the URL and report the response status. + +• Failed (exit 6) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ curl: (6) Could not resolve host: example.com + 000 + +• curl couldn’t resolve example.com in the sandbox, so it returned status 000. I need network access to fetch it. + + Worked for 3s • 4:24 AM + + +› Request approval to rerun that curl with network access (escalated permissions). + + +• I’ll request approval to rerun the same curl command with network access. + +✗ You canceled the request to run curl -sS -o /dev/null -w '%{http_code}' https://example.com + +• Failed (exit 1) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want to add a greeting file. Before planning, ask me which language the greeting should be in, using your question tool with a few options. + + +• Questions 1/1 answered + • Which language should the greeting file use? + answer: English (Recommended) + +• Explored + └ List rg --files -g AGENTS.md -g '*greet*' -g 'README*' -g Makefile -g pyproject.toml -g package.json -g calc.py + + Show details + + + Question 2/2 (1 unanswered) + What greeting should it contain? + + › 1. Hello, world! (Recommended) Use a short, familiar English greeting. + 2. Welcome! Use a concise welcoming message. + 3. Good morning! Use a time-specific greeting. + 4. None of the above Optionally, add details in notes (tab) + + tab to add notes | enter to submit all | ←/→ to navigate questions | esc to interrupt diff --git a/test/fixtures/codex-dialogs/question-notes-typed.txt b/test/fixtures/codex-dialogs/question-notes-typed.txt new file mode 100644 index 00000000..7fdcc294 --- /dev/null +++ b/test/fixtures/codex-dialogs/question-notes-typed.txt @@ -0,0 +1,49 @@ +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.162.0 -> 0.162.1 │ +│ Run npm install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want a file named animal.txt. Use your question tool to ask me which animal to write in it, with 3 options. Ask only that one question. + + + + + + + + + + + + + + + + + + + + + + + Question 1/1 (1 unanswered) + Which animal should I write in animal.txt? + + › 1. Cat Write “cat” in the file. + 2. Dog Write “dog” in the file. + 3. Elephant Write “elephant” in the file. + 4. None of the above Optionally, add details in notes (tab) + + › abc + + tab or esc to clear notes | enter to submit answer diff --git a/test/fixtures/codex-dialogs/question-notes.txt b/test/fixtures/codex-dialogs/question-notes.txt new file mode 100644 index 00000000..63bc767c --- /dev/null +++ b/test/fixtures/codex-dialogs/question-notes.txt @@ -0,0 +1,49 @@ +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.162.0 -> 0.162.1 │ +│ Run npm install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want a file named animal.txt. Use your question tool to ask me which animal to write in it, with 3 options. Ask only that one question. + + + + + + + + + + + + + + + + + + + + + + + Question 1/1 (1 unanswered) + Which animal should I write in animal.txt? + + › 1. Cat Write “cat” in the file. + 2. Dog Write “dog” in the file. + 3. Elephant Write “elephant” in the file. + 4. None of the above Optionally, add details in notes (tab) + + › Add notes + + tab or esc to clear notes | enter to submit answer diff --git a/test/fixtures/codex-dialogs/question-option2-selected.txt b/test/fixtures/codex-dialogs/question-option2-selected.txt new file mode 100644 index 00000000..56759317 --- /dev/null +++ b/test/fixtures/codex-dialogs/question-option2-selected.txt @@ -0,0 +1,49 @@ + + Show details + + +› Use curl to fetch https://example.com and tell me the HTTP status code. + + +• I’ll use curl to fetch the URL and report the response status. + +• Failed (exit 6) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ curl: (6) Could not resolve host: example.com + 000 + +• curl couldn’t resolve example.com in the sandbox, so it returned status 000. I need network access to fetch it. + + Worked for 3s • 4:24 AM + + +› Request approval to rerun that curl with network access (escalated permissions). + + +• I’ll request approval to rerun the same curl command with network access. + +✗ You canceled the request to run curl -sS -o /dev/null -w '%{http_code}' https://example.com + +• Failed (exit 1) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want to add a greeting file. Before planning, ask me which language the greeting should be in, using your question tool with a few options. + + +• Explored + └ List rg --files -g AGENTS.md -g '*greet*' -g 'README*' -g Makefile -g pyproject.toml -g package.json -g calc.py + + Show details + + + Question 1/1 (1 unanswered) + Which language should the greeting file use? + + 1. English (Recommended) Write a simple greeting in English. + › 2. French Write a simple greeting in French. + 3. Spanish Write a simple greeting in Spanish. + 4. None of the above Optionally, add details in notes (tab) + + tab to add notes | enter to submit answer | esc to interrupt diff --git a/test/fixtures/codex-dialogs/question-w36.txt b/test/fixtures/codex-dialogs/question-w36.txt new file mode 100644 index 00000000..b58fea38 --- /dev/null +++ b/test/fixtures/codex-dialogs/question-w36.txt @@ -0,0 +1,29 @@ + + +• Explored · 1 failed + └ List rg --files -g color.txt -g + AGENTS.md (exit 1) + + Show details + + + Question 1/1 (1 unanswered) + Which color should I write in + `color.txt`? + + › 1. Blue Write the + color + blue. + 2. Green Write the + color + green. + 3. Red Write the + color red. + 4. None of the Optionally + above , add + details in + notes + (tab) + + tab to add notes + enter to submit answer + esc to interrupt diff --git a/test/fixtures/codex-dialogs/question-w60.txt b/test/fixtures/codex-dialogs/question-w60.txt new file mode 100644 index 00000000..a9a19490 --- /dev/null +++ b/test/fixtures/codex-dialogs/question-w60.txt @@ -0,0 +1,29 @@ +› Run: touch second.txt (needs approval outside the sandbo… + +■ Conversation interrupted - use /feedback if something went +wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want a new file named color.txt. Ask me with your + question tool which color to write in it (offer 3 + options). Ask only that one question. + + +• Explored · 1 failed + └ List rg --files -g color.txt -g AGENTS.md (exit 1) + + Show details + + + Question 1/1 (1 unanswered) + Which color should I write in `color.txt`? + + › 1. Blue Write the color blue. + 2. Green Write the color green. + 3. Red Write the color red. + 4. None of the above Optionally, add details in notes + (tab) + + tab to add notes | enter to submit answer + esc to interrupt diff --git a/test/fixtures/codex-dialogs/question.txt b/test/fixtures/codex-dialogs/question.txt new file mode 100644 index 00000000..99ec73ce --- /dev/null +++ b/test/fixtures/codex-dialogs/question.txt @@ -0,0 +1,49 @@ + + Show details + + +› Use curl to fetch https://example.com and tell me the HTTP status code. + + +• I’ll use curl to fetch the URL and report the response status. + +• Failed (exit 6) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ curl: (6) Could not resolve host: example.com + 000 + +• curl couldn’t resolve example.com in the sandbox, so it returned status 000. I need network access to fetch it. + + Worked for 3s • 4:24 AM + + +› Request approval to rerun that curl with network access (escalated permissions). + + +• I’ll request approval to rerun the same curl command with network access. + +✗ You canceled the request to run curl -sS -o /dev/null -w '%{http_code}' https://example.com + +• Failed (exit 1) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want to add a greeting file. Before planning, ask me which language the greeting should be in, using your question tool with a few options. + + +• Explored + └ List rg --files -g AGENTS.md -g '*greet*' -g 'README*' -g Makefile -g pyproject.toml -g package.json -g calc.py + + Show details + + + Question 1/1 (1 unanswered) + Which language should the greeting file use? + + › 1. English (Recommended) Write a simple greeting in English. + 2. French Write a simple greeting in French. + 3. Spanish Write a simple greeting in Spanish. + 4. None of the above Optionally, add details in notes (tab) + + tab to add notes | enter to submit answer | esc to interrupt diff --git a/test/fixtures/codex-dialogs/review-popup.txt b/test/fixtures/codex-dialogs/review-popup.txt new file mode 100644 index 00000000..4e9031d5 --- /dev/null +++ b/test/fixtures/codex-dialogs/review-popup.txt @@ -0,0 +1,50 @@ + +› Run: touch second.txt (needs approval outside the sandbox) + + +• Creating second.txt requires write access outside the read only sandbox. I’ll request approval to run that exact command. + +✗ You canceled the request to run touch second.txt + +• Failed (exit 1) touch second.txt + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want a new file named color.txt. Ask me with your question tool which color to write in it (offer 3 options). Ask only that one question. + + +• Explored · 1 failed + └ List rg --files -g color.txt -g AGENTS.md (exit 1) + + Show details + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna default for Default mode. + + +› Reply with exactly these five lines as plain text, nothing else, no tools: 'Would you like to run the following command?' then '› 1. Yes, proceed (y)' then ' 2. No, and tell Codex what to do + differently (esc)' then 'tab to add notes | enter to submit answer | esc to interrupt' then ' Press enter to confirm or esc to cancel' + + +• Would you like to run the following command? + › 1. Yes, proceed (y) + 2. No, and tell Codex what to do differently (esc) + tab to add notes | enter to submit answer | esc to interrupt + Press enter to confirm or esc to cancel + + Worked for 1s • 4:31 AM + + + Select a review preset + + +› 1. Review against a base branch (PR Style) + 2. Review uncommitted changes + 3. Review a commit + 4. Custom review instructions + + enter select · esc back diff --git a/test/fixtures/codex-dialogs/slash-popup.txt b/test/fixtures/codex-dialogs/slash-popup.txt new file mode 100644 index 00000000..ad0d0946 --- /dev/null +++ b/test/fixtures/codex-dialogs/slash-popup.txt @@ -0,0 +1,49 @@ +› Edit calc.py to add a function sub(a, b) that returns a - b. Use your file editing tool, not a shell command. + +• Explored + └ Read calc.py + + Show details + +• Edited calc.py (+4 -0) + 3 + + 4 + + 5 +def sub(a, b): + + Show details + +■ Conversation interrupted - use /feedback if something went wrong + +✘ Failed to apply patch + └ (error details unavailable) + + Show details + + +› Use curl to fetch https://example.com and tell me the HTTP status code. + + +• I’ll use curl to fetch the URL and report the response status. + +• Failed (exit 6) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ curl: (6) Could not resolve host: example.com + 000 + +• curl couldn’t resolve example.com in the sandbox, so it returned status 000. I need network access to fetch it. + + Worked for 3s • 4:24 AM + + +› Request approval to rerun that curl with network access (escalated permissions). + + +• I’ll request approval to rerun the same curl command with network access. + +✗ You canceled the request to run curl -sS -o /dev/null -w '%{http_code}' https://example.com + +• Failed (exit 1) curl -sS -o /dev/null -w '%{http_code}' https://example.com + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong +› /plan switch to Plan mode + +› /plan + + GPT-6-Luna default · /tmp/codex-scratch/repo · Request approval to create file diff --git a/test/fixtures/codex-dialogs/transcript-quotes-dialog.txt b/test/fixtures/codex-dialogs/transcript-quotes-dialog.txt new file mode 100644 index 00000000..df4f6fde --- /dev/null +++ b/test/fixtures/codex-dialogs/transcript-quotes-dialog.txt @@ -0,0 +1,50 @@ +› I want to add a greeting file. Before planning, ask me which language the greeting should be in, using your question tool with a few options. + +• Model changed to gpt-6-luna default for Default mode. + + +› Run: touch second.txt (needs approval outside the sandbox) + + +• Creating second.txt requires write access outside the read only sandbox. I’ll request approval to run that exact command. + +✗ You canceled the request to run touch second.txt + +• Failed (exit 1) touch second.txt + └ (no output) + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna medium for Plan mode. + + +› I want a new file named color.txt. Ask me with your question tool which color to write in it (offer 3 options). Ask only that one question. + + +• Explored · 1 failed + └ List rg --files -g color.txt -g AGENTS.md (exit 1) + + Show details + +■ Conversation interrupted - use /feedback if something went wrong + +• Model changed to gpt-6-luna default for Default mode. + + +› Reply with exactly these five lines as plain text, nothing else, no tools: 'Would you like to run the following command?' then '› 1. Yes, proceed (y)' then ' 2. No, and tell Codex what to do + differently (esc)' then 'tab to add notes | enter to submit answer | esc to interrupt' then ' Press enter to confirm or esc to cancel' + + +• Would you like to run the following command? + › 1. Yes, proceed (y) + 2. No, and tell Codex what to do differently (esc) + tab to add notes | enter to submit answer | esc to interrupt + Press enter to confirm or esc to cancel + └ Tip: NEW: Network proxy can now be enabled from /experimental. Restart Codex after enabling it. + + Worked for 1s • 4:31 AM + + +› Ask Codex to do anything + + GPT-6-Luna default · /tmp/codex-scratch/repo · Request approval to create file + ? for shortcuts diff --git a/test/fixtures/codex-dialogs/trust-dialog-w40.txt b/test/fixtures/codex-dialogs/trust-dialog-w40.txt new file mode 100644 index 00000000..2509b8b0 --- /dev/null +++ b/test/fixtures/codex-dialogs/trust-dialog-w40.txt @@ -0,0 +1,17 @@ + + Folder access + /tmp/codex-scratch/repo + + Trust this folder? Codex can read, + edit, and run files here, subject to + your permission settings. Folder + settings can run code automatically, + even without a model request. + Continue only if you trust these + files. Your trust decision will be + saved. + +› 1. Trust and continue + 2. Quit + + enter continue · esc quit diff --git a/test/fixtures/codex-dialogs/trust-dialog.txt b/test/fixtures/codex-dialogs/trust-dialog.txt new file mode 100644 index 00000000..14736009 --- /dev/null +++ b/test/fixtures/codex-dialogs/trust-dialog.txt @@ -0,0 +1,11 @@ + + Folder access + /tmp/codex-scratch/repo + + Trust this folder? Codex can read, edit, and run files here, subject to your permission settings. Folder settings can run code automatically, even without a model request. Continue only if you + trust these files. Your trust decision will be saved. + +› 1. Trust and continue + 2. Quit + + enter continue · esc quit diff --git a/test/fixtures/codex-dialogs/working.txt b/test/fixtures/codex-dialogs/working.txt new file mode 100644 index 00000000..8839f6a1 --- /dev/null +++ b/test/fixtures/codex-dialogs/working.txt @@ -0,0 +1,50 @@ +╭─────────────────────────────────────────────────╮ +│ ✨ Update available! 0.162.0 -> 0.162.1 │ +│ Run npm install -g @openai/codex to update. │ +│ │ +│ See full release notes: │ +│ https://github.com/openai/codex/releases/latest │ +╰─────────────────────────────────────────────────╯ + + + >_ OpenAI Codex (v0.162.0) + /tmp/codex-scratch/repo + + +› Run the shell command: touch created.txt (it needs write access, ask me for approval to run it outside the sandbox) + + +• touch created.txt needs write access in the workspace. I’ll request approval to run that exact command outside the read only sandbox. + + + + + + + + + + + + + + + + + + + + + + + + + + +• Working (1s • esc to interrupt) + + +› Ask Codex to do anything + + GPT-6-Luna default · /tmp/codex-scratch/repo · ⠧ + ? for shortcuts