Skip to content

fix(tui): restore the header box's right border - #769

Merged
ericleepi314 merged 1 commit into
mainfrom
fix/header-box-right-border
Jul 29, 2026
Merged

fix(tui): restore the header box's right border#769
ericleepi314 merged 1 commit into
mainfrom
fix/header-box-right-border

Conversation

@ericleepi314

@ericleepi314 ericleepi314 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #764. The header box shipped with no right border — a left edge and a top rule running off the screen.

Cause

It was exactly two columns too wide.

appLayout sized the intro banner and session panel with composer.cols - 2. That's the composer's reserve — and composer.cols is the terminal width, which the composer separately subtracts its prompt prefix from (stableComposerColumns). The transcript's reserve is different and larger:

Reserve Columns
Scrollbar gutter — marginLeft={1} on the wrapper + the scrollbar's width={1} 2
paddingX={1} on the transcript's inner column 2
Total 4

So the panel drew itself at term - 2 inside a container of term - 4, and ScrollBox clipped the overflow — taking the right border with it.

Fix

Two changes. Either one fixes the symptom; both are worth having.

1. Name the transcript's reserve. New transcriptPanelWidth() sits next to stableComposerColumns() in inputMetrics.ts so the two budgets can't be confused again, and both Banner and SessionPanel use it.

2. Stop predicting the container. Dropped the explicit width={cols} from the panel's outer Box. That prop made the border a prediction of the container width; without it the box stretches to whatever the container actually is, so a caller that over-reports can no longer push the border off screen. The feed column gains flexGrow to absorb the real remainder, and its footer rule becomes a top-border-only Box instead of '─'.repeat(w) — a repeat count can't track a column that flexes.

Why the test missed it

width={cols} was tuned against a harness whose container was the whole terminal, and the render test inherited that assumption — it rendered SessionPanel at the root and asserted rows were exactly cols wide. Both agreed with each other and disagreed with the app.

The test now renders inside a stand-in for appLayout's real wrapper and asserts rows match the container width.

One detail worth recording, because it cost a mutation cycle: a plain <Box> wrapper is not sufficient. It has flexShrink, so an over-wide child is quietly shrunk to fit and the bug disappears. ScrollBox constrains its children rather than being expanded by them, so the child is clipped — the actual on-screen behavior. Verified:

Wrapper Restoring width={cols}
plain <Box> 0 tests fail — bug invisible
<ScrollBox> 4 tests fail

Testing

  • New: panel ignores an over-reported maxWidth (4 widths); unit coverage for transcriptPanelWidth.
  • Existing render tests retargeted from terminal width to container width.
  • npm run typecheck clean; eslint clean on all changed files. (appLayout.tsx has one pre-existing import-order error on cronIndicator.js, confirmed present at HEAD without these changes — left alone.)
  • Suite 8 failed / 1576 passed — same 8 pre-existing failures as baseline (createGatewayEventHandler, cursorDriftRegression, statusRule, useConfigSync, virtualHeights).

🤖 Generated with Claude Code

The header box rendered with a left edge and a top rule that ran off the
screen, no right border at all. It was exactly two columns too wide.

`appLayout` sized the intro banner and session panel with
`composer.cols - 2`. That is the *composer's* reserve; `composer.cols` is the
terminal width and the composer separately subtracts its prompt prefix. The
transcript's reserve is different and larger: a scrollbar gutter beside the
ScrollBox (`marginLeft={1}` plus the scrollbar's own `width={1}`) and
`paddingX={1}` on the inner column -- four columns, not two. The panel drew
itself two columns wider than its container, and ScrollBox clipped the
overflow, taking the right border with it.

Two changes, either of which fixes the symptom; both are worth having:

- Add `transcriptPanelWidth()` next to `stableComposerColumns()`, naming the
  transcript's reserve so the two budgets can't be confused again, and use it
  for both `Banner` and `SessionPanel`.

- Drop the explicit `width={cols}` from the panel's outer Box. That prop made
  the border a *prediction* of the container width; without it the box simply
  stretches to whatever the container is, so a caller that over-reports can no
  longer push the border off screen. The feed column gains `flexGrow` to absorb
  the real remainder, and its footer rule becomes a top-border-only Box rather
  than `'─'.repeat(w)`, since a repeat count can't track a column that flexes.

`width={cols}` was tuned against a harness whose container was the whole
terminal, which is also why the render test missed this. The test now renders
inside a stand-in for appLayout's real wrapper and asserts rows match the
container width rather than the terminal width. A plain `<Box>` wrapper is not
sufficient: it has flexShrink, so an over-wide child is quietly shrunk and the
bug vanishes. ScrollBox constrains its children instead, so the child is
clipped -- the on-screen behavior. Verified by mutation: restoring
`width={cols}` fails 4 tests under the ScrollBox wrapper and 0 under a Box one.

Adds a test that the panel ignores an over-reported `maxWidth`, plus unit
coverage for `transcriptPanelWidth`.

Suite: 8 failed / 1576 passed -- the same 8 pre-existing failures
(createGatewayEventHandler, cursorDriftRegression, statusRule, useConfigSync,
virtualHeights) as baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericleepi314
ericleepi314 merged commit a8fb451 into main Jul 29, 2026
2 checks passed
@ericleepi314
ericleepi314 deleted the fix/header-box-right-border branch July 29, 2026 15:26
ericleepi314 added a commit that referenced this pull request Jul 29, 2026
…aint (#770)

#769 fixed the width the panel is TOLD (the transcript reserves a scrollbar
gutter the composer budget does not) but also removed the box's explicit
`width`, letting it size to its container instead. On a real terminal the
right border was still missing, and only appeared after resizing the window.

Measured on a pty at 200 columns, with the component instrumented: the panel
computes `maxWidth=196, cols=196, leftW=44, w=145` on the very first frame --
correct. The rendered box is 198. So the arithmetic was never wrong; the
container was. On the first paint the transcript's ScrollBox has not settled
and reports its full width, without the scrollbar gutter its steady-state
layout reserves, so a container-sized box renders two columns too wide and is
clipped. Because the intro row is committed to scrollback and never repainted,
it stays clipped until a resize forces a relayout -- exactly the reported "I
have to resize the window to see the border".

Restore `width={cols}`. `cols` derives from `transcriptPanelWidth` and
`process.stdout.columns`, both correct on frame 1, so pinning to it is right at
first paint AND after resize; the same pty run now closes the box at both
sizes. #769's other half stands: without its `maxWidth` fix, `cols` would still
be the composer's budget and pinning to it would reproduce the original bug.

The render test asserted the box fills its container, which is what let this
through -- under a correctly-sized container both behaviors agree. It now
renders against a DELIBERATELY oversized container (gutter omitted, reproducing
frame 1) and asserts the box holds its steady-state width regardless. Removing
`width={cols}` fails it at all 9 widths.

Suite: 8 failed / 1581 passed -- the same 8 pre-existing failures.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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