mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-20 09:09:35 +02:00
Merge pull request #624 from Ark0N/feat/codex-blocked-detection
feat(codex): raise codex approval, question and trust dialogs as BLOCKED Conflict resolved against #634 in src/session.ts _probePaneWorking(): #624's assignment (a dialog on screen is never work) is kept, and #634's trust-dialog verdict block (log the hold once, remember the reading) follows it, before _readWatching(). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
c76600315e
75 files changed
+2922
-112
No files matched your search
@@ -183,6 +183,63 @@ describe('workDetect.workingLine is guarded like every other config regex', () =
|
||||
expect(compileVersionRegex(src), `${entry.id} declares an awaitingLine the guard refuses`).not.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('holds the optional blockedLine to the same guard', () => {
|
||||
// Run over the foot of a capture at every settle and on a timer while a dialog is
|
||||
// up, and a clis.json override can set it.
|
||||
for (const blockedLine of ['(a+)+b', 'a'.repeat(201), '([unclosed', '']) {
|
||||
expectRejected(
|
||||
(e) => {
|
||||
(e.capabilities as Record<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', () => {
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
/**
|
||||
* 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,
|
||||
BLOCKED_QUIET_RECHECK_MS,
|
||||
IDLE_SILENCE_MS,
|
||||
PANE_PROBE_MIN_INTERVAL_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',
|
||||
// The same prompt, reached through the update prompt (`2. Skip`) on 0.162.0 with 0.163.0
|
||||
// out: `2. Back to Agent Command Center` over `enter continue · esc back`. Captured
|
||||
// 2026-10-16 from Codeman's own launch flags in a fresh git repo, at 120 columns.
|
||||
'trust-dialog-back.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);
|
||||
}
|
||||
|
||||
/**
|
||||
* What the pane shows, and how often the session has read it: every read is a synchronous
|
||||
* `tmux capture-pane` on the live server.
|
||||
*/
|
||||
type FakeScreen = { text: string | null; captures?: number };
|
||||
|
||||
/** A session whose mux answers captures from `screen.text` (null = unreadable). */
|
||||
function withFakePane(screen: FakeScreen, mode: SessionMode = 'codex'): Session {
|
||||
const mux = {
|
||||
isAvailable: () => true,
|
||||
capturePaneText: () => {
|
||||
screen.captures = (screen.captures ?? 0) + 1;
|
||||
return 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]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The pane changes the only way a real one can: tmux redraws it to the attached client,
|
||||
* which reaches the session as output. A cursor move and an erase, with no composer glyph
|
||||
* in it, so it marks activity without arming the idle confirmation.
|
||||
*/
|
||||
function redraw(session: Session, screen: FakeScreen, text: string | null): void {
|
||||
screen.text = text;
|
||||
feed(session, '\x1b[2;1H\x1b[K');
|
||||
}
|
||||
|
||||
/** 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.
|
||||
redraw(session, screen, 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);
|
||||
|
||||
redraw(session, screen, fixture('working.txt'));
|
||||
vi.advanceTimersByTime(BLOCKED_RECHECK_MS);
|
||||
redraw(session, screen, 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);
|
||||
// The pane moved, so the session looks, and tmux answers nothing it can read.
|
||||
redraw(session, screen, null);
|
||||
const before = screen.captures!;
|
||||
vi.advanceTimersByTime(BLOCKED_RECHECK_MS * 5);
|
||||
expect(screen.captures! - before).toBeGreaterThanOrEqual(5);
|
||||
expect(session.screenBlocked).toBe(true);
|
||||
|
||||
// ...and it is still watching: once the screen reads again without the dialog, it clears.
|
||||
redraw(session, screen, fixture('after-approve.txt'));
|
||||
vi.advanceTimersByTime(BLOCKED_RECHECK_MS * BLOCKED_CLEAR_READS);
|
||||
expect(session.screenBlocked).toBe(false);
|
||||
});
|
||||
|
||||
it('does not re-read a quiet pane every interval while a dialog sits unanswered', () => {
|
||||
// Each read is a synchronous `tmux capture-pane` (about 8 ms of blocked event loop,
|
||||
// measured 2026-10-14), and a dialog can sit unanswered overnight. It leaves the
|
||||
// screen only through a repaint, which reaches the session as output, so a pane that
|
||||
// drew nothing since the last reading that saw the dialog is still showing it.
|
||||
vi.useFakeTimers();
|
||||
const screen: FakeScreen = { text: fixture('exec-approval.txt') };
|
||||
const session = withFakePane(screen);
|
||||
settle(session);
|
||||
expect(session.screenBlocked).toBe(true);
|
||||
|
||||
const quietMs = 10 * 60_000;
|
||||
const before = screen.captures!;
|
||||
vi.advanceTimersByTime(quietMs);
|
||||
// The safety-net read only, never one per BLOCKED_RECHECK_MS (that would be 300 here).
|
||||
expect(screen.captures! - before).toBeLessThanOrEqual(quietMs / BLOCKED_QUIET_RECHECK_MS);
|
||||
expect(session.screenBlocked).toBe(true);
|
||||
|
||||
// The answer repaints the pane, and the clear comes as fast as before.
|
||||
redraw(session, screen, fixture('after-approve.txt'));
|
||||
vi.advanceTimersByTime(BLOCKED_RECHECK_MS * BLOCKED_CLEAR_READS);
|
||||
expect(session.screenBlocked).toBe(false);
|
||||
});
|
||||
|
||||
it('takes the confirming read even when no output follows the reading that missed the dialog', () => {
|
||||
// tmux updates the pane before it sends this client the repaint, so a capture can see
|
||||
// the dialog gone while the session has been sent nothing yet. Skipping a quiet pane
|
||||
// is right only while the last reading SAW the dialog: once one reading missed it,
|
||||
// waiting for output could leave the alert up for the whole safety-net interval.
|
||||
vi.useFakeTimers();
|
||||
const screen: FakeScreen = { text: fixture('exec-approval.txt') };
|
||||
const session = withFakePane(screen);
|
||||
settle(session);
|
||||
vi.advanceTimersByTime(PANE_PROBE_MIN_INTERVAL_MS);
|
||||
screen.text = fixture('after-approve.txt'); // and not one byte of output, from here on
|
||||
(session as unknown as { _probePaneWorking(): boolean | null })._probePaneWorking();
|
||||
expect(session.screenBlocked).toBe(true);
|
||||
vi.advanceTimersByTime(BLOCKED_RECHECK_MS);
|
||||
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);
|
||||
redraw(session, screen, 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,12 @@
|
||||
|
||||
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. Back to Agent Command Center
|
||||
|
||||
enter continue · esc back
|
||||
@@ -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
|
||||
@@ -13,6 +13,7 @@ import fastifyCookie from '@fastify/cookie';
|
||||
import { registerApprovalRoutes } from '../../src/web/routes/approval-routes.js';
|
||||
import { registerHookEventRoutes } from '../../src/web/routes/hook-event-routes.js';
|
||||
import { approvalInbox } from '../../src/web/approval-inbox.js';
|
||||
import { raiseScreenBlocked } from '../../src/web/screen-blocked.js';
|
||||
import { installRouteErrorHandler } from '../../src/web/route-error-handler.js';
|
||||
import { httpStatusForErrorCode, type ApiErrorCode } from '../../src/types.js';
|
||||
import { createMockRouteContext, type MockSession } from '../mocks/index.js';
|
||||
@@ -455,7 +456,6 @@ describe('approval routes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('approval routes: supervised-hook (DeepSeek) alert-only items', () => {
|
||||
let harness: RouteTestHarness;
|
||||
let session: MockSession;
|
||||
@@ -505,6 +505,59 @@ describe('approval routes: supervised-hook (DeepSeek) alert-only items', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('approval routes: dialogs read off a hook-less screen (codex) are alert-only', () => {
|
||||
let harness: RouteTestHarness;
|
||||
let session: MockSession;
|
||||
const noopDeps = {
|
||||
broadcast: () => {},
|
||||
broadcastSessionStateDebounced: () => {},
|
||||
sendPushNotifications: () => {},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createEnvelopeHarness();
|
||||
session = harness.ctx.sessions.get(SESSION_ID)!;
|
||||
session.mode = 'codex';
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
for (const item of approvalInbox.listPending()) {
|
||||
approvalInbox.resolveForSession(item.sessionId, 'dismissed');
|
||||
}
|
||||
approvalInbox.onPending = approvalInbox.onUpdated = approvalInbox.onResolved = undefined;
|
||||
await harness.app.close();
|
||||
});
|
||||
|
||||
it('lists the item, and the page-load sweep keeps it', async () => {
|
||||
raiseScreenBlocked(session as never, noopDeps, '› 1. Yes, proceed (y)\n Press enter to confirm or esc to cancel');
|
||||
const approvals = await listApprovals(harness);
|
||||
expect(approvals).toHaveLength(1);
|
||||
expect(approvals[0]).toMatchObject({ sessionId: SESSION_ID, kind: 'permission', answerInTerminal: true });
|
||||
});
|
||||
|
||||
it('refuses an answer with the terminal-only error, not "cannot have pending approvals"', async () => {
|
||||
// The hooks gate would say a codex session cannot have an item at all, which is false
|
||||
// about the one the caller holds; the alert-only check runs first.
|
||||
raiseScreenBlocked(session as never, noopDeps, null);
|
||||
const [item] = await listApprovals(harness);
|
||||
for (const payload of [{ action: 'approve' }, { action: 'deny' }, { action: 'option', option: 1 }]) {
|
||||
const res = await harness.app.inject({ method: 'POST', url: `/api/approvals/${item.id}/answer`, payload });
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.json().error).toMatch(/^Codex approvals must be answered in the terminal/);
|
||||
}
|
||||
expect(session.writeBuffer).toEqual([]);
|
||||
expect(await listApprovals(harness)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('can still be dismissed', async () => {
|
||||
raiseScreenBlocked(session as never, noopDeps, null);
|
||||
const [item] = await listApprovals(harness);
|
||||
const res = await harness.app.inject({ method: 'POST', url: `/api/approvals/${item.id}/dismiss`, payload: {} });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(await listApprovals(harness)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('approval routes: multi-user scoping', () => {
|
||||
const saved: Record<string, string | undefined> = {};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import { installRouteErrorHandler } from '../../src/web/route-error-handler.js';
|
||||
import { ApiErrorCode, httpStatusForErrorCode } from '../../src/types.js';
|
||||
import { createMockRouteContext, type MockRouteContext } from '../mocks/index.js';
|
||||
import { sessionWaits } from '../../src/web/session-wait-registry.js';
|
||||
import { MAX_WAIT_MS } from '../../src/config/agent-wait.js';
|
||||
import { MAX_WAIT_MS, MIN_WAIT_MS } from '../../src/config/agent-wait.js';
|
||||
|
||||
// Distinct per file on purpose: the three wait suites share the process-wide
|
||||
// `sessionWaits` singleton, so a common id let one file's leftover waiter be counted
|
||||
@@ -315,6 +315,39 @@ describe('POST /api/sessions/:id/input with wait', () => {
|
||||
expect(body.data.status).toBe('busy');
|
||||
});
|
||||
|
||||
it('a duplicate on a codex pane showing a dialog answers blocked, as GET .../wait does', async () => {
|
||||
// A screen-read dialog is a level, not an edge: the original delivery may have raised
|
||||
// it before the caller's connection dropped, and the retry must not wait out its
|
||||
// timeout for an edge that already passed.
|
||||
const { app, ctx } = await harness();
|
||||
const session = ctx.sessions.get(SESSION_ID)! as unknown as {
|
||||
mode: string;
|
||||
status: string;
|
||||
screenBlocked?: boolean;
|
||||
writeBuffer: string[];
|
||||
};
|
||||
session.mode = 'codex';
|
||||
await send(app, { input: 'first', clientId: 'c-codex', seq: 1 });
|
||||
session.status = 'idle';
|
||||
session.screenBlocked = true;
|
||||
|
||||
const replay = await send(app, {
|
||||
input: 'first',
|
||||
clientId: 'c-codex',
|
||||
seq: 1,
|
||||
wait: 'blocked',
|
||||
waitTimeout: MIN_WAIT_MS,
|
||||
});
|
||||
const body = replay.json();
|
||||
|
||||
expect(body.data.duplicate).toBe(true);
|
||||
expect(body.data.wait).toMatchObject({ signal: 'blocked', immediate: true, timedOut: false });
|
||||
|
||||
// ...and only to a caller that asked for it: such a pane is idle too.
|
||||
const idle = await send(app, { input: 'first', clientId: 'c-codex', seq: 1, wait: 'idle' });
|
||||
expect(idle.json().data.wait).toMatchObject({ signal: 'idle', immediate: true });
|
||||
});
|
||||
|
||||
it('gives the dedup seq back when a full waiter pool rejects the request', async () => {
|
||||
// Otherwise the caller's retry is refused as a duplicate and the input vanishes.
|
||||
const { app, ctx } = await harness();
|
||||
|
||||
@@ -34,6 +34,7 @@ import { installRouteErrorHandler } from '../../src/web/route-error-handler.js';
|
||||
import { ApiErrorCode, httpStatusForErrorCode } from '../../src/types.js';
|
||||
import { createMockRouteContext, type MockRouteContext } from '../mocks/index.js';
|
||||
import { sessionWaits } from '../../src/web/session-wait-registry.js';
|
||||
import { approvalInbox } from '../../src/web/approval-inbox.js';
|
||||
import { MAX_WAIT_MS, MIN_WAIT_MS, MAX_WAITERS_TOTAL, MAX_WAITERS_PER_OWNER } from '../../src/config/agent-wait.js';
|
||||
|
||||
// Distinct per file on purpose: the three wait suites share the process-wide
|
||||
@@ -463,6 +464,80 @@ describe('GET /api/sessions/:id/wait: modes that install no hooks', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/sessions/:id/wait: blocked read off a codex screen', () => {
|
||||
/** The listener deps the screen-blocked raise reaches; nothing else matters here. */
|
||||
function stubDeps() {
|
||||
return {
|
||||
broadcast: vi.fn(),
|
||||
broadcastSessionStateDebounced: vi.fn(),
|
||||
sendPushNotifications: vi.fn(),
|
||||
getRunSummaryTracker: vi.fn(() => undefined),
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
approvalInbox.resolveForSession(SESSION_ID, 'session_ended');
|
||||
});
|
||||
|
||||
it('accepts until=blocked for codex, whose dialogs are read off its screen', async () => {
|
||||
const { app, ctx } = await harness();
|
||||
ctx.sessions.get(SESSION_ID)!.mode = 'codex';
|
||||
const res = await app.inject({ method: 'GET', url: `/api/sessions/${SESSION_ID}/wait?until=blocked&timeout=1000` });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json().data.wait.until).toEqual(['blocked']);
|
||||
});
|
||||
|
||||
it('answers blocked at once for a dialog already on screen, and only to a caller who asked', async () => {
|
||||
// A hook-raised blocked is an edge; this one is a level the server is still reading,
|
||||
// so a caller who asks after the dialog appeared does not wait out its timeout.
|
||||
const { app, ctx } = await harness();
|
||||
const session = ctx.sessions.get(SESSION_ID)! as unknown as {
|
||||
mode: string;
|
||||
status: string;
|
||||
screenBlocked?: boolean;
|
||||
};
|
||||
session.mode = 'codex';
|
||||
session.status = 'idle';
|
||||
session.screenBlocked = true;
|
||||
|
||||
const asked = await app.inject({ method: 'GET', url: `/api/sessions/${SESSION_ID}/wait?until=idle,blocked` });
|
||||
expect(asked.json().data.wait).toMatchObject({ signal: 'blocked', immediate: true });
|
||||
|
||||
// Such a pane is idle too, and a caller that asked for idle alone keeps getting idle.
|
||||
const idle = await app.inject({ method: 'GET', url: `/api/sessions/${SESSION_ID}/wait?until=idle` });
|
||||
expect(idle.json().data.wait).toMatchObject({ signal: 'idle', immediate: true });
|
||||
});
|
||||
|
||||
it('wakes a parked until=blocked waiter when the session raises a screen-read dialog', async () => {
|
||||
const { app, ctx } = await harness();
|
||||
const session = ctx.sessions.get(SESSION_ID)!;
|
||||
session.mode = 'codex';
|
||||
attachSessionListeners(session as never, createSessionListeners(session as never, stubDeps() as never));
|
||||
|
||||
const pending = app.inject({ method: 'GET', url: `/api/sessions/${SESSION_ID}/wait?until=blocked&timeout=600000` });
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
expect(sessionWaits.signalWaiterCount(SESSION_ID)).toBe(1);
|
||||
|
||||
session.emit('blockedChanged', true, '› 1. Yes, proceed (y)\n Press enter to confirm or esc to cancel');
|
||||
|
||||
const body = (await pending).json();
|
||||
expect(body.data.wait).toMatchObject({ signal: 'blocked', immediate: false, timedOut: false });
|
||||
expect(approvalInbox.getForSession(SESSION_ID)?.answerInTerminal).toBe(true);
|
||||
|
||||
// ...and the dialog leaving the screen resolves the item it opened.
|
||||
session.emit('blockedChanged', false, null);
|
||||
expect(approvalInbox.getForSession(SESSION_ID)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('still refuses until=stop for codex: nothing on a screen says a turn ended', async () => {
|
||||
const { app, ctx } = await harness();
|
||||
ctx.sessions.get(SESSION_ID)!.mode = 'codex';
|
||||
const res = await app.inject({ method: 'GET', url: `/api/sessions/${SESSION_ID}/wait?until=stop,blocked` });
|
||||
expect(res.statusCode).toBe(400);
|
||||
expect(res.json().error).toContain('Signal(s) stop never fire for codex');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/sessions/:id/wait: liveness beats the reported status', () => {
|
||||
it('answers exit for a session whose PTY is gone, not the idle its status claims', async () => {
|
||||
// Session parks a dead PTY at status 'idle' and the object survives in the map,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* What the server raises for a dialog read off a hook-less CLI's screen (codex), and what
|
||||
* clears it: `web/screen-blocked.ts`. The session side, and the codex captures it reads,
|
||||
* are in `codex-blocked-detection.test.ts`.
|
||||
*
|
||||
* The contract: exactly what a Claude `permission_prompt` hook raises (the `blocked` wait
|
||||
* signal, an Approvals Inbox item, the `hook:permission_prompt` broadcast, push and
|
||||
* webhook, a run-summary row), but ALERT-ONLY, since the dialog was read from text the
|
||||
* agent writes: the item is `answerInTerminal`, push carries no `approvalId`, and the
|
||||
* inbox never resolves or answers it on its own.
|
||||
*/
|
||||
import { describe, expect, it, vi, afterEach } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { raiseScreenBlocked, clearScreenBlocked, screenBlockedMessage } from '../src/web/screen-blocked.js';
|
||||
import { approvalInbox } from '../src/web/approval-inbox.js';
|
||||
import { sessionWaits } from '../src/web/session-wait-registry.js';
|
||||
import { SseEvent } from '../src/web/sse-events.js';
|
||||
import type { SessionMode } from '../src/types.js';
|
||||
|
||||
const FIXTURES = new URL('./fixtures/codex-dialogs/', import.meta.url);
|
||||
const fixture = (name: string): string => readFileSync(new URL(name, FIXTURES), 'utf8');
|
||||
|
||||
describe('raiseScreenBlocked / clearScreenBlocked', () => {
|
||||
const SESSION = { id: 'screen-blocked-test', name: 'w9-scratch', mode: 'codex' as SessionMode };
|
||||
|
||||
function deps() {
|
||||
const recordHookEvent = vi.fn();
|
||||
return {
|
||||
broadcast: vi.fn(),
|
||||
broadcastSessionStateDebounced: vi.fn(),
|
||||
sendPushNotifications: vi.fn(),
|
||||
getRunSummaryTracker: vi.fn(() => ({ recordHookEvent })),
|
||||
recordHookEvent,
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
approvalInbox.resolveForSession(SESSION.id, 'session_ended');
|
||||
sessionWaits.cancelAll(SESSION.id);
|
||||
});
|
||||
|
||||
it('raises what a permission_prompt hook raises, alert-only', async () => {
|
||||
const d = deps();
|
||||
const waiting = sessionWaits.waitForSignal(SESSION.id, { until: ['blocked'], timeoutMs: 5000 });
|
||||
const itemId = raiseScreenBlocked(SESSION, d, fixture('exec-approval.txt'));
|
||||
|
||||
expect((await waiting).signal).toBe('blocked');
|
||||
|
||||
const item = approvalInbox.getForSession(SESSION.id)!;
|
||||
expect(item.id).toBe(itemId);
|
||||
expect(item.kind).toBe('permission');
|
||||
expect(item.answerInTerminal).toBe(true);
|
||||
expect(item.message).toBe('Codex is waiting for your answer in the terminal');
|
||||
// The frame is the card's context, and nothing parsed it into answer buttons.
|
||||
expect(item.context).toContain('Press enter to confirm or esc to cancel');
|
||||
expect(item.options).toBeUndefined();
|
||||
|
||||
expect(d.broadcast).toHaveBeenCalledWith(
|
||||
SseEvent.HookPermissionPrompt,
|
||||
expect.objectContaining({ sessionId: SESSION.id, source: 'screen', approvalId: itemId })
|
||||
);
|
||||
expect(d.broadcastSessionStateDebounced).toHaveBeenCalledWith(SESSION.id);
|
||||
// Push and webhook get the message and NO approvalId, so no Approve/Deny buttons.
|
||||
const [event, push] = d.sendPushNotifications.mock.calls[0] as [string, Record<string, unknown>];
|
||||
expect(event).toBe(SseEvent.HookPermissionPrompt);
|
||||
expect(push).toMatchObject({ sessionId: SESSION.id, sessionName: 'w9-scratch', source: 'screen' });
|
||||
expect(push.message).toBe(screenBlockedMessage('codex'));
|
||||
expect(push.approvalId).toBeUndefined();
|
||||
expect(d.recordHookEvent).toHaveBeenCalledWith('permission_prompt', expect.objectContaining({ source: 'screen' }));
|
||||
});
|
||||
|
||||
it('never steers a keystroke: the inbox item cannot be answered', () => {
|
||||
raiseScreenBlocked(SESSION, deps(), fixture('exec-approval.txt'));
|
||||
const item = approvalInbox.getForSession(SESSION.id)!;
|
||||
// No capture callback, so the inbox's Claude-shaped sweep never resolves it either:
|
||||
// the session owns its lifetime.
|
||||
expect(approvalInbox.verifyStillAnswerable(item.id)).toBe(true);
|
||||
approvalInbox.resolveIfDialogGone(SESSION.id);
|
||||
expect(approvalInbox.getForSession(SESSION.id)?.id).toBe(item.id);
|
||||
});
|
||||
|
||||
it('clears by resolving the item it opened, and only that one', () => {
|
||||
const resolved: string[] = [];
|
||||
const prev = approvalInbox.onResolved;
|
||||
approvalInbox.onResolved = (info) => resolved.push(`${info.id}:${info.resolution}`);
|
||||
try {
|
||||
const itemId = raiseScreenBlocked(SESSION, deps(), null);
|
||||
clearScreenBlocked(SESSION.id, itemId);
|
||||
expect(approvalInbox.getForSession(SESSION.id)).toBeUndefined();
|
||||
expect(resolved).toContain(`${itemId}:resolved_in_terminal`);
|
||||
|
||||
// An item it did not open (here a newer one) is left alone.
|
||||
const newer = raiseScreenBlocked(SESSION, deps(), null);
|
||||
clearScreenBlocked(SESSION.id, itemId);
|
||||
expect(approvalInbox.getForSession(SESSION.id)?.id).toBe(newer);
|
||||
clearScreenBlocked(SESSION.id, null);
|
||||
expect(approvalInbox.getForSession(SESSION.id)?.id).toBe(newer);
|
||||
} finally {
|
||||
approvalInbox.onResolved = prev;
|
||||
}
|
||||
});
|
||||
|
||||
it('raises nothing for a session whose CLI reports blocked through hooks', () => {
|
||||
const d = deps();
|
||||
expect(raiseScreenBlocked({ ...SESSION, mode: 'claude' }, d, null)).toBeNull();
|
||||
expect(d.broadcast).not.toHaveBeenCalled();
|
||||
expect(d.sendPushNotifications).not.toHaveBeenCalled();
|
||||
expect(approvalInbox.getForSession(SESSION.id)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SessionWaitRegistry,
|
||||
WaitCapacityError,
|
||||
hooksAvailableForMode,
|
||||
blockedSignalAvailable,
|
||||
parseWaitSignals,
|
||||
resolveWaitSignals,
|
||||
signalForStatus,
|
||||
@@ -100,6 +101,26 @@ describe('signalForStatus', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('blockedSignalAvailable', () => {
|
||||
it('is true where hooks report it, and for a CLI that declares a blockedLine (codex)', () => {
|
||||
expect(blockedSignalAvailable('claude')).toBe(true);
|
||||
expect(blockedSignalAvailable('codex')).toBe(true);
|
||||
// Not keyed on hooks: codex has none, and its screen-read dialogs are still a source.
|
||||
expect(hooksAvailableForMode('codex')).toBe(false);
|
||||
});
|
||||
|
||||
it('is false for a CLI with neither (shell, gemini, opencode)', () => {
|
||||
for (const mode of ['shell', 'gemini', 'opencode'] as const) {
|
||||
expect(blockedSignalAvailable(mode)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('follows the per-session deepseek bridge, which is its only source', () => {
|
||||
expect(blockedSignalAvailable('deepseek')).toBe(true);
|
||||
expect(blockedSignalAvailable('deepseek', { deepSeekStatusReporting: false })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hooksAvailableForMode', () => {
|
||||
it('is true only for claude', () => {
|
||||
expect(hooksAvailableForMode('claude')).toBe(true);
|
||||
@@ -144,8 +165,22 @@ describe('resolveWaitSignals', () => {
|
||||
const result = resolveWaitSignals('stop', codex);
|
||||
expect(result.until).toEqual([]);
|
||||
expect(result.error).toContain('codex');
|
||||
expect(resolveWaitSignals('blocked', codex).error).toBeTruthy();
|
||||
expect(resolveWaitSignals('idle,stop', codex).error).toBeTruthy();
|
||||
// gemini declares no blockedLine, so nothing can ever raise `blocked` for it.
|
||||
expect(resolveWaitSignals('blocked', { mode: 'gemini' as SessionMode }).error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('accepts `blocked` on codex, whose dialogs are read off the screen, but still not `stop`', () => {
|
||||
// capabilities.workDetect.blockedLine gives codex a source for `blocked`; nothing on a
|
||||
// screen says a turn ended, so `stop` keeps its 400.
|
||||
expect(resolveWaitSignals('blocked', codex)).toEqual({ until: ['blocked'], error: null });
|
||||
expect(resolveWaitSignals('idle,blocked,exit', codex)).toEqual({ until: ['idle', 'blocked', 'exit'], error: null });
|
||||
const stop = resolveWaitSignals('stop,blocked', codex);
|
||||
expect(stop.until).toEqual([]);
|
||||
expect(stop.error).toContain('stop');
|
||||
expect(stop.error).not.toContain('Signal(s) stop, blocked');
|
||||
// The suggestion names what does fire for codex.
|
||||
expect(stop.error).toContain('idle, blocked or exit');
|
||||
});
|
||||
|
||||
it('drops hook-only signals from the DEFAULT set instead of erroring', () => {
|
||||
|
||||
@@ -97,6 +97,24 @@ describe('sendPushNotifications -> webhook (the headless case)', () => {
|
||||
expect(sendNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('carries a screen-read dialog (codex) with its own message, and a hook message stays out', async () => {
|
||||
// web/screen-blocked.ts sends the CLI's own label, built server-side, never the
|
||||
// dialog's text. A Claude hook's `message` keeps the body it always had.
|
||||
const { send } = await headlessServer({});
|
||||
await send('hook:permission_prompt', {
|
||||
sessionId: 's3',
|
||||
sessionName: 'w3-app',
|
||||
source: 'screen',
|
||||
message: 'Codex is waiting for your answer in the terminal',
|
||||
});
|
||||
await send('hook:permission_prompt', {
|
||||
sessionId: 's4',
|
||||
sessionName: 'w4-app',
|
||||
message: 'Claude needs your permission to use Bash',
|
||||
});
|
||||
expect(got.map((g) => g.body)).toEqual(['[w3-app] Codex is waiting for your answer in the terminal', '[w4-app]']);
|
||||
});
|
||||
|
||||
it('dedupes an immediate repeat of the same event for the same session', async () => {
|
||||
const { send } = await headlessServer({});
|
||||
await send('hook:idle_prompt', { sessionId: 's1', sessionName: 'w1-app' });
|
||||
|
||||
Reference in new issue
Block a user