Skip to content

feat(composer): live, attribute-aware composer state (agent-code#800, #1313) - #54

Merged
Juliusolsson05 merged 4 commits into
mainfrom
fix/composer-occupied-0157
Sep 25, 2026
Merged

Juliusolsson05 merged 4 commits into
mainfrom
fix/composer-occupied-0157

Conversation

@Juliusolsson05

@Juliusolsson05 Juliusolsson05 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Refs Juliusolsson05/agent-code#800 and Juliusolsson05/agent-code#1313. The app side follows in agent-code once this merges.

Plan: docs/plans/2026-09-25-composer-state-0157.md (first commit).

Evidence

testing/fixtures/composer-0157/idle-draft-ctrlc.json is a raw PTY recording of codex-cli 0.157.0 at 100×30: idle start, a typed draft with no Enter and no model request, then Ctrl+C twice. The home directory name is redacted with a same-length string.

  • Empty composer: › Ask Codex to do anything with the placeholder dim (\x1b[2m), then a blank row, the status row and a second footer row (hints or warnings).
  • Typed draft: › please review the draft in plain cells.
  • Ctrl+C: clears the draft, and the dim placeholder returns.
  • Replayed through HeadlessTerminal, classifyCodex01491ComposerSurface returns unknown for both frames, because it requires exactly one footer row. The app's text-only empty check refuses the empty composer (agent-code#1313), and nothing reports a draft (agent-code#800).

Change

  • HeadlessTerminal.snapshotComposerCells(): viewport rows with each non-blank cell's dim flag, from the live buffer. It returns null while bytes are still parsing.
  • classifyCodexComposerState(rows) returns empty, drafted or unknown (revised after review round 1):
    • Anchor: › row, then a blank row, then the footer: the status row (or, during a turn, the queue row), plus at most one more footer row. Anything else is unknown.
    • drafted: plain cells after the marker, an [Image #n] label on the composer rows, or Codex's "tab to queue message" hint.
    • empty: no plain cells and Codex's own ? for shortcuts hint.
    • Hints are read only where Codex puts them (review round 2): at the start of a hint row below the status row, or as the queue row itself. The status row carries the user-controlled cwd and is never searched. A folder named for shortcuts or to queue message used to forge empty or drafted. Codex shows that hint only in FooterMode::ComposerEmpty, and its is_empty() counts attachments and bash mode (vendor/codex-src/…/footer.rs:224-231). Dim cells alone are not enough: the quit frame's › Shutting down... is dim, and an image above the textarea keeps the dim placeholder.
    • A Vim atom on either footer row means unknown.
  • CodexHeadless.getComposerState() wires the two together. Both are exported from the index.
  • The 0.149.1 classifier and PromptInputEvidence are untouched; they stay version-gated.

For the app side (agent-code)

Only empty is consent, and only drafted is occupancy. unknown must not map to ready: Codex readiness latches after startup (markComposerReady), so an occupancy reason needs its own publication path (#54 review C).

Tests

ComposerState.recorded.test.ts (10 tests):

  • Replay of the recording through the real terminal:
    • idle reads empty;
    • the typed draft reads drafted;
    • after Ctrl+C it reads empty again;
    • the › Shutting down... quit frame (cut at that exact paint) is never empty.
  • Empty only on Codex's hint: a bare marker plus the hint reads empty; with no hint it reads unknown.
  • Images: an image label above the textarea is never empty; a dim image label on the composer row is drafted.
  • The running-turn queue footer: the recorded 0.149.1 active-footer-tab-queue frame reads drafted, and so does a dim row under the queue hint.
  • Continuation rows: a draft only on a continuation row is drafted. The composer bound is 12 continuation rows (drafted); 13 reads unknown.
  • Not a composer: a transcript › row, a markerless pane, and Vim on either footer row all read unknown.
  • The cwd never supplies a hint: ~/for shortcuts and ~/to queue message in the status row. These tests were red on round 2's head.
  • Mutation-checked: each rule above fails at least one test when removed. That covers dim, the hint requirement, the queue row and queue hint, Vim on both rows, the marker guard, the bound, continuation rows and the image guard.

The full suite passes (231 tests), and tsc --noEmit is clean. The fixture's home directory and session id are redacted with same-length values.

🤖 Generated with Claude Code

Juliusolsson05 and others added 4 commits September 25, 2026 09:03
…, #1313)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… #1313)

Codex 0.157 paints its empty-composer placeholder dim and a typed draft
plain, under a two-row footer the 0.149.1 classifier rejects. getComposerState
reads the live buffer's dim flags inside an anchored bottom pane: empty,
drafted, or unknown when the pane is not provably the composer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ue footer, images, Vim on either row

Dim cells alone called the quit frame (`› Shutting down...`, dim) and an
image attached above the textarea empty. Codex shows `? for shortcuts` only
in ComposerEmpty, and its is_empty counts attachments, so `empty` now needs
that hint. Drafts under the running-turn queue footer read as drafted. Tests
pin the quit frame, continuation rows, a markerless pane, the row bound and
Vim on the second footer row. The fixture's session id is redacted too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, never from the cwd

The status row carries the cwd, which is user-controlled: a folder named
`for shortcuts` forged `empty` over an attached image or the quit frame, and
one named `to queue message` forged `drafted`. The shortcuts and queue hints
are now matched only at the start of a hint row (or the queue row itself).
The quit test cuts at the `› Shutting down...` frame itself, and the comment
says `unknown` is neither ready nor occupied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Final disposition (pre-merge), head 03c64f3. Three reviewers: A (correctness, Codex), B (tests, Codex), C (evidence, Grok). Two rounds each; no round 3.

Reviewer Round 1 Round 2 (at 0701918)
A FIX-BEFORE-MERGE FIX-BEFORE-MERGE
B FIX-BEFORE-MERGE FIX-BEFORE-MERGE
C FIX-BEFORE-MERGE FIX-BEFORE-MERGE

Round 1 → fixed in 0701918:

  • A1 blocker / C: an image above the textarea read as empty; so did the dim quit frame. empty now requires Codex's own ? for shortcuts hint, which Codex shows only in FooterMode::ComposerEmpty (is_empty() counts attachments).
  • A2: a draft under the running-turn queue footer read as unknown. The queue row now counts as the footer head; tested with the recorded 0.149.1 active-footer-tab-queue frame.
  • A3 / B: continuation rows, a markerless pane, and the row bound (12 continuation rows reads drafted, 13 reads unknown). All tested.
  • B3: a session id left in the fixture. Redacted with a same-length value.
  • C: Vim on the second footer row. Checked on both footer rows.

Round 2 → fixed in 03c64f3:

  • A1 / B1 blocker, C major: the hints were matched anywhere in the footer, including the status row's user-controlled cwd. A folder named for shortcuts forged empty over an image or the quit frame, and to queue message forged drafted. Hints now match only at the start of a hint row (or the queue row), and the status row is never searched. The reviewers' exact probes are a test, red on 0701918 and green here.
  • B3 minor: the quit test cut after the screen was cleared. It now cuts at the › Shutting down... paint and asserts that frame is on screen.
  • C major: the comment told callers unknown is not a refusal, which invites mapping it to ready. It now says unknown maps to neither ready nor occupied; only empty is consent and only drafted is occupancy.

Residuals, stated in the PR body for the app side:

  • The evidence is one 0.157 recording at 100×30 (idle, draft, Ctrl+C, quit) plus upstream source.
  • Narrow panes where Codex drops the hint read unknown, the safe direction.
  • Codex readiness latches after startup, so the app needs its own publication path for occupancy.

The full suite passes (231 tests), and tsc --noEmit is clean.

🤖 Generated with Claude Code

@Juliusolsson05
Juliusolsson05 merged commit 1ac3c50 into main Sep 25, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant