Skip to content

Keep History controls reachable as entries accumulate - #1041

Merged
taras merged 3 commits into
mainfrom
codex/repl-history-geometry-1017
Nov 1, 2026
Merged

taras merged 3 commits into
mainfrom
codex/repl-history-geometry-1017

Conversation

@taras

@taras taras commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

Growing History maps can push action controls off screen and pass fractional geometry to the renderer. The wide layout now bounds the map and gap using the complete controls’ measured space, keeping History and Exit reachable as entries accumulate. Strict whole-cell validation remains in place.

The terminal regression checks every entry through 24 entries at 160 columns and 30 at 200, reads the drawer’s own rows, and preserves selection and the draft. An isolated cold-reopen addendum also passes with 18 completed entries plus one unfinished submission at 160×36 and 200×36, with no provider prompts or replay. The user’s saved session is untouched.

All three first-green review findings are addressed. Five focused test files and changed-file lint/format pass; the affected original Opus review and cold-reopen verification pass at the signed head. The saved performance harness was rerun with its limitations documented. CI verifies the pushed head before delivery; the one full Fable review does not repeat.

Closes #1017

The accepted wide row left its map to the drawing engine's own fit(),
but that engine answers from its own prior renders rather than this
frame's measuring pass — measured, an eighty-four-cell map the
measuring pass confirmed whole came back 82.0078125 from the engine
that commits the frame, once a twelfth entry grew the map's text past
whatever the drawing engine had last measured for it. The control
beside the map inherited the fraction and ReplRenderError ended the
running command.

Stating the map at the width this frame already measured removes the
second, independent ask, mirroring how the row's gap is already
stated rather than left to grow() beside it.
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

PR #1041: Keep History on whole cells after twelve entries

3 files, +170 / -65

Scope

✅ PR scope looks good.

Structural

Oxlint structural signals:

  • no-unused-vars ×20: packages/cli/src/repl/program.ts, packages/cli/src/repl/application.ts
  • no-unnecessary-boolean-literal-compare ×1: packages/cli/src/repl/application.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 130 diagnostics across 2 files (10 rules)
Density: 0.765 violations/added-line

no-shadow (73): packages/cli/src/repl/program.ts, packages/cli/src/repl/application.ts
no-unused-vars (20): packages/cli/src/repl/program.ts, packages/cli/src/repl/application.ts
no-floating-promises (14): packages/cli/src/repl/program.ts
consistent-function-scoping (7): packages/cli/src/repl/application.ts
consistent-return (5): packages/cli/src/repl/application.ts, packages/cli/src/repl/program.ts
no-duplicate-type-constituents (4): packages/cli/src/repl/application.ts
no-useless-default-assignment (3): packages/cli/src/repl/application.ts, packages/cli/src/repl/program.ts
unbound-method (2): packages/cli/src/repl/program.ts
no-array-sort (1): packages/cli/src/repl/application.ts
no-unnecessary-boolean-literal-compare (1): packages/cli/src/repl/application.ts

Correctness

No extraneous code patterns detected.

@taras
taras marked this pull request as ready for review October 31, 2026 23:46

@taras taras left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Review of 10a936f4 (first green CI: run 41253230878)

Verdict: request changes. (Posted as a comment review: GitHub does not let the author account of a pull request set the request-changes state.) The change does what it says for the twelfth entry, and I reproduced both sides: base c731892f ends on entry 12 with the issue's ReplRenderError ... x=120.0078125 in 3 of 3 runs, and this head completes 14 entries, Draft typing and History open/navigate/close in 3 of 3 runs. One correctness finding keeps it short of #1017's acceptance ("twelve and further entries finish without ending the command"; "every History/action control remains reachable"): at 160x36 the nineteenth entry ends the command with the same refusal, and [exit] stops being drawn after the eighteenth. Details and a concrete recommendation are in the inline comment on wideHistoryMapWidth.

What I checked

  • The three-file diff against c731892f; the accepted non-wide historySplit path and the renderer's integral() refusal are untouched.
  • The cause: the map was the one box on the accepted wide row still asking the drawing engine to fit() its text while the gap beside it was stated from the measuring pass; stating the map from the same pass is consistent with packages/cli/src/repl/renderer.ts's two-engine design and with the existing historyGap idiom. The probe pass reads the fitted width before the value is stated, so there is no circularity.
  • The regression test's evidence (inline comment: one assertion holds before the drawer opens, and the entry count stops short of the row-full point).
  • Comment volume and one untrue doc claim (inline comment, non-blocking).

Performance (measured)

Workload: the repository's own page object at 160x36, sequential plain entries awaited to their rail mark, 24 Draft keystrokes, three History cycles (open, ArrowUp x2, ArrowDown x2, close). Boundary: completed presentation write into the test suite's in-process terminal emulator. That is the application's frame output, not a TTY flush and not a visible paint. Same host, Deno 2.9.7, runs sequential, 3 per condition, about 1.6 s active window each, startup excluded.

Equal state on both trees (11 entries, then Draft and History):

metric head (3 runs) base (3 runs)
active-window writes per second 62.5, 62.8, 60.1 62.6, 61.8, 61.7
Enter to rail mark, p50 ms 61.1, 59.1, 59.4 59.7, 60.5, 60.0
Draft keystroke to shown, p50 / p95 ms 6.6 / 7.0, 6.6 / 6.9, 6.6 / 7.3 6.6 / 7.0, 6.8 / 7.5, 6.5 / 6.8
History open/nav/close, p50 / p95 ms 8.8 / 17.8, 8.7 / 18.6, 8.9 / 54.2 8.5 / 17.2, 8.8 / 18.8, 8.9 / 17.7

Warmed prefix (entries 1 to 11 inside the 14-entry runs, 33 samples pooled): Enter to rail mark p50 / p95 64.7 / 116.8 ms at head against 65.0 / 116.0 ms at base; write interval p50 about 10 ms on both. Head at 14 entries: write interval p50 8.3 to 8.7 ms, p95 45 to 46 ms, worst 77 ms; Draft keystroke p50 6.4 to 6.6 ms; History p50 8.7 to 9.0 ms. The 54.2 ms History p95 is one sample in one run; the other five runs sit at 17 to 19 ms. The "Enter to rail mark" latency includes executing the entry and journal I/O, not only rendering.

CPU profile (Node v26.5.1 --cpu-prof, head, 14-entry workload): about 1.17 s of 4.8 s sampled is the workload, the rest is loader startup. Hot stacks are the emulator's cell reads in the page object's write hook (harness cost), the wasm layout engine under measure from fitPlain/prepareFrame, and detach under describe -> drawerContent -> presentationFor. Nothing this PR adds appears as a leaf.

Conclusion: no measurable cost from the change; head and base agree within run-to-run noise on every equal-state metric.

Limitations: in-process emulator boundary; Deno 2.9.7 has no --cpu-prof, so the profile is from Node and is not comparable to the Deno timings; three runs per condition; the baseline cannot reach entries 12 to 14, so those have no baseline; no FPS budget exists for this surface and none is asserted.

Comment thread packages/cli/src/repl/application.ts Outdated
export function wideHistoryMapWidth(
widthOf: (id: string) => number | undefined,
): number | undefined {
const map = widthOf(HISTORY_MAP);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Correctness: at 160x36 the command still ends, now on the nineteenth entry, and [exit] is lost at the eighteenth.

Consequence: #1017's acceptance is "twelve and further entries finish without ending the command" and "every History/action control remains reachable". At this head, continuing the issue's own workload past fourteen entries at the accepted size still ends xmd repl.

Evidence (same page object the regression uses, useReplPage at 160x36, Entry N text. awaited to its rail mark, run at this head):

  • Entry 17: the History row fills all 160 columns: HISTORY 1 ✓ 2 ✓ ... 17 ✓ ── HEAD · SETTLED [history] [exit].
  • Entry 18: the row reads ... 18 ✓ ── HEAD · SETTLED [history] and [exit] is no longer drawn.
  • Entry 19: ReplRenderError: the engine placed node-15 at a fractional position (x=120.0091323852539, y=34, width=11, height=1); the command ends.
  • At 200 columns the same sequence happens at entries 24 and 25 (x=160.00685119628906), so the point tracks the row width. Base c731892f ends at entry 12 (x=120.0078125) in 3 of 3 runs; this head completes 14 in 3 of 3.

Why: compactHistoryRuns emits a run for every entry (about three cells each) and nothing bounds it. The stated width is the probe pass's fitted width of the whole text, which protects the row only while that text fits beside the controls. Once it does not, the row is back in the arrangement the renderer refuses, and the control row has already dropped a control to get there.

Recommendation: bound the accepted wide map by what the controls need, the way the non-wide row already does. prepareFrame runs the parts probe (historyProbeOf / historySplitOf) only under profileFor(view.size) !== "wide"; running it for the wide profile too and stating the map at min(natural, row - needed) with the gap derived from that keeps the accepted layout while the text fits and shortens the band before a control is lost or refused. Windowing the compact rail to the entries that fit is the other option, but that is a visual decision for the accepted layout's owner. Either way, extend the regression past the row-full point at 160 columns (entry 19 today) instead of stopping at 14, since 14 sits inside the range the fix already covers.

// History's own actions stay reachable: its drawer still opens and shows
// the entries just admitted.
yield* openHistory(page);
expect(page.screen().shows("Entry 14")).toBe(true);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Test evidence: this assertion holds before the drawer opens.

page.screen().shows("Entry 14") is already true on the screen before openHistory (the transcript shows the entry's text; measured true at this head in every run), so it does not show that the drawer lists the entries just admitted. openHistory itself already proves the drawer opened and owns its keys.

Recommendation: read the drawer's own rows. packages/cli/tests/repl-agent-journey.test.ts has a historyMarkers helper over drawerLines that filters admitted|recorded rows, and chooseMarker scopes its lookup rightOf: "[close]"; asserting the admitted entry's marker that way is discriminating. Two smaller notes: expect(page.repl.terminal.ending).toBeUndefined() on line 2918 cannot fail here, because the render refusal propagates out of submit as a thrown error before this line runs (in the base runs it arrives out of entrySettles with terminal.ending still undefined), so the fourteen awaited rail marks are the real evidence; and per the finding on wideHistoryMapWidth, fourteen entries is below the point where the rail fills the row at this width.

* and absent at every other profile, whose rows ask their map a different
* way.
*/
readonly wideHistoryMap?: number | undefined;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Minimalism: the same incident is narrated three times, and this doc overstates absence. Non-blocking.

The 84 against 82.0078125 story appears here, in wideHistoryMapWidth's doc, and in a twenty-line inline comment on the HISTORY_MAP box. AGENTS.md's Code Rules ask comments to describe the surprising behavior; the reason fits in one place (the drawing engine answers a fit() from its own render history, so a width this frame already measured is stated rather than asked again), and the measured numbers belong in the commit message and this PR.

Also, prepareFrame computes historyWideMap on every profile, and the probe manifest holds box:history:map on all of them, so off the wide profile the value is present and unused rather than "absent at every other profile".

Recommendation: keep the reason once on wideHistoryMapWidth, make this field's doc and the box comment one line each pointing to it, and either compute the value only on the wide profile (beside the existing profileFor(view.size) !== "wide" branch in prepareFrame) or drop the absence claim.

taras added 2 commits October 31, 2026 20:34
, PR #1041 review)

The prior fix stated the map's width from a measured value instead of
the drawing engine's own fit(), but left that width unbounded. The
map's text grows about three cells an entry, so once it outgrew what
the row's gap-and-controls split could still spare, the engine's own
remainder arithmetic went fractional again — and separately, the
measuring engine's own fit() answer for a long enough map can itself
go fractional, not only the drawing engine's. At 160x36 the eighteenth
entry silently dropped [exit] and the nineteenth crashed the command;
at 200x36 the same thing happened at twenty-four and twenty-five.

wideGapWidth and wideHistoryMapWidth are consolidated into one
wideHistoryWidths: it bounds the map to what the row's controls need
once the half-split gap would leave them short, or once the map's own
width cannot be trusted as a whole number at all, dropping the gap to
zero in that regime rather than taking a share of the controls' own
room. The accepted row's appearance is unchanged while the text fits.

The regression test now drives past the row-full point at both 160 and
200 columns, checks every action stays reachable at every entry, reads
the drawer's own admitted rows instead of the transcript (which was
already true before the drawer opened), and confirms History selection
and the draft survive drawer interaction. Consolidates the repeated
measuring/drawing rationale into wideHistoryWidths's own doc and
corrects the field docs, which now compute only on the wide profile.
@taras taras changed the title Keep History on whole cells after twelve entries Keep History controls reachable as entries accumulate Nov 1, 2026
@taras
taras merged commit 87fbc1b into main Nov 1, 2026
58 checks passed
@taras
taras deleted the codex/repl-history-geometry-1017 branch November 1, 2026 01:07
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.

Keep xmd repl running when History reaches twelve entries

1 participant