Skip to content

Commit 0e99b31

Browse files
committed
Stop the provider setup screen from garbling text on short terminals
Header, intro, step, and instruction rows had no flexShrink guard, unlike every other chrome box in the layout, so a terminal too short for the full column let the flex algorithm compress them onto the same row instead of clipping from the bottom. The list height budget also reserved far more chrome rows than the picker actually uses, leaving the footer to go missing on terminals that had room for it. Fixes CL-5363.
1 parent 1e617b1 commit 0e99b31

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

‎src/tui-opentui/provider-setup.test.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,3 +693,49 @@ describe("runProviderSetup paste", () => {
693693
expect(values?.apiKey).toBe(key)
694694
})
695695
})
696+
697+
describe("runProviderSetup pick-list height cap", () => {
698+
// Every terminal size gets a bounded frame — no chrome row overlaps
699+
// another (the header/intro/step/instruction rows used to compress into
700+
// each other when the flex column ran out of room), and the picker never
701+
// paints past the terminal's own row count.
702+
for (const height of [24, 16, 12, 8, 6]) {
703+
test(`stays within a ${height}-row terminal with no overlapping chrome`, async () => {
704+
const harness = await createHarness({ width: 80, height })
705+
runProviderSetup({
706+
onSubmit: async () => {},
707+
showTelemetryNotice: false,
708+
createRenderer: async () => harness.renderer,
709+
})
710+
await harness.renderOnce()
711+
await harness.renderOnce()
712+
const lines = harness.captureCharFrame().split("\n")
713+
expect(lines.length).toBeLessThanOrEqual(height + 1)
714+
// The garbled-overlap bug glued the step line and the intro line
715+
// together on one row; each survives as its own line, or is clipped
716+
// entirely, but never merges into the other.
717+
const stepLine = lines.find((l) => l.includes("step 1 of 3"))
718+
if (stepLine !== undefined) {
719+
expect(stepLine).not.toContain("connect an inference provider")
720+
}
721+
})
722+
}
723+
724+
test("keyboard navigation scrolls a long provider list and keeps the active row visible", async () => {
725+
const harness = await createHarness({ width: 80, height: 16 })
726+
runProviderSetup({
727+
onSubmit: async () => {},
728+
showTelemetryNotice: false,
729+
createRenderer: async () => harness.renderer,
730+
})
731+
await harness.renderOnce()
732+
await harness.renderOnce()
733+
const ids = providerChoiceRows(providerChoices()).map((r) => r.id)
734+
for (let i = 0; i < ids.length - 1; i++) harness.pressKey("ARROW_DOWN")
735+
await harness.renderOnce()
736+
const frame = harness.captureCharFrame()
737+
const last = providerChoiceRows(providerChoices()).at(-1)
738+
expect(last).toBeDefined()
739+
expect(frame).toContain(last!.label.slice(0, 20))
740+
})
741+
})

‎src/tui-opentui/provider-setup.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,8 +645,15 @@ export async function runProviderSetup(
645645
})
646646

647647
function listHeight(): number {
648+
// Fixed chrome above the list: root padding, header, intro, step,
649+
// instruction, one populated summary row, and the list box's own
650+
// padding — plus the footer below it, and slack for a long label
651+
// wrapping onto a second terminal row. A tighter budget than the old
652+
// flat -14 so the list actually uses the room a short terminal leaves it
653+
// instead of reserving rows nothing else needs and sitting short.
654+
const chromeRows = 12
648655
const rows = renderer.height || 24
649-
return Math.max(LIST_ROWS_MIN, Math.min(LIST_ROWS_MAX, rows - 14))
656+
return Math.max(LIST_ROWS_MIN, Math.min(LIST_ROWS_MAX, rows - chromeRows))
650657
}
651658

652659
const steps = (): readonly SetupStep[] => stepsFor(choice)
@@ -669,25 +676,32 @@ export async function runProviderSetup(
669676
paddingRight: margin,
670677
})
671678

679+
// flexShrink: 0 on every one of these — like every box sibling below — so a
680+
// short terminal clips the column from the bottom instead of the flex
681+
// algorithm compressing these single-line rows into each other.
672682
const header = new TextRenderable(renderer, {
673683
id: "provider-setup-header",
674684
content: `${PRODUCT_NAME.toLowerCase()} · setup`,
675685
fg: UI.inFlightBright,
686+
flexShrink: 0,
676687
})
677688
const intro = new TextRenderable(renderer, {
678689
id: "provider-setup-welcome",
679690
content: "connect an inference provider — switch later with /model",
680691
fg: UI.textDim,
692+
flexShrink: 0,
681693
})
682694
const step = new TextRenderable(renderer, {
683695
id: "provider-setup-step",
684696
content: "",
685697
fg: UI.action,
698+
flexShrink: 0,
686699
})
687700
const instruction = new TextRenderable(renderer, {
688701
id: "provider-setup-instruction",
689702
content: "",
690703
fg: UI.text,
704+
flexShrink: 0,
691705
})
692706

693707
const summary = new BoxRenderable(renderer, {

0 commit comments

Comments
 (0)