Skip to content

fix(ask-user-question): stop the active-row marker shifting the preview box - #2541

Merged
flora131 merged 1 commit into
mainfrom
fix/askq-preview-active-row-align
Aug 19, 2026
Merged

fix(ask-user-question): stop the active-row marker shifting the preview box#2541
flora131 merged 1 commit into
mainfrom
fix/askq-preview-active-row-align

Conversation

@flora131

@flora131 flora131 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

The ask_user_question preview box drew its border 11-13 columns early on the active option row only, in side-by-side layout. OVERLAY_ACTIVE_ROW_MARKER was an APC string terminated by BEL; ECMA-48 permits BEL to terminate OSC but not APC, so tmux kept consuming the row padding and the preview column gap as APC payload and resumed drawing at the box border's SGR.

Two layers, per the reviewed plan:

  1. Terminate the marker with ST (ESC \) at its definition, as the spec requires.
  2. Strip the marker centrally in applyLineResets on both interactive renderers — pi-tui's last transform over the composited screen, and where it already removes its own CURSOR_MARKER. Every component tree either renderer paints converges there, so overlay, inline, widget, and workflows stage-chat mounts are covered, including hosts that never call ReservedBottomOverlay.takeActiveRow (previously the only stripper in the tree, and the reason the defect escaped on the stage-chat path).

Changes

path change
packages/coding-agent/src/core/extensions/ui-types.ts ST terminator on OVERLAY_ACTIVE_ROW_MARKER; new stripOverlayActiveRowMarker helper
packages/coding-agent/src/modes/interactive/interactive-tui.ts applyLineResets overrides on AtomicTuiAltScreen and a new AtomicTuiMainScreen; main-screen construction site repointed
packages/coding-agent/test/overlay-active-row-marker-strip.test.ts new — drives both renderer classes against a recording terminal (3 tests)
test/unit/ask-user-question-preview-pane.test.ts new — first regression tests covering PreviewPane (12 tests)
packages/coding-agent/CHANGELOG.md one bullet appended to the existing ### Fixed subsection under ## [Unreleased]
packages/coding-agent/docs/extensions.md marker documentation updated to state the ST terminator and the central strip

Six files, +388 / −5. preview-pane.ts, wrapping-select.ts, and reserved-bottom-overlay.ts are deliberately untouched.

Validation

  • npm run check — clean (biome, tsc --noEmit, tsgo -p tsconfig.build.json --noEmit, shrinkwrap up to date).
  • npm run test:unit — 685 files, 6756 passed / 2 skipped.
  • npm run test --workspace=@bastani/atomic — 475 files, 3911 passed / 39 skipped.
  • Red-green: with both production files reverted to origin/main, the renderer test is 3 failed / 3 and the PreviewPane test 6 failed / 6; restoring the fix returns both to green.
  • Real tmux + real CLI, 140x44, tmux 3.7b: box-border column measured [33,33,33,33,33,33] after the fix for option 1, option 2, and option 1 again; before the fix [20,33,...] and [33,33,22,...]. Full captures below.

Notes for the reviewer

  • OVERLAY_ACTIVE_ROW_MARKER is publicly re-exported and its value changed, but every in-repo consumer imports the constant rather than the literal, and the docs instruct extensions to embed the constant — so this is fix:, not fix!:.
  • pi-tui's own CURSOR_MARKER is still BEL-terminated (dist/tui.js:21). It does not leak today because pi-tui strips it centrally, but it is the same latent class. Upstream matter.
  • TMUX-EVIDENCE.md is intentionally not committed — it is evidence, not shipped content, and AGENTS.md keeps scratch artifacts out of the tree. It is carried verbatim in this body instead.
  • No QA end-to-end video: this is a terminal/TUI scenario with no web frontend, so the objective-mandated tmux pane capture is the equivalent proof.

Implementation notes

Implementation Notes — ask_user_question preview box misaligns on the active option row

Worktree: /Users/tonystark/Documents/projects/atomic-preview-active-row-align
Branch: fix/askq-preview-active-row-align (already checked out at stage start; base commit 3646705921, never main)

Contract amendments received

Amendment A — inherited from loop 1 (user-authored, verbatim):

"Scope the fix to EVERY host path that paints this dialog, not only the one you reproduced. takeActiveRow in reserved-bottom-overlay.ts is currently the sole stripper of OVERLAY_ACTIVE_ROW_MARKER; your capture's footer ("ctrl+x return to graph") shows the frame painted through a path that never calls it. Enumerate the paint paths (interactive reserved-bottom overlay, the workflows overlay adapter in packages/workflows/src/tui/, print/SDK, tui-alt-screen) and state for each whether the marker can reach the terminal.
Prefer defense in depth over patching one adapter: correct the APC termination at the marker definition (ECMA-48 requires ST, ESC-backslash, not BEL) AND guarantee a single central strip before paint. Do not fix only the reproduced adapter and declare it done.
Verify the fix in the SAME real tmux + real atomic CLI scenario, not only in unit tests."

Implemented as required behavior in loop 1: both layers landed (ST terminator at the definition; central strip in applyLineResets on both renderers), and the fix was verified in the same real tmux + real CLI scenario.

Amendment B — received for loop 2 (user-authored, verbatim):

"The ONLY outstanding defect is delivery state: the working tree is uncommitted. Loop 2 is a commit-and-deliver loop, not a redesign loop.
Do NOT modify, revisit, re-derive, or "improve" any of: the fix in ui-types.ts or interactive-tui.ts, the two new test files, TMUX-EVIDENCE.md, the CHANGELOG entry, or the docs/extensions.md edit. They are reviewed and accepted. Re-opening settled code is drift.
Required work, in order:

  1. Commit the reviewed delta on branch fix/askq-preview-active-row-align — the 4 modified files plus the 2 new test files. Conventional commit message.
  2. Do NOT commit TMUX-EVIDENCE.md into the repository. It is evidence, not shipped content, and AGENTS.md requires scratch artifacts stay out of the tree. Keep it in the worktree as the source text for the pull request body only.
  3. Confirm npm run check still passes on the committed state, then let the PR stage carry the TMUX-EVIDENCE.md content verbatim into the pull request body.
    Do not re-run the full test suites; two reviewers already did."

Amendment B was executed literally in loop 2. Nothing under its "do not modify" list was touched: git diff origin/main for the four source/docs files is byte-identical to the reviewed +41/−5, and both new test files were committed unchanged.

Amendment B vs. Amendment B's own step 3 and the "do not re-run the suites" clause — resolved. The repository's prek pre-commit hook runs npm run check and npm run test:unit and cannot be skipped without --no-verify, which the gh-commit skill forbids in capitals. Those runs are mandated, not discretionary, so committing without --no-verify honours both clauses at once. Recorded rather than treated as a conflict.

No new user steering arrived during loop 2 beyond Amendment B.

Acceptance matrix

# Contract clause Verification Status
1 Fix implemented in the designated worktree, on fix/askq-preview-active-row-align, never main git branch --show-currentfix/askq-preview-active-row-align; all edits under that path pass
2 Root cause identified from a REAL tmux capture of a REAL running atomic CLI tmux pipe-pane -o on a live CLI: APC atomic:active occurrences: 2 | BEL-term: 2 | ST-term: 0, mark mid-line followed by 13 pad+gap spaces pass
3 Before- and after-fix tmux capture-pane -p output stored verbatim in TMUX-EVIDENCE.md in the worktree TMUX-EVIDENCE.md present at 216 lines / 15 324 bytes, 16 balanced fence lines, both signature capture lines found by grep -F. Deliberately untracked per Amendment B: the criterion says in the worktree, not committed pass
4 After-fix capture shows the box aligned identically on every row, for every selection Machine-measured border column: after = [33,33,33,33,33,33] for both selections; before = [20,33,…] / [33,33,22,…] pass
5 Regression tests covering PreviewPane exist and pass; there were none before test/unit/ask-user-question-preview-pane.test.ts (12 tests) + packages/coding-agent/test/overlay-active-row-marker-strip.test.ts (3 tests) pass
6 npm run check passes; the vitest suites covering the touched code pass npm run check clean; npm run test:unit 685 files / 6756 passed; coding-agent suite 475 files / 3911 passed pass
7 ## [Unreleased] entry in packages/coding-agent/CHANGELOG.md appended to the existing ### Fixed subsection, no duplicate heading pass
8 No scratch or repro harness files remain in the tree Post-commit git status --porcelain --untracked-files=all → exactly ?? TMUX-EVIDENCE.md. Sweep verified clean: no packages/workflows/dist, no tsconfig.build.json, no bun.lock/yarn.lock/pnpm-lock.yaml, no issues.md; harness lives in /tmp/askq-preview-repro pass
9 User-facing docs describing this behavior stay accurate packages/coding-agent/docs/extensions.md:2832 rewritten pass
10 Reviewed delta committed on the branch, six paths, evidence file excluded (Amendment B 1-2) git log --oneline origin/main..HEAD → one commit 192a9b8fc3; git show --stat HEAD → 6 files, +388/−5; git ls-files TMUX-EVIDENCE.md → empty pass
11 npm run check passes on the committed state (Amendment B 3) re-run after the commit: biome clean, tsc --noEmit clean, tsgo -p tsconfig.build.json --noEmit clean, shrinkwrap up to date, exit 0 pass
PR body carries the evidence verbatim out of this stage's scope (Ignore requests to submit a PR); the branch is committed but not pushed, and TMUX-EVIDENCE.md is the body's source text deferred to the authorized final action

Root cause — confirmed first-hand, not inherited

Reproduced live at tmux 3.7b, session 140×44, atomic from worktree source. tmux pipe-pane -o over the painted frames:

\x1b[31;1H\x1b[2K\x1b[0m\x1b]8;;\x07\x1b[38;2;138;190;183m❯ 1. Encrypt at rest\x1b[39m\x1b_atomic:active\x07             \x1b[38;2;138;190;183m┌──────…

OVERLAY_ACTIVE_ROW_MARKER was "\u001B_atomic:active\u0007" — an APC string terminated by BEL. ECMA-48 allows BEL for OSC but not APC, so tmux kept consuming the pad and the column gap as payload and resumed drawing at the border's SGR. The shift equals leftWidth − visibleWidth(leftClamped) + PREVIEW_COLUMN_GAP + PREVIEW_PADDING_LEFT: 13 columns for option 1, 11 for option 2, both matching the captures exactly.

PreviewPane.renderSideBySide is what buries the mark mid-line (WrappingSelect appends it at end-of-line; the join glues the preview column behind it), which is why only side-by-side layout is affected.

The frame that leaked was painted through the workflows stage-chat custom-UI slot (footer ctrl+x return to graph), a path that never calls ReservedBottomOverlay.takeActiveRow — the repository's only stripper before this change.

Fix

Two layers, three edits, no restyle of surrounding code.

  1. packages/coding-agent/src/core/extensions/ui-types.ts — terminator changed to ST: "\u001B_atomic:active\u001B\\". Safe because pi-tui's extractAnsiCode already accepts APC terminated by BEL or ST (dist/utils.js:382-394); verified at runtime that visibleWidth is still 0 and truncateToWidth still preserves the mark.
  2. Same file — stripOverlayActiveRowMarker(lines), one definition of the removal.
  3. packages/coding-agent/src/modes/interactive/interactive-tui.tsapplyLineResets overridden on AtomicTuiAltScreen and on a new AtomicTuiMainScreen, with the main-screen construction site repointed. That seam is pi-tui's last transform over the composited screen before it builds the byte buffer, and is where pi-tui already removes its own CURSOR_MARKER, so it covers every component tree either renderer paints.

ReservedBottomOverlay.takeActiveRow was deliberately left alone: it needs the mark to pick the crop window and runs on the component tree long before this seam.

preview-pane.ts was deliberately not modified. Hoisting the mark to end-of-line there is redundant once the central strip lands, and editing those lines risks exactly the restyle the objective forbids.

Validation

command outcome
npm run check pass (biome, tsc --noEmit, tsgo -p tsconfig.build.json --noEmit, shrinkwrap up to date)
npm run test:unit 685 files passed, 6756 passed / 2 skipped
npm run test --workspace=@bastani/atomic 475 files passed / 4 skipped, 3911 passed / 39 skipped
npx vitest --run --project unit test/unit/ask-user-question-preview-pane.test.ts 12 passed
npx vitest --run test/overlay-active-row-marker-strip.test.ts (coding-agent) 3 passed
npx vitest --run test/ask-user-question-transcript-scroll.test.ts (owns the mark / boundOverlayLines contract) 76 passed

Red-green proof. With interactive-tui.ts reverted to HEAD and everything else in place, test/overlay-active-row-marker-strip.test.ts reports 2 failed / 1 passed — both renderer tests fail on written.includes(OVERLAY_ACTIVE_ROW_MARKER) === false. Restoring the file returns 3 passed.

Honest limit on the PreviewPane column tests. The pane's own arithmetic was already self-consistent before the fix, so the column-invariance assertions in ask-user-question-preview-pane.test.ts do not fail pre-fix — they pin the invariant the fix must not break. The tests that genuinely fail pre-fix are the two renderer-level ones plus the terminator-shape assertion.

Loop 2 — delivery (commit only, no push, no PR)

No file under Amendment B's "do not modify" list was touched. git diff origin/main --stat before staging was byte-identical to the reviewed delta: 4 files, +41 / −5.

Staged by explicit path, never git add -A / git add . / git commit -a, because TMUX-EVIDENCE.md is untracked and not gitignored (git check-ignore exits 1) and a blanket stage would sweep it in. git diff --cached --name-only confirmed six paths and zero matches for TMUX-EVIDENCE.md before committing.

Commit: 192a9b8fc34d776e17e07f8bdce25f5e4a4f61f8fix(ask-user-question): stop the active-row marker shifting the preview box, with the Assistant-model: Claude Opus 5 trailer. Six files, +388 / −5 (the +347 over the source delta is the two new test files).

--no-verify was not used. The prek pre-commit hooks ran and all passed:

check for added large files..............................................Passed
check for case conflicts.................................................Passed
check for merge conflicts................................................Passed
detect private key.......................................................Passed
biome check --write......................................................Passed
npm run check............................................................Passed
npm run test:unit........................................................Passed

The biome check --write hook made no rewrite, so there was no re-stage/retry loop.

Post-commit verification:

check result
git branch --show-current fix/askq-preview-active-row-align
git log --oneline origin/main..HEAD exactly one commit, 192a9b8fc3
git show --stat HEAD 6 files, +388 / −5; two create mode lines for the new tests
git status --porcelain --untracked-files=all exactly ?? TMUX-EVIDENCE.md — as §4 of the research predicts, not empty, and correct under Amendment B
npm run check on the committed tree exit 0
git ls-files TMUX-EVIDENCE.md empty — the evidence file is not tracked
TMUX-EVIDENCE.md integrity 216 lines / 15 324 bytes, 16 balanced fence lines, ❯ 1. Encrypt at rest┌ (BEFORE) and ❯ 1. Encrypt at rest ┌ (AFTER) both present

Stopped before push and PR, deliberately. The launch contract says "Ignore requests to submit a PR; the authorized final action handles that after approval." git push -u origin is an external write and the first half of PR submission, so the branch is committed locally and not pushed. The research's checklist steps 7-9 belong to the PR stage. Consequently the pre-push hooks (test-integration, test-ci-contracts) have not run in this session.

Judgement call recorded, not re-opened: OVERLAY_ACTIVE_ROW_MARKER is publicly re-exported and its value changed, but every in-repo consumer imports the constant rather than the literal and the docs instruct extensions to embed the constant, so the commit is fix: rather than fix!:. Worth one sentence in the PR body.

Deferred / out of scope

  • pi-tui's own CURSOR_MARKER is still BEL-terminated (dist/tui.js:21). pi-tui strips it centrally so it does not leak today, but it is the same latent class. Upstream matter; worth a line in the PR.
  • If a later stage runs further live tmux verification, list and stop stray atomic / bun … cli.ts processes from other worktrees first. A reviewer's first E2E attempt was contaminated by them and showed legacy BEL output until they were stopped.
  • Branch push and PR creation are deferred to the authorized final action. The research artifact's §6 carries the exact gh pr create --body-file recipe and the round-trip greps that close criterion 9.
  • Paint paths 3, 4, 5, and 6-with-reserveTranscriptRows:false (ordinary overlay, inline mount, widget, engine overlay branch) were not exercised end-to-end. The central strip covers them structurally because it sits downstream of the whole component tree; that structural claim is proven by capture for the workflows stage-chat path and by unit test for both renderer classes.
  • The original reporter's terminal is unidentified. The mechanism is proven in tmux 3.7b; a terminal that ends APC on BEL would never have shown the shift.

QA E2E Video

Not applicable: this is a terminal/TUI scenario with no web frontend, so the QA evidence is the tmux pane capture required by the objective rather than a playwright-cli .webm. playwright-cli was not invoked and no video was produced. The equivalent proof lives in /Users/tonystark/Documents/projects/atomic-preview-active-row-align/TMUX-EVIDENCE.md, which carries before- and after-fix tmux capture-pane -p output plus the raw tmux pipe-pane byte counts from a real interactive atomic CLI at 140×44, driven through /workflow ask-user-question-graph-overlayF2Enter and moved between both options.

Reproduction harness (outside the tree, by design)

/tmp/askq-preview-repro/ — stand-in openai-responses model server, agent config, the project workflow fixture, and every capture. Nothing was written into the worktree, so "no scratch files" holds by construction.

bun /tmp/askq-preview-repro/model-server.ts /tmp/askq-preview-repro/state
tmux new-session -d -s askqprev -x 140 -y 44 -c /tmp/askq-preview-repro/project
# in the pane:
cd /tmp/askq-preview-repro/project && NODE_ENV=production \
  ATOMIC_CODING_AGENT_DIR=/tmp/askq-preview-repro/agent ATOMIC_SKIP_VERSION_CHECK=1 \
  bun <worktree>/packages/coding-agent/src/cli.ts --approve --offline --no-session
# then: /workflow ask-user-question-graph-overlay → ↵ → F2 → ↵ → ↓ / ↑
tmux capture-pane -p -t askqprev

NODE_ENV=production is load-bearing: under a test runner the workflows extension hands stages a stub session that never calls a tool. A fresh worktree also needs packages/natives/native/atomic_natives.<platform>.node (gitignored, built only by test/global-setup-natives.ts); it was already present in this worktree.


TMUX evidence — ask_user_question preview box misaligned on the active option row

Verbatim tmux capture-pane -p output from a real interactive atomic CLI, before and after the fix.

Environment (identical for every capture)

  • tmux 3.7b, TERM=tmux-256color, session askqprev, geometry 140 columns x 44 rows
  • atomic built from this worktree's source (packages/coding-agent/src/cli.ts), version 0.0.0 (versionless release base), base commit 3646705
  • Scenario: real interactive CLI -> /workflow ask-user-question-graph-overlay -> F2 (graph viewer) -> Enter (stage chat) -> the stage's real ask_user_question dialog, whose two options carry box-drawing previews. Footer reads ctrl+x return to graph, so the frame is painted through the workflows stage-chat custom-UI slot.
  • Capture command: tmux capture-pane -p -t askqprev, pane rows 25-44 (the dialog region), reproduced verbatim below.

Root cause, from the raw byte stream

tmux pipe-pane -o over the same painted frames, before the fix. The active row carries
OVERLAY_ACTIVE_ROW_MARKER mid-line, terminated by BEL:

\x1b[31;1H\x1b[2K\x1b[0m\x1b]8;;\x07\x1b[38;2;138;190;183m❯ 1. Encrypt at rest\x1b[39m\x1b_atomic:active\x07             \x1b[38;2;138;190;183m┌──────…
                                                                                                  ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
                                                                                                  APC, BEL-terminated      13 pad+gap spaces

Measured on that capture: APC atomic:active occurrences: 2 | BEL-term: 2 | ST-term: 0.

ECMA-48 lets BEL terminate OSC but not APC; tmux's own input.c says so explicitly. So tmux
keeps consuming bytes as APC payload, eats the pad and the column gap, and resumes drawing at the
border's SGR — the border lands early by exactly leftWidth - visibleWidth(leftClamped) + PREVIEW_COLUMN_GAP + PREVIEW_PADDING_LEFT: 13 columns for option 1, 11 for option 2. Both match the
captures below.

PreviewPane.renderSideBySide is what buries the mark mid-line — WrappingSelect appends it at
end-of-line and the side-by-side join glues the preview column after it. That is why only
side-by-side layout is affected.

BEFORE the fix

BEFORE — option 1 selected (❯ 1. Encrypt at rest)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  Storage

 PREVIEW-ALIGN-QUESTION

❯ 1. Encrypt at rest┌──────────────────────────────────────────┐
     AES-256 at rest             │   ╭────────────────────────────╮         │
  2. Plaintext storage           │   │  api ──▶ db                │         │
     Nothing encrypted           │   │  cache: off                │         │
                                 │   ╰────────────────────────────╯         │
                                 └──────────────────────────────────────────┘

                                 Notes: n add notes

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  3. Chat about this

 enter select · ↑/↓ navigate · n add notes · esc cancel                                                             ctrl+x return to graph

BEFORE — option 2 selected (❯ 2. Plaintext storage)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  Storage

 PREVIEW-ALIGN-QUESTION

  1. Encrypt at rest             ┌──────────────────────────────────────────┐
     AES-256 at rest             │   ╭────────────────────────────╮         │
❯ 2. Plaintext storage│   │  api ──▶ cache ──▶ db      │         │
     Nothing encrypted           │   │  cache: on                 │         │
                                 │   ╰────────────────────────────╯         │
                                 └──────────────────────────────────────────┘

                                 Notes: n add notes

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  3. Chat about this

 enter select · ↑/↓ navigate · n add notes · esc cancel                                                             ctrl+x return to graph

BEFORE — cursor moved back to option 1 (❯ 1. Encrypt at rest)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  Storage

 PREVIEW-ALIGN-QUESTION

❯ 1. Encrypt at rest┌──────────────────────────────────────────┐
     AES-256 at rest             │   ╭────────────────────────────╮         │
  2. Plaintext storage           │   │  api ──▶ db                │         │
     Nothing encrypted           │   │  cache: off                │         │
                                 │   ╰────────────────────────────╯         │
                                 └──────────────────────────────────────────┘

                                 Notes: n add notes

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  3. Chat about this

 enter select · ↑/↓ navigate · n add notes · esc cancel                                                             ctrl+x return to graph

AFTER the fix

AFTER — option 1 selected (❯ 1. Encrypt at rest)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  Storage

 PREVIEW-ALIGN-QUESTION

❯ 1. Encrypt at rest             ┌──────────────────────────────────────────┐
     AES-256 at rest             │   ╭────────────────────────────╮         │
  2. Plaintext storage           │   │  api ──▶ db                │         │
     Nothing encrypted           │   │  cache: off                │         │
                                 │   ╰────────────────────────────╯         │
                                 └──────────────────────────────────────────┘

                                 Notes: n add notes

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  3. Chat about this

 enter select · ↑/↓ navigate · n add notes · esc cancel                                                             ctrl+x return to graph

AFTER — option 2 selected (❯ 2. Plaintext storage)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  Storage

 PREVIEW-ALIGN-QUESTION

  1. Encrypt at rest             ┌──────────────────────────────────────────┐
     AES-256 at rest             │   ╭────────────────────────────╮         │
❯ 2. Plaintext storage           │   │  api ──▶ cache ──▶ db      │         │
     Nothing encrypted           │   │  cache: on                 │         │
                                 │   ╰────────────────────────────╯         │
                                 └──────────────────────────────────────────┘

                                 Notes: n add notes

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  3. Chat about this

 enter select · ↑/↓ navigate · n add notes · esc cancel                                                             ctrl+x return to graph

AFTER — cursor moved back to option 1 (❯ 1. Encrypt at rest)

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  Storage

 PREVIEW-ALIGN-QUESTION

❯ 1. Encrypt at rest             ┌──────────────────────────────────────────┐
     AES-256 at rest             │   ╭────────────────────────────╮         │
  2. Plaintext storage           │   │  api ──▶ db                │         │
     Nothing encrypted           │   │  cache: off                │         │
                                 │   ╰────────────────────────────╯         │
                                 └──────────────────────────────────────────┘

                                 Notes: n add notes

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  3. Chat about this

 enter select · ↑/↓ navigate · n add notes · esc cancel                                                             ctrl+x return to graph

Machine-measured border column

Column index of the first box-drawing character (, , ) on each box row of the capture:

capture first box-border column, per box row
BEFORE — option 1 [20, 33, 33, 33, 33, 33]
BEFORE — option 2 [33, 33, 22, 33, 33, 33]
BEFORE — option 1 again [20, 33, 33, 33, 33, 33]
AFTER — option 1 [33, 33, 33, 33, 33, 33]
AFTER — option 2 [33, 33, 33, 33, 33, 33]
AFTER — option 1 again [33, 33, 33, 33, 33, 33]

Every after-fix row is column 33. Before the fix the active row was 20 (option 1 selected) and 22 (option 2 selected) — 13 and 11 columns early, matching the formula above.

Raw byte stream after the fix

Same scenario, tmux pipe-pane -o over the same two selections (2064 bytes captured):

APC atomic:active occurrences: 0 | BEL-term: 0 | ST-term: 0
any ESC_ APC introducer:        0
tmux pane_title:                atomic - project        (no `atomic:active` hijack)

That zero is the load-bearing result: the mark no longer reaches the terminal on a paint path that
never calls ReservedBottomOverlay.takeActiveRow.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Greptile Summary

This change removes the internal active-row marker from final interactive terminal output and adds regression coverage for renderer and preview-pane behavior.

One repository import-convention issue remains: the new interactive-tui.ts import uses a .ts suffix rather than the required emitted .js suffix.

Confidence Score: 4/5

The rendering behavior is covered, but the new import should follow the repository’s required ESM suffix convention before merging.

There is one non-security P2 finding and no P0 or P1 findings, which corresponds to a score of 4.

Files Needing Attention: packages/coding-agent/src/modes/interactive/interactive-tui.ts

Prompt To Fix All With AI
### Issue 1
packages/coding-agent/src/modes/interactive/interactive-tui.ts:11
**ESM import suffix is inconsistent**

This new source import uses a `.ts` suffix instead of the repository-required TypeScript ESM `.js` convention. Reference `../../core/extensions/ui-types.js` so the source consistently targets the emitted module specifier.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(ask-user-question): stop the active-..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

  • Context used - AGENTS.md (source)

…ew box

OVERLAY_ACTIVE_ROW_MARKER is an APC string terminated by BEL. ECMA-48 lets
BEL terminate OSC but not APC, so a conforming terminal — tmux among them —
keeps consuming bytes as APC payload. PreviewPane's side-by-side join glues
the marked option row and the preview column onto one physical line, so the
terminal swallowed the pad and the column gap and drew the box border 13
columns early with option 1 selected and 11 with option 2, on the cursor row
alone.

ReservedBottomOverlay.takeActiveRow was the only stripper in the tree, and
the workflows stage-chat custom-UI slot paints a mounted component directly
without it, so the mark reached the terminal on that path.

Terminate the mark with ST as the spec requires, and strip it centrally in
applyLineResets on both interactive renderers — the last step before pi-tui
turns the composited screen into bytes, and where it already removes its own
CURSOR_MARKER. Every component tree either renderer paints converges there,
so overlay, inline, widget, and stage-chat mounts are all covered, including
hosts that never call takeActiveRow.

Assistant-model: Claude Opus 5
@mintlify

mintlify Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
bastani 🟢 Ready View Preview Aug 19, 2026, 8:53 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@flora131

Copy link
Copy Markdown
Collaborator Author

Implementation Notes — ask_user_question preview box misaligns on the active option row

Worktree: /Users/tonystark/Documents/projects/atomic-preview-active-row-align
Branch: fix/askq-preview-active-row-align (already checked out at stage start; base commit 3646705921, never main)

Contract amendments received

Amendment A — inherited from loop 1 (user-authored, verbatim):

"Scope the fix to EVERY host path that paints this dialog, not only the one you reproduced. takeActiveRow in reserved-bottom-overlay.ts is currently the sole stripper of OVERLAY_ACTIVE_ROW_MARKER; your capture's footer ("ctrl+x return to graph") shows the frame painted through a path that never calls it. Enumerate the paint paths (interactive reserved-bottom overlay, the workflows overlay adapter in packages/workflows/src/tui/, print/SDK, tui-alt-screen) and state for each whether the marker can reach the terminal.
Prefer defense in depth over patching one adapter: correct the APC termination at the marker definition (ECMA-48 requires ST, ESC-backslash, not BEL) AND guarantee a single central strip before paint. Do not fix only the reproduced adapter and declare it done.
Verify the fix in the SAME real tmux + real atomic CLI scenario, not only in unit tests."

Implemented as required behavior in loop 1: both layers landed (ST terminator at the definition; central strip in applyLineResets on both renderers), and the fix was verified in the same real tmux + real CLI scenario.

Amendment B — received for loop 2 (user-authored, verbatim):

"The ONLY outstanding defect is delivery state: the working tree is uncommitted. Loop 2 is a commit-and-deliver loop, not a redesign loop.
Do NOT modify, revisit, re-derive, or "improve" any of: the fix in ui-types.ts or interactive-tui.ts, the two new test files, TMUX-EVIDENCE.md, the CHANGELOG entry, or the docs/extensions.md edit. They are reviewed and accepted. Re-opening settled code is drift.
Required work, in order:

  1. Commit the reviewed delta on branch fix/askq-preview-active-row-align — the 4 modified files plus the 2 new test files. Conventional commit message.
  2. Do NOT commit TMUX-EVIDENCE.md into the repository. It is evidence, not shipped content, and AGENTS.md requires scratch artifacts stay out of the tree. Keep it in the worktree as the source text for the pull request body only.
  3. Confirm npm run check still passes on the committed state, then let the PR stage carry the TMUX-EVIDENCE.md content verbatim into the pull request body.
    Do not re-run the full test suites; two reviewers already did."

Amendment B was executed literally in loop 2. Nothing under its "do not modify" list was touched: git diff origin/main for the four source/docs files is byte-identical to the reviewed +41/−5, and both new test files were committed unchanged.

Amendment B vs. Amendment B's own step 3 and the "do not re-run the suites" clause — resolved. The repository's prek pre-commit hook runs npm run check and npm run test:unit and cannot be skipped without --no-verify, which the gh-commit skill forbids in capitals. Those runs are mandated, not discretionary, so committing without --no-verify honours both clauses at once. Recorded rather than treated as a conflict.

No new user steering arrived during loop 2 beyond Amendment B.

Acceptance matrix

# Contract clause Verification Status
1 Fix implemented in the designated worktree, on fix/askq-preview-active-row-align, never main git branch --show-currentfix/askq-preview-active-row-align; all edits under that path pass
2 Root cause identified from a REAL tmux capture of a REAL running atomic CLI tmux pipe-pane -o on a live CLI: APC atomic:active occurrences: 2 | BEL-term: 2 | ST-term: 0, mark mid-line followed by 13 pad+gap spaces pass
3 Before- and after-fix tmux capture-pane -p output stored verbatim in TMUX-EVIDENCE.md in the worktree TMUX-EVIDENCE.md present at 216 lines / 15 324 bytes, 16 balanced fence lines, both signature capture lines found by grep -F. Deliberately untracked per Amendment B: the criterion says in the worktree, not committed pass
4 After-fix capture shows the box aligned identically on every row, for every selection Machine-measured border column: after = [33,33,33,33,33,33] for both selections; before = [20,33,…] / [33,33,22,…] pass
5 Regression tests covering PreviewPane exist and pass; there were none before test/unit/ask-user-question-preview-pane.test.ts (12 tests) + packages/coding-agent/test/overlay-active-row-marker-strip.test.ts (3 tests) pass
6 npm run check passes; the vitest suites covering the touched code pass npm run check clean; npm run test:unit 685 files / 6756 passed; coding-agent suite 475 files / 3911 passed pass
7 ## [Unreleased] entry in packages/coding-agent/CHANGELOG.md appended to the existing ### Fixed subsection, no duplicate heading pass
8 No scratch or repro harness files remain in the tree Post-commit git status --porcelain --untracked-files=all → exactly ?? TMUX-EVIDENCE.md. Sweep verified clean: no packages/workflows/dist, no tsconfig.build.json, no bun.lock/yarn.lock/pnpm-lock.yaml, no issues.md; harness lives in /tmp/askq-preview-repro pass
9 User-facing docs describing this behavior stay accurate packages/coding-agent/docs/extensions.md:2832 rewritten pass
10 Reviewed delta committed on the branch, six paths, evidence file excluded (Amendment B 1-2) git log --oneline origin/main..HEAD → one commit 192a9b8fc3; git show --stat HEAD → 6 files, +388/−5; git ls-files TMUX-EVIDENCE.md → empty pass
11 npm run check passes on the committed state (Amendment B 3) re-run after the commit: biome clean, tsc --noEmit clean, tsgo -p tsconfig.build.json --noEmit clean, shrinkwrap up to date, exit 0 pass
PR body carries the evidence verbatim out of this stage's scope (Ignore requests to submit a PR); the branch is committed but not pushed, and TMUX-EVIDENCE.md is the body's source text deferred to the authorized final action

Root cause — confirmed first-hand, not inherited

Reproduced live at tmux 3.7b, session 140×44, atomic from worktree source. tmux pipe-pane -o over the painted frames:

\x1b[31;1H\x1b[2K\x1b[0m\x1b]8;;\x07\x1b[38;2;138;190;183m❯ 1. Encrypt at rest\x1b[39m\x1b_atomic:active\x07             \x1b[38;2;138;190;183m┌──────…

OVERLAY_ACTIVE_ROW_MARKER was "\u001B_atomic:active\u0007" — an APC string terminated by BEL. ECMA-48 allows BEL for OSC but not APC, so tmux kept consuming the pad and the column gap as payload and resumed drawing at the border's SGR. The shift equals leftWidth − visibleWidth(leftClamped) + PREVIEW_COLUMN_GAP + PREVIEW_PADDING_LEFT: 13 columns for option 1, 11 for option 2, both matching the captures exactly.

PreviewPane.renderSideBySide is what buries the mark mid-line (WrappingSelect appends it at end-of-line; the join glues the preview column behind it), which is why only side-by-side layout is affected.

The frame that leaked was painted through the workflows stage-chat custom-UI slot (footer ctrl+x return to graph), a path that never calls ReservedBottomOverlay.takeActiveRow — the repository's only stripper before this change.

Fix

Two layers, three edits, no restyle of surrounding code.

  1. packages/coding-agent/src/core/extensions/ui-types.ts — terminator changed to ST: "\u001B_atomic:active\u001B\\". Safe because pi-tui's extractAnsiCode already accepts APC terminated by BEL or ST (dist/utils.js:382-394); verified at runtime that visibleWidth is still 0 and truncateToWidth still preserves the mark.
  2. Same file — stripOverlayActiveRowMarker(lines), one definition of the removal.
  3. packages/coding-agent/src/modes/interactive/interactive-tui.tsapplyLineResets overridden on AtomicTuiAltScreen and on a new AtomicTuiMainScreen, with the main-screen construction site repointed. That seam is pi-tui's last transform over the composited screen before it builds the byte buffer, and is where pi-tui already removes its own CURSOR_MARKER, so it covers every component tree either renderer paints.

ReservedBottomOverlay.takeActiveRow was deliberately left alone: it needs the mark to pick the crop window and runs on the component tree long before this seam.

preview-pane.ts was deliberately not modified. Hoisting the mark to end-of-line there is redundant once the central strip lands, and editing those lines risks exactly the restyle the objective forbids.

Validation

command outcome
npm run check pass (biome, tsc --noEmit, tsgo -p tsconfig.build.json --noEmit, shrinkwrap up to date)
npm run test:unit 685 files passed, 6756 passed / 2 skipped
npm run test --workspace=@bastani/atomic 475 files passed / 4 skipped, 3911 passed / 39 skipped
npx vitest --run --project unit test/unit/ask-user-question-preview-pane.test.ts 12 passed
npx vitest --run test/overlay-active-row-marker-strip.test.ts (coding-agent) 3 passed
npx vitest --run test/ask-user-question-transcript-scroll.test.ts (owns the mark / boundOverlayLines contract) 76 passed

Red-green proof. With interactive-tui.ts reverted to HEAD and everything else in place, test/overlay-active-row-marker-strip.test.ts reports 2 failed / 1 passed — both renderer tests fail on written.includes(OVERLAY_ACTIVE_ROW_MARKER) === false. Restoring the file returns 3 passed.

Honest limit on the PreviewPane column tests. The pane's own arithmetic was already self-consistent before the fix, so the column-invariance assertions in ask-user-question-preview-pane.test.ts do not fail pre-fix — they pin the invariant the fix must not break. The tests that genuinely fail pre-fix are the two renderer-level ones plus the terminator-shape assertion.

Loop 2 — delivery (commit only, no push, no PR)

No file under Amendment B's "do not modify" list was touched. git diff origin/main --stat before staging was byte-identical to the reviewed delta: 4 files, +41 / −5.

Staged by explicit path, never git add -A / git add . / git commit -a, because TMUX-EVIDENCE.md is untracked and not gitignored (git check-ignore exits 1) and a blanket stage would sweep it in. git diff --cached --name-only confirmed six paths and zero matches for TMUX-EVIDENCE.md before committing.

Commit: 192a9b8fc34d776e17e07f8bdce25f5e4a4f61f8fix(ask-user-question): stop the active-row marker shifting the preview box, with the Assistant-model: Claude Opus 5 trailer. Six files, +388 / −5 (the +347 over the source delta is the two new test files).

--no-verify was not used. The prek pre-commit hooks ran and all passed:

check for added large files..............................................Passed
check for case conflicts.................................................Passed
check for merge conflicts................................................Passed
detect private key.......................................................Passed
biome check --write......................................................Passed
npm run check............................................................Passed
npm run test:unit........................................................Passed

The biome check --write hook made no rewrite, so there was no re-stage/retry loop.

Post-commit verification:

check result
git branch --show-current fix/askq-preview-active-row-align
git log --oneline origin/main..HEAD exactly one commit, 192a9b8fc3
git show --stat HEAD 6 files, +388 / −5; two create mode lines for the new tests
git status --porcelain --untracked-files=all exactly ?? TMUX-EVIDENCE.md — as §4 of the research predicts, not empty, and correct under Amendment B
npm run check on the committed tree exit 0
git ls-files TMUX-EVIDENCE.md empty — the evidence file is not tracked
TMUX-EVIDENCE.md integrity 216 lines / 15 324 bytes, 16 balanced fence lines, ❯ 1. Encrypt at rest┌ (BEFORE) and ❯ 1. Encrypt at rest ┌ (AFTER) both present

Stopped before push and PR, deliberately. The launch contract says "Ignore requests to submit a PR; the authorized final action handles that after approval." git push -u origin is an external write and the first half of PR submission, so the branch is committed locally and not pushed. The research's checklist steps 7-9 belong to the PR stage. Consequently the pre-push hooks (test-integration, test-ci-contracts) have not run in this session.

Judgement call recorded, not re-opened: OVERLAY_ACTIVE_ROW_MARKER is publicly re-exported and its value changed, but every in-repo consumer imports the constant rather than the literal and the docs instruct extensions to embed the constant, so the commit is fix: rather than fix!:. Worth one sentence in the PR body.

Deferred / out of scope

  • pi-tui's own CURSOR_MARKER is still BEL-terminated (dist/tui.js:21). pi-tui strips it centrally so it does not leak today, but it is the same latent class. Upstream matter; worth a line in the PR.
  • If a later stage runs further live tmux verification, list and stop stray atomic / bun … cli.ts processes from other worktrees first. A reviewer's first E2E attempt was contaminated by them and showed legacy BEL output until they were stopped.
  • Branch push and PR creation are deferred to the authorized final action. The research artifact's §6 carries the exact gh pr create --body-file recipe and the round-trip greps that close criterion 9.
  • Paint paths 3, 4, 5, and 6-with-reserveTranscriptRows:false (ordinary overlay, inline mount, widget, engine overlay branch) were not exercised end-to-end. The central strip covers them structurally because it sits downstream of the whole component tree; that structural claim is proven by capture for the workflows stage-chat path and by unit test for both renderer classes.
  • The original reporter's terminal is unidentified. The mechanism is proven in tmux 3.7b; a terminal that ends APC on BEL would never have shown the shift.

QA E2E Video

Not applicable: this is a terminal/TUI scenario with no web frontend, so the QA evidence is the tmux pane capture required by the objective rather than a playwright-cli .webm. playwright-cli was not invoked and no video was produced. The equivalent proof lives in /Users/tonystark/Documents/projects/atomic-preview-active-row-align/TMUX-EVIDENCE.md, which carries before- and after-fix tmux capture-pane -p output plus the raw tmux pipe-pane byte counts from a real interactive atomic CLI at 140×44, driven through /workflow ask-user-question-graph-overlayF2Enter and moved between both options.

Reproduction harness (outside the tree, by design)

/tmp/askq-preview-repro/ — stand-in openai-responses model server, agent config, the project workflow fixture, and every capture. Nothing was written into the worktree, so "no scratch files" holds by construction.

bun /tmp/askq-preview-repro/model-server.ts /tmp/askq-preview-repro/state
tmux new-session -d -s askqprev -x 140 -y 44 -c /tmp/askq-preview-repro/project
# in the pane:
cd /tmp/askq-preview-repro/project && NODE_ENV=production \
  ATOMIC_CODING_AGENT_DIR=/tmp/askq-preview-repro/agent ATOMIC_SKIP_VERSION_CHECK=1 \
  bun <worktree>/packages/coding-agent/src/cli.ts --approve --offline --no-session
# then: /workflow ask-user-question-graph-overlay → ↵ → F2 → ↵ → ↓ / ↑
tmux capture-pane -p -t askqprev

NODE_ENV=production is load-bearing: under a test runner the workflows extension hands stages a stub session that never calls a tool. A fresh worktree also needs packages/natives/native/atomic_natives.<platform>.node (gitignored, built only by test/global-setup-natives.ts); it was already present in this worktree.

type TuiInputListener,
TuiMainScreen,
} from "@earendil-works/pi-tui";
import { stripOverlayActiveRowMarker } from "../../core/extensions/ui-types.ts";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 ESM import suffix is inconsistent

This new source import uses a .ts suffix instead of the repository-required TypeScript ESM .js convention. Reference ../../core/extensions/ui-types.js so the source consistently targets the emitted module specifier.

Context Used: AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/coding-agent/src/modes/interactive/interactive-tui.ts
Line: 11

Comment:
**ESM import suffix is inconsistent**

This new source import uses a `.ts` suffix instead of the repository-required TypeScript ESM `.js` convention. Reference `../../core/extensions/ui-types.js` so the source consistently targets the emitted module specifier.

**Context Used:** AGENTS.md ([source](https://github.com/bastani-inc/atomic/blob/main/AGENTS.md))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declining this one — the .js convention does not govern packages/coding-agent.

AGENTS.md states the rule and its rationale together: "Source files use .js import extensions… The repo ships as .ts files; Bun resolves .js specifiers to the underlying .ts source directly — no loader hook required." That reasoning applies to the raw-TypeScript companion packages, which ship unbuilt. The very next bullet carves this package out: "packages/coding-agent is copied from upstream pi and keeps its existing build setup."

The code agrees:

scope .ts relative imports .js relative imports
packages/coding-agent/src 2449 161
packages/workflows/src 0 1735

tsconfig.base.json sets allowImportingTsExtensions: true and rewriteRelativeImportExtensions: true, which is precisely the configuration that makes a .ts specifier correct here and rewrites it at emit.

And within this file, lines 12-14 are clipboard.ts, open-browser.ts, transcript-follow-indicator.ts. Taking the suggestion would make line 11 the only .js import in the file, and it would point at a path that does not exist on disk — ui-types.js is not a file, only ui-types.ts is.

Please remember this distinction: .js specifiers for the raw-TypeScript packages (workflows, subagents, mcp, web-access, intercom), .ts specifiers inside packages/coding-agent.

@flora131
flora131 merged commit cef866e into main Aug 19, 2026
18 checks passed
@flora131
flora131 deleted the fix/askq-preview-active-row-align branch August 19, 2026 21:18
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