fix(tui): restore the header box's right border - #769
Merged
Conversation
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
appLayoutsized the intro banner and session panel withcomposer.cols - 2. That's the composer's reserve — andcomposer.colsis the terminal width, which the composer separately subtracts its prompt prefix from (stableComposerColumns). The transcript's reserve is different and larger:marginLeft={1}on the wrapper + the scrollbar'swidth={1}paddingX={1}on the transcript's inner columnSo the panel drew itself at
term - 2inside a container ofterm - 4, andScrollBoxclipped 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 tostableComposerColumns()ininputMetrics.tsso the two budgets can't be confused again, and bothBannerandSessionPaneluse it.2. Stop predicting the container. Dropped the explicit
width={cols}from the panel's outerBox. 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 gainsflexGrowto absorb the real remainder, and its footer rule becomes a top-border-onlyBoxinstead 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 renderedSessionPanelat the root and asserted rows were exactlycolswide. 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 hasflexShrink, so an over-wide child is quietly shrunk to fit and the bug disappears.ScrollBoxconstrains its children rather than being expanded by them, so the child is clipped — the actual on-screen behavior. Verified:width={cols}<Box><ScrollBox>Testing
maxWidth(4 widths); unit coverage fortranscriptPanelWidth.npm run typecheckclean;eslintclean on all changed files. (appLayout.tsxhas one pre-existing import-order error oncronIndicator.js, confirmed present at HEAD without these changes — left alone.)createGatewayEventHandler,cursorDriftRegression,statusRule,useConfigSync,virtualHeights).🤖 Generated with Claude Code