fix(codex): read the model above codex 0.162's new footer hint row

Every codex tile and tab showed no model, unlike claude and deepseek.
Codex reports its model only in the footer under the composer
(`  GPT-6-Luna default · ~/codeman-cases/testcase`), and the registry read
the pane's LAST row for it. Codex 0.162.0 added a hint row under the
footer at rest (`  ← for agents · ? for shortcuts`, or `  ? for
shortcuts`), so the last row was always the hint and `displayModel`
stayed null. Measured live on the 1.36.0 beta: the hint is there at rest
and after a turn, and gone while a prompt is being typed (the footer is
the last row again then).

The codex modelDetect window is now two rows, and the footer must be
either the last row or followed by exactly one more two-space-indented
row. Anchoring to the end of the window keeps the guard the one-row rule
had: with the footer hidden, the last two rows are a transcript line and
the `›` composer, so a footer-shaped line the agent printed is not read.
Tests cover both 0.162 hint variants, the typing layout, the hint never
read as a model, and a forged footer-plus-indented pair above the
composer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-09 03:10:36 +02:00
parent 112c533ac7
commit 3fe5d1b278
2 changed files with 46 additions and 8 deletions
+16 -6
View File
@@ -645,13 +645,23 @@ const CODEX: CliEntry = {
},
// 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,
// cwd). It is the pane's LAST row, below the composer, so the transcript never
// reaches it, and the effort word right after the model is codex's own format: an
// open slash-command popup or a bare line of prose does not have that shape. A
// footer without an effort word (a model with no reasoning setting) is not read,
// and the session keeps its last known or launch model.
// cwd). It sits below the composer, so the transcript never reaches it, and the
// effort word right after the model is codex's own format: an open slash-command
// popup or a bare line of prose does not have that shape. A footer without an effort
// word (a model with no reasoning setting) is not read, and the session keeps its
// last known or launch model.
// ⚠️ It is not always the LAST row. 0.162.0 (measured 2026-10-09) adds a hint row
// under it at rest, ` ← for agents · ? for shortcuts` or ` ? for shortcuts`, and
// drops it again while a prompt is being typed. With a one-row window the footer was
// never seen and every codex tile showed no model. So the window is two rows and the
// footer is either the last one or followed by exactly one more two-space-indented
// row. The `$` (no `m` flag: the end of the window) is what keeps the guard the
// one-row rule had: with the footer hidden, the last two rows are a transcript line
// and the `›` composer, and a forged footer-shaped transcript line is not followed by
// an indented row, so it is not read.
modelDetect: {
screenLine: String.raw`^ {2}([A-Za-z0-9][\w.:/@+-]{0,79}) (?:none|minimal|low|medium|high|xhigh|max|default) · `,
screenLine: String.raw`(?:^|\n) {2}([A-Za-z0-9][\w.:/@+-]{0,79}) (?:none|minimal|low|medium|high|xhigh|max|default) · [^\n]*(?:\n {2}[^\n]*)?$`,
screenLines: 2,
},
// Two columns, like claude's, measured on a live 0.154.0 answer: the `•`/`›`/`⚠`
// markers sit in the gutter, prose continuations sit at 2, and a nested YAML block
+30 -2
View File
@@ -51,8 +51,11 @@ function dshPane(statusLine: string | null, transcript: string[] = []): string {
}
const DSH_LIVE = dshPane(' qwen3.8-27b · medium · th-scratch');
/** The foot of a codex pane: transcript, composer, then the status line on the last row. */
function codexPane(statusLine: string | null, transcript: string[] = []): string {
/**
* The foot of a codex pane: transcript, composer, then the status line, and from 0.162.0
* on a hint row under it at rest (`hint`; absent while a prompt is being typed).
*/
function codexPane(statusLine: string | null, transcript: string[] = [], hint: string | null = null): string {
return [
'│ directory: ~/codeman-cases/th-scratch │',
'│ permissions: YOLO mode │',
@@ -61,7 +64,9 @@ function codexPane(statusLine: string | null, transcript: string[] = []): string
' plan for free – let’s build together.',
...transcript,
'› Explain this codebase',
'',
...(statusLine === null ? [] : [statusLine]),
...(hint === null ? [] : [hint]),
'',
].join('\n');
}
@@ -69,6 +74,8 @@ const CODEX_LIVE = codexPane(' gpt-5.6-terra default · ~/codeman-cases/th-scra
const CODEX_154 = codexPane(
' gpt-5.6-sol medium · Context 98% left · ~/codeman-cases/codex-probe · 5h 99% left · weekly 94% left'
);
/** 0.162.0 at rest, verbatim from the 1.36.0 beta pane whose tile showed no model. */
const CODEX_162 = codexPane(' GPT-6-Luna default · ~/codeman-cases/testcase', [], ' ← for agents · ? for shortcuts');
describe('the registry patterns', () => {
it('compile through compileVersionRegex() with exactly one capture group', () => {
@@ -181,6 +188,17 @@ describe('readScreenModel', () => {
expect(readScreenModel(CODEX_154, CODEX, CODEX_ROWS)).toBe('gpt-5.6-sol');
});
it("reads codex's model above the 0.162.0 hint row, and with the hint gone while typing", () => {
expect(readScreenModel(CODEX_162, CODEX, CODEX_ROWS)).toBe('GPT-6-Luna');
const shortHint = codexPane(' GPT-6-Luna default · ~/codeman-cases/testcase', [], ' ? for shortcuts');
expect(readScreenModel(shortHint, CODEX, CODEX_ROWS)).toBe('GPT-6-Luna');
// Typing hides the hint, which puts the footer back on the last row.
const typing = codexPane(' GPT-6-Luna default · ~/codeman-cases/testcase');
expect(readScreenModel(typing, CODEX, CODEX_ROWS)).toBe('GPT-6-Luna');
// Neither hint row is ever read as a model.
expect(readScreenModel(codexPane(null, [], ' ← for agents · ? for shortcuts'), CODEX, CODEX_ROWS)).toBeUndefined();
});
it('never takes a transcript line shaped like the footer', () => {
// The agent printed a line exactly like each CLI's footer, and the real footer is
// hidden (a dsh status bar switched off; a codex popup over its last row). The
@@ -200,6 +218,16 @@ describe('readScreenModel', () => {
' evil-model high · ~/codeman-cases/th-scratch',
]);
expect(readScreenModel(codexBoth, CODEX, CODEX_ROWS)).toBe('gpt-5.6-terra');
// The two-row window must not open the door either: a forged line followed by an
// indented transcript row still has the `›` composer under it, so it is never read.
const codexForgedPair = codexPane(null, [' evil-model high · ~/codeman-cases/th-scratch', ' ? for shortcuts']);
expect(readScreenModel(codexForgedPair, CODEX, CODEX_ROWS)).toBeUndefined();
const codexBoth162 = codexPane(
' GPT-6-Luna default · ~/codeman-cases/testcase',
[' evil-model high · ~/codeman-cases/th-scratch'],
' ? for shortcuts'
);
expect(readScreenModel(codexBoth162, CODEX, CODEX_ROWS)).toBe('GPT-6-Luna');
});
it('does not read a popup under the composer as a model', () => {