mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(codex): read Codex's own background-terminal row
Codex states background work too, and it says so in a different place. Claude writes `· 1 monitor ·` on the last row of the screen; Codex pins `1 background terminal running · /ps to view · /stop to close` ABOVE its composer, which puts that row third from the bottom once the status line and the composer are counted. So how far up the screen to look is now per-CLI data as well: `capabilities.workDetect.watchingLines`, bounded to 1..8 by the schema, and defaulting to Claude's two. That bound is the point. The window is half the injection guard, since every row it adds is another row the agent itself may be able to write, and the label is what silences an idle alert. The other half is the anchor, and Codex's is ` · /ps to view`: chrome naming a slash command only the CLI can offer, so a session that writes "I left 1 background terminal running for you" into its own output matches nothing. Measured against a live codex-cli 0.154.0 pane rather than read out of a binary. The row appears when the terminal starts, follows the composer down as the conversation grows, and is gone after `/stop`. Verified end to end on an isolated beta: the session payload carried `watching: "1 background terminal"` and the badge rendered with it, and both cleared when the terminal stopped. The fixtures in the tests are that capture verbatim. Codex has no hook signals, so no idle prompt and no false NEEDS YOU row: for a Codex session this is the badge alone, which is the case the maintainer said a registry field could cover and a hook never could. Cross-CLI tests pin that neither pattern fires on the other's screen, and that a CLI declaring nothing still reports nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
74884a20eb
commit
64c288a683
@@ -318,6 +318,9 @@ const capabilitiesSchema = z
|
||||
'watchingLine must be a regex compileVersionRegex() accepts: at most 200 characters, no nested quantifiers'
|
||||
)
|
||||
.optional(),
|
||||
// Bounded hard: this is how far up the screen a config file may push the search,
|
||||
// and every row it adds is one more row the agent itself may be able to write.
|
||||
watchingLines: z.number().int().min(1).max(8).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
|
||||
@@ -530,7 +530,21 @@ const CODEX: CliEntry = {
|
||||
// `Working (2m 49s • esc to interrupt)` above it while a turn runs. It animates no
|
||||
// braille spinner, and it never prints `esc to interrupt` at rest, so that phrase
|
||||
// alone separates a running turn from an idle one.
|
||||
workDetect: { promptGlyph: '›', workingLine: '[Ee]sc to interrupt' },
|
||||
// Codex pins a row of its own while a background terminal it started is still
|
||||
// running: ` 1 background terminal running · /ps to view · /stop to close`. Unlike
|
||||
// Claude's footer chip that row sits ABOVE the composer, which puts it third from the
|
||||
// bottom once the status line and the composer are counted, hence `watchingLines`.
|
||||
// The ` · /ps to view` tail is the anchor: it is CLI chrome, it names a slash command
|
||||
// that only the CLI can offer, and without it a bare count in the transcript would do.
|
||||
// Measured against a live codex-cli 0.154.0 pane on 2026-09-22: the row appears when
|
||||
// the terminal starts, follows the composer down as the conversation grows, and is
|
||||
// gone after `/stop`.
|
||||
workDetect: {
|
||||
promptGlyph: '›',
|
||||
workingLine: '[Ee]sc to interrupt',
|
||||
watchingLine: String.raw`(\d+ background terminals?) running · /ps to view`,
|
||||
watchingLines: 4,
|
||||
},
|
||||
transcript: 'codex-rollout',
|
||||
altScreen: 'strip-full',
|
||||
echo: { policy: 'predict', anchor: { kind: 'cursor' }, predictProfile: 'codex' },
|
||||
|
||||
@@ -345,13 +345,23 @@ export interface CliCapabilities {
|
||||
/** Source of a regex matching the status line this CLI draws while a turn runs. */
|
||||
workingLine: string;
|
||||
/**
|
||||
* Source of a regex matching the chip this CLI draws at the foot of its screen while
|
||||
* work it started in the background is still running, e.g. Claude's `· 1 monitor ·`.
|
||||
* Capture group 1 is the label Codeman shows, and the whole match stands in when the
|
||||
* pattern declares no group. A CLI that omits this reports no background work, which
|
||||
* is what every CLI did before the field existed.
|
||||
* Source of a regex matching the row this CLI draws while work it started in the
|
||||
* background is still running, e.g. Claude's `· 1 monitor ·` footer chip or Codex's
|
||||
* `1 background terminal running · /ps to view`. Capture group 1 is the label Codeman
|
||||
* shows, and the whole match stands in when the pattern declares no group. A CLI that
|
||||
* omits this reports no background work, which is what every CLI did before the field
|
||||
* existed.
|
||||
*/
|
||||
watchingLine?: string;
|
||||
/**
|
||||
* How many rows at the FOOT of the screen that row can appear in, counting non-blank
|
||||
* rows only. Claude writes its chip on the last row and keeps the default; Codex pins
|
||||
* its own above the composer, which puts it third from the bottom, so it declares
|
||||
* more. Keep each number as small as that CLI's layout allows: every extra row is
|
||||
* another row an agent might be able to write, and the label is what silences an
|
||||
* alert. See `watchingLabel()` in `session-activity.ts`.
|
||||
*/
|
||||
watchingLines?: number;
|
||||
};
|
||||
/** No direct-PTY fallback: the CLI must run inside tmux (secrets ride tmux setenv). */
|
||||
requiresMux: boolean;
|
||||
|
||||
+23
-14
@@ -95,18 +95,20 @@ export function isPaneQuiet(lastActivityAt: number, now: number, silenceMs: numb
|
||||
}
|
||||
|
||||
/**
|
||||
* How many lines at the foot of a pane capture may hold the background-work chip.
|
||||
* How many rows at the foot of a pane capture may hold the background-work row, for a
|
||||
* CLI that declares no number of its own (`capabilities.workDetect.watchingLines`).
|
||||
*
|
||||
* Claude Code draws that chip on the last row of the screen. The row above it is the
|
||||
* status line, which a user's own `statusLine` command writes, and two lines is what
|
||||
* Claude Code draws its chip on the LAST row of the screen. The row above it is the
|
||||
* status line, which a user's own `statusLine` command writes, and two rows is what
|
||||
* covers the chip wherever a trailing blank or a one-line notice pushes it up by one.
|
||||
* Codex pins its row above the composer instead and declares four.
|
||||
*
|
||||
* ⚠️ The ceiling is the security boundary, not a tidiness measure. The label is
|
||||
* PANE-DERIVED, so everything on that screen above the footer is text an agent wrote
|
||||
* itself, and an agent that printed `· 1 monitor ·` into its own output would silence
|
||||
* its own idle alert. Keep the window at the footer, keep each CLI's pattern anchored
|
||||
* on the separator its footer actually uses, and never widen this to a whole-pane
|
||||
* search.
|
||||
* PANE-DERIVED, and everything on that screen above the CLI's own chrome is text the
|
||||
* agent wrote itself, so an agent that printed `· 1 monitor ·` into its output would
|
||||
* silence its own idle alert. Keep each CLI's window as small as its layout allows,
|
||||
* keep its pattern anchored on chrome only that CLI can draw, and never widen either to
|
||||
* a whole-pane search.
|
||||
*/
|
||||
export const WATCHING_TAIL_LINES = 2;
|
||||
|
||||
@@ -122,20 +124,27 @@ export const MAX_WATCHING_LABEL_CHARS = 40;
|
||||
* entry (`capabilities.workDetect.watchingLine`); group 1 is the label when the pattern
|
||||
* declares one, and the whole match stands in when it does not.
|
||||
*
|
||||
* Each candidate line is tested on its own, bottom row first, so a pattern can anchor
|
||||
* itself with `^` or `$` against a single row rather than against a joined block. The
|
||||
* answer is stripped of ANSI and capped, because it ends up on a badge and in an
|
||||
* approval card.
|
||||
* Each candidate row is tested on its own, bottom row first, so a pattern can anchor
|
||||
* itself with `^` or `$` against a single row rather than against a joined block. Blank
|
||||
* rows are dropped before the window is taken, because a CLI that leaves a blank line
|
||||
* between its chrome rows would otherwise spend the window on nothing. The answer is
|
||||
* stripped of ANSI and capped, because it ends up on a badge and in an approval card.
|
||||
*
|
||||
* @param tailLines how many non-blank rows from the bottom to look at, defaulting to
|
||||
* `WATCHING_TAIL_LINES`; a CLI declares its own when its row is not the last one
|
||||
* @returns the label, or null when the pane shows no background work
|
||||
*/
|
||||
export function watchingLabel(paneText: string | null | undefined, pattern: RegExp): string | null {
|
||||
export function watchingLabel(
|
||||
paneText: string | null | undefined,
|
||||
pattern: RegExp,
|
||||
tailLines: number = WATCHING_TAIL_LINES
|
||||
): string | null {
|
||||
if (!paneText) return null;
|
||||
const lines = stripAnsi(paneText)
|
||||
.split('\n')
|
||||
.map((line) => line.trimEnd())
|
||||
.filter((line) => line !== '');
|
||||
for (const line of lines.slice(-WATCHING_TAIL_LINES).reverse()) {
|
||||
for (const line of lines.slice(-Math.max(1, tailLines)).reverse()) {
|
||||
// A pattern compiled by compileVersionRegex() never carries the `g` flag, but a
|
||||
// caller reaching in from a test or a config reload might, and a stale lastIndex
|
||||
// would make the same screen match every other call.
|
||||
|
||||
+3
-1
@@ -2760,7 +2760,9 @@ export class Session extends EventEmitter {
|
||||
// Called only from the probe, and only with what a capture returned: `null` is
|
||||
// "the screen could not be read", which is not evidence that nothing is running.
|
||||
if (!pattern || paneText === null) return;
|
||||
this._watching = watchingLabel(paneText, pattern);
|
||||
// How far up the screen this CLI's row can sit is its own business: Claude writes on
|
||||
// the last row, Codex pins one above its composer. Both stay at the foot.
|
||||
this._watching = watchingLabel(paneText, pattern, getCli(this.mode)?.capabilities.workDetect?.watchingLines);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user