Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 24 additions & 23 deletions ui-tui/src/__tests__/brandingRenderWidth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,16 @@ const info = (over: Partial<SessionInfo> = {}): SessionInfo => ({
...over
})

const boxRows = (cols: number, sessionInfo: SessionInfo, theme: Theme = DEFAULT_THEME) =>
render(cols, transcriptPanelWidth(cols), sessionInfo, theme)

/** Same wrapper, but the panel is told a width its container does not have. */
const boxRowsWithMaxWidth = (cols: number, maxWidth: number) => render(cols, maxWidth, info(), DEFAULT_THEME)

async function render(
/** `oversizedContainer` drops the scrollbar gutter, reproducing the first paint
* where the ScrollBox has not yet reserved it. */
async function boxRows(
cols: number,
maxWidth: number,
sessionInfo: SessionInfo,
theme: Theme = DEFAULT_THEME
theme: Theme = DEFAULT_THEME,
{ oversizedContainer = false }: { oversizedContainer?: boolean } = {}
): Promise<string[]> {
const maxWidth = transcriptPanelWidth(cols)
const gutter = oversizedContainer ? 0 : TRANSCRIPT_SCROLLBAR_GUTTER
Object.defineProperty(process.stdout, 'columns', { configurable: true, value: cols, writable: true })

const stdout = new PassThrough()
Expand Down Expand Up @@ -112,11 +110,7 @@ async function render(
React.createElement(SessionPanel, { info: sessionInfo, maxWidth, sid: 'a1b2c3d4', t: theme })
)
),
React.createElement(Box, {
flexShrink: 0,
marginLeft: TRANSCRIPT_SCROLLBAR_GUTTER - 1,
width: TRANSCRIPT_SCROLLBAR_GUTTER - 1
})
gutter ? React.createElement(Box, { flexShrink: 0, marginLeft: gutter - 1, width: gutter - 1 }) : null
),
{
patchConsole: false,
Expand Down Expand Up @@ -181,22 +175,29 @@ describe('header box render width', () => {
}
})

// The shipped regression: appLayout passed `composer.cols - 2` (the COMPOSER's
// reserve) instead of the transcript's, so the panel believed it had two more
// columns than its container had. Combined with an explicit `width={cols}` on
// the box, the right border was pushed off screen — the box rendered with a
// left edge, a top rule running to the terminal edge, and no `╮`/`│`/`╯`.
// The regression that made the border invisible in a real terminal.
//
// 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. A box that sizes to its container therefore renders two columns
// too wide, gets clipped, and — because the intro row is committed to
// scrollback and never repainted — stays broken until a resize.
//
// The panel must now survive a caller that over-reports: with no hard width it
// stretches to its real container regardless of what it was told.
it.each([34, 60, 100, 140])('ignores an over-reported maxWidth at %i columns', async cols => {
const rows = await boxRowsWithMaxWidth(cols, cols - 2)
// So the panel must hold its own width against a container that is bigger
// than the steady-state one. `oversizedContainer` reproduces frame 1 by
// omitting the gutter.
it.each(WIDTHS)('holds its width when the container is mis-measured at %i columns', async cols => {
const rows = await boxRows(cols, info(), DEFAULT_THEME, { oversizedContainer: true })
const top = rows.find(r => r.startsWith('╭'))
const bottom = rows.find(r => r.startsWith('╰'))

expect(rows.length).toBeGreaterThan(0)
expect(top).toBeDefined()
expect(bottom).toBeDefined()
expect(top?.endsWith('╮')).toBe(true)
expect(bottom?.endsWith('╯')).toBe(true)

// Still the steady-state width, not the container's inflated one.
for (const row of rows) {
expect(stringWidth(row)).toBe(transcriptPanelWidth(cols))
}
Expand Down
24 changes: 17 additions & 7 deletions ui-tui/src/components/branding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -676,13 +676,23 @@ export function SessionPanel({ info, logoPalette, maxWidth, sid, t }: SessionPan
marginBottom={1}
paddingX={1}
paddingY={1}
// Deliberately NO explicit width: the box stretches to its container, so
// the right border is wherever the container actually ends. Pinning it to
// `cols` instead makes the border a *prediction* of the container width,
// and when that prediction was two columns high (the transcript reserves a
// scrollbar gutter the composer budget doesn't) the right border was
// pushed off screen entirely. `cols` still drives the column split below,
// where being slightly off only shifts a truncation point.
// Explicit width, deliberately — do NOT let this box size to its
// container.
//
// On the FIRST paint the transcript's ScrollBox has not settled: it
// reports its full width, without the scrollbar gutter its steady-state
// layout reserves. A container-sized box therefore renders two columns
// wider than the terminal can show and its right border is clipped off
// screen, staying that way because the intro row is committed to
// scrollback and never repainted — until a resize forces a relayout, at
// which point the border appears. That is exactly the "I have to resize
// to see the border" symptom.
//
// `cols` does not have that problem: it comes from `maxWidth`
// (transcriptPanelWidth) and process.stdout.columns, both correct on the
// first frame. Measured on a real pty: cols=196 on frame 1 at a 200-col
// terminal, while the container claimed 198.
width={cols}
>
{wide && (
<Box alignItems="center" flexDirection="column" width={leftW}>
Expand Down
Loading