mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
feat(codex): read approval, question and trust dialogs off the screen
Codex is the one CLI Codeman launches with its approvals on by default, and it declares `hooks: 'none'`, so nothing tells Codeman it stopped to ask. A codex session parked on "Would you like to run the following command?" read as plainly idle, and one parked on a question read as WORKING, because the question's own key hints end in `esc to interrupt`, codex's working line. This adds a generic, optional registry field, `capabilities.workDetect.blockedLine` (plus `blockedLines`, the window), and the session state that reads it: - `isBlockedScreen()` (session-activity.ts, pure) joins the last `blockedLines` non-blank rows of a capture and tests the pattern once, so a pattern can demand several markers and anchor the dialog's footer on the end of the window. - `Session` reads it off the same capture the idle confirmation already takes, emits `blockedChanged`, and exposes `screenBlocked`. It raises on one reading, and while a dialog is up re-reads the screen every 2 s, because answering resumes the turn and a busy pane would not reach the settle path again until the turn ended. It clears only after two readable captures in a row without the dialog, so a capture taken mid-repaint cannot clear and re-raise it; an unreadable capture changes nothing. A blocked screen wins over the working line in the same capture. - The pattern goes through `compileVersionRegex()` in both the schema refine and at use (lazily cached), like every other config regex. Codex's pattern was measured on a live codex-cli 0.162.0 pane on 2026-10-14 (`-a on-request -s read-only`, widths 200 down to 36): it needs a `› N.` option row and then the dialog's key-hint row as the last row, allowing for that row to wrap below about 60 columns. Command, edit and network-escalation approvals, single and multi-question dialogs and the startup trust prompt match; the idle and working views, menus the user opened (`/model`, `/permissions`, `/review`), the composer holding the dialogs' words and a transcript quoting them verbatim do not. `Implement this plan?` is left out on purpose: its footer is the same as `/model`'s and the turn has ended there. The captures are kept as fixtures (trailing whitespace trimmed, the scratch path replaced). Nothing consumes `blockedChanged` yet; the next commit raises the alert. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a8a8bb71a3
commit
beae377122
38 files changed
+2016
-5
No files matched your search
@@ -368,6 +368,22 @@ const capabilitiesSchema = z
|
|||||||
'awaitingLine must be a regex compileVersionRegex() accepts: at most 200 characters, no nested quantifiers'
|
'awaitingLine must be a regex compileVersionRegex() accepts: at most 200 characters, no nested quantifiers'
|
||||||
)
|
)
|
||||||
.optional(),
|
.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()
|
.strict()
|
||||||
// A window with nothing to search is a typo, not a configuration. Refused at LOAD
|
// 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,
|
(v) => v.watchingLines === undefined || v.watchingLine !== undefined,
|
||||||
'watchingLines has nothing to bound without a watchingLine'
|
'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(),
|
.optional(),
|
||||||
model: z
|
model: z
|
||||||
.object({ source: z.enum(['flag', 'claude-settings-file', 'none']), param: z.string().optional() })
|
.object({ source: z.enum(['flag', 'claude-settings-file', 'none']), param: z.string().optional() })
|
||||||
|
|||||||
@@ -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
|
// 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
|
// 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.
|
// 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
|
// What contains it is that codex declares `hooks: 'none'`: no IDLE prompt from a codex
|
||||||
// session ever reaches `notePrompt()`, so there is no idle item to pre-acknowledge
|
// session ever reaches `notePrompt()` (its screen-read dialogs below open PERMISSION
|
||||||
// and a forged label costs a wrong badge and nothing else. A CLI that gains hook
|
// items, which a watching label never acknowledges), so there is no idle item to
|
||||||
// signals must not keep a pattern this soft.
|
// 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.
|
// ⚠️ 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 —
|
// 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
|
// 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.
|
// 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
|
// Nothing is misfiled by that (codex raises no idle prompts), and there is no row to
|
||||||
// match until codex pins one.
|
// 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: {
|
workDetect: {
|
||||||
promptGlyph: '›',
|
promptGlyph: '›',
|
||||||
workingLine: '[Ee]sc to interrupt',
|
workingLine: '[Ee]sc to interrupt',
|
||||||
watchingLine: String.raw`^\s{0,4}(\d+ background terminals?) running · /ps to view · /stop to close$`,
|
watchingLine: String.raw`^\s{0,4}(\d+ background terminals?) running · /ps to view · /stop to close$`,
|
||||||
watchingLines: 4,
|
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:
|
// 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,
|
// ` gpt-5.6-terra default · ~/codeman-cases/th-scratch` (model, reasoning effort,
|
||||||
|
|||||||
@@ -406,6 +406,32 @@ export interface CliCapabilities {
|
|||||||
* tested. See `isAwaitingWorkers()` in `session-activity.ts`.
|
* tested. See `isAwaitingWorkers()` in `session-activity.ts`.
|
||||||
*/
|
*/
|
||||||
awaitingLine?: string;
|
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
|
* How many columns this CLI indents its transcript body by, so a copy taken from its
|
||||||
|
|||||||
@@ -159,6 +159,66 @@ export function watchingLabel(
|
|||||||
return null;
|
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
|
* 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
|
* draws only its composer border and, sometimes, a right-aligned hint
|
||||||
|
|||||||
+140
-1
@@ -89,7 +89,11 @@ import {
|
|||||||
isPaneQuiet,
|
isPaneQuiet,
|
||||||
watchingLabel,
|
watchingLabel,
|
||||||
isAwaitingWorkers,
|
isAwaitingWorkers,
|
||||||
|
isBlockedScreen,
|
||||||
WATCHING_TAIL_LINES,
|
WATCHING_TAIL_LINES,
|
||||||
|
BLOCKED_TAIL_LINES,
|
||||||
|
BLOCKED_RECHECK_MS,
|
||||||
|
BLOCKED_CLEAR_READS,
|
||||||
IDLE_RECHECK_MS,
|
IDLE_RECHECK_MS,
|
||||||
PANE_PROBE_MIN_INTERVAL_MS,
|
PANE_PROBE_MIN_INTERVAL_MS,
|
||||||
PANE_PROBE_RECHECK_MS,
|
PANE_PROBE_RECHECK_MS,
|
||||||
@@ -596,6 +600,21 @@ export class Session extends EventEmitter {
|
|||||||
private _watchingWindow = WATCHING_TAIL_LINES;
|
private _watchingWindow = WATCHING_TAIL_LINES;
|
||||||
/** Lazily compiled `capabilities.workDetect.awaitingLine`. See _awaitingLinePattern(). */
|
/** Lazily compiled `capabilities.workDetect.awaitingLine`. See _awaitingLinePattern(). */
|
||||||
private _awaitingLineRe: RegExp | null | undefined = undefined;
|
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
|
* 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`
|
* 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;
|
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
|
* 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
|
* 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._expensiveProcessTimer = null;
|
||||||
}
|
}
|
||||||
this._pendingCleanData = '';
|
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 using mux, mark the session as detached but don't kill it
|
||||||
if (this._muxSession && this._mux) {
|
if (this._muxSession && this._mux) {
|
||||||
this._mux.setAttached(this.id, false);
|
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;
|
if (now - this._lastPaneProbeAt < PANE_PROBE_MIN_INTERVAL_MS) return this._lastPaneProbeWorking;
|
||||||
this._lastPaneProbeAt = now;
|
this._lastPaneProbeAt = now;
|
||||||
const text = this._mux.capturePaneText?.(this._muxSession.muxName) ?? null;
|
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
|
// 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.
|
// composer is up and the pane is quiet, but the next turn starts without the user.
|
||||||
this._lastPaneProbeWorking =
|
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._readWatching(text);
|
||||||
this._readScreenModel(text);
|
this._readScreenModel(text);
|
||||||
return this._lastPaneProbeWorking;
|
return this._lastPaneProbeWorking;
|
||||||
@@ -3462,6 +3504,95 @@ export class Session extends EventEmitter {
|
|||||||
return isAwaitingWorkers(paneText, this._awaitingLineRe, glyph);
|
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.
|
* 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.
|
// into a program that never enabled tracking.
|
||||||
this._cliMouseModes.clear();
|
this._cliMouseModes.clear();
|
||||||
this._syncCliMouseTracking();
|
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);
|
this._markActivity(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4016,6 +4150,11 @@ export class Session extends EventEmitter {
|
|||||||
clearTimeout(this._trustDialogTimer);
|
clearTimeout(this._trustDialogTimer);
|
||||||
this._trustDialogTimer = null;
|
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
|
// Clear activity timeout to prevent memory leak
|
||||||
if (this.activityTimeout) {
|
if (this.activityTimeout) {
|
||||||
|
|||||||
@@ -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();
|
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<string, unknown>).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<string, unknown>).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<string, unknown>).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<string, unknown>).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', () => {
|
describe('no shell text can reach the command line', () => {
|
||||||
|
|||||||
@@ -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<ConstructorParameters<typeof Session>[0]>['mux'];
|
||||||
|
return new Session({
|
||||||
|
workingDir: '/tmp',
|
||||||
|
mode,
|
||||||
|
mux,
|
||||||
|
muxSession: { muxName: 'codeman-test', sessionId: 'test', createdAt: Date.now() },
|
||||||
|
} as ConstructorParameters<typeof Session>[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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
+50
@@ -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
|
||||||
+50
@@ -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
|
||||||
+50
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
+49
@@ -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
|
||||||
+50
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+50
@@ -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
|
||||||
+50
@@ -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
|
||||||
+50
@@ -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
|
||||||
+50
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+49
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+49
@@ -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
|
||||||
@@ -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
|
||||||
+29
@@ -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
|
||||||
+29
@@ -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
|
||||||
+49
@@ -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
|
||||||
+50
@@ -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
|
||||||
+49
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
+11
@@ -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
|
||||||
+50
@@ -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
|
||||||
Reference in new issue
Block a user