Skip to content

Bound the slash-command and provider-setup pickers so they scroll instead of overflowing - #370

Merged
TheGreatAxios merged 4 commits into
mainfrom
cl-5412-picker-scroll
Aug 7, 2026
Merged

Bound the slash-command and provider-setup pickers so they scroll instead of overflowing#370
TheGreatAxios merged 4 commits into
mainfrom
cl-5412-picker-scroll

Conversation

@TheGreatAxios

@TheGreatAxios TheGreatAxios commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Both tickets turned out to have different root causes, not one shared bug:

  • CL-5412 (slash-command picker): already fixed. It routes through the
    same shared resolveGeometry / createListViewport / visibleSlice
    machinery as the @ mention picker and every other list overlay in
    shell.ts. A 50-entry catalog stays bounded and scrolls correctly at
    every terminal height tested. This PR adds the regression coverage the
    ticket asked for; no source change.
  • CL-5363 (agent configuration provider picker): real bug, now fixed.
    provider-setup.ts is a standalone full-screen renderer that does not
    go through resolveGeometry — it owns the whole terminal directly with
    its own flex-column layout and its own listHeight() budget. Two defects:
    1. Every direct child of the screen's root column needs flexShrink: 0.
      header, intro, step, instruction, statusLine, guidance,
      and footer were all missing it while every sibling box already had
      it. On a short terminal the flex algorithm compressed these
      unprotected single-line rows into each other instead of clipping from
      the bottom — visible as garbled, overlapping text. First caught on
      the provider pick-list (stepe1tofn3i·fproviderrovider — switch later with /model); a first pass fixed only header/intro/step/
      instruction and missed that statusLine/guidance/footer had
      the identical omission, which reproduces independently on the
      failed-connection-test screen once both rows are populated. All
      seven are now covered.
    2. listHeight() reserved a flat rows - 14 for chrome, which is more
      than the picker step actually costs, so the list sat shorter than it
      needed to and the footer could still go missing on terminals that had
      room for it. Tightened to a documented -12; still a hand-derived
      constant, not a computed one — see the follow-up note below.
  • landing.ts / stream.ts: checked for the same class of bug, found
    nothing to fix. landing.ts's boxes are mounted as children of
    shell.ts's resolveGeometry-managed root (via createLandingAbove/
    createLandingBelow), not a standalone tree, so they inherit the
    shell's chrome-collapse discipline instead of doing their own; rendered
    at 24/16/12/8/6 rows and it clips cleanly with no overlap at every size.
    stream.ts has no renderable tree of its own at all — it's row-layout
    math consumed by shell.ts's (also geometry-managed) transcript
    painting. Neither is a second instance of provider-setup.ts's
    standalone-layout risk.

Net effect: +166 / -1 lines. All of the growth is test coverage (three
new describe/test blocks); the fix itself is a 22-line diff in
provider-setup.ts.

Why not one shared fix

CL-5412's picker and CL-5363's picker are not the same code path. The
shell's inset overlays (mentions, palette/slash, permissions) share one
mechanism; provider-setup.ts is a dedicated onboarding/full-screen
surface with its own layout because it runs before a shell exists. Routing
it through resolveGeometry would mean threading a shell-shaped zone
model into a screen that isn't part of the shell's zone system — a much
larger, riskier change for the same outcome the existing
list-viewport.ts machinery it already uses gives it directly. The actual
bug was a missing flexShrink: 0 and an over-conservative constant, not a
missing abstraction, so the fix stays local.

Follow-up (not built here, filed separately)

The narrower convergence between this file and shell.ts is a small
shared helper that computes a chrome budget from an explicit list of
fixed rows, called from both places. That would make the -12 here a
derived value instead of a hand-tuned guess — and, more importantly, it
would have surfaced the missing flexShrink on statusLine/guidance/
footer as "these rows aren't in the fixed-row list" at write time
instead of compiling silently and shipping. Routing provider-setup.ts
through the shell's resolveGeometry itself was considered and rejected:
that resolver operates over the shell's ZoneId height model, and this
screen runs before a shell exists, so adopting it would mean standing up
a degenerate shell or inventing a second zone taxonomy — a real cost, not
an excuse. The bounded-helper version is the one worth doing.

Changes

  • src/tui-opentui/provider-setup.ts: add flexShrink: 0 to all seven
    chrome text rows (header, intro, step, instruction,
    statusLine, guidance, footer); tighten listHeight()'s chrome
    budget from a flat -14 to a documented -12.
  • src/tui-opentui/provider-setup.test.ts: bounded-frame + no-overlap
    regression tests at 24/16/12/8/6 rows, a keyboard-scroll test, and a
    failed-connection-test regression that walks into the
    statusLine+guidance state where the second omission was found.
  • src/tui-opentui/palette-paint.test.ts: bounded-frame + scroll-window
    regression tests for the slash/command palette at the same five heights,
    against a 50-entry fake catalog.

Verification

  • bun run typecheck - clean
  • bun run build - clean
  • bun run test (canonical, ./src ./tests ./evals) - 4050 pass, 0 fail
  • bun test ./src ./tests ./evals --randomize --seed 42 - 4050 pass, 0 fail
  • Rendered real frames (not just green tests) for both pickers at 24, 16,
    12, 8, and 6 terminal rows, with a 40+ entry list, via the existing
    withTestRenderer/createHarness harnesses, and for landing.ts at
    the same five heights to rule it out.

Slash/command palette (50 fake commands), before scroll

height=24: 8 rows shown, bounded, prompt box (message... box) intact below
height=16: 4 rows shown, bounded, prompt box intact
height=12: 1 row shown, bounded, prompt box intact
height=8:  2 rows shown, bounded, prompt box intact
height=6:  0 rows shown (no room), bounded -- no overflow, no garbling

After pressing Down 20 times at height=12 (window scrolls, not just the
cursor):

+----------------------------------------------------------------------------+
| >                                                                          |
| Fake command number 20 with a longish label                                |
+----------------------------------------------------------------------------+
+----------------------------------------------------------------------------+
| message...                                                                 |
...

overlayList height=1 offset=20 active=20 count=50 -- the window follows
the active row exactly.

Agent configuration / provider picker, before fix (height=12)

  stepe1tofn3i.fproviderrovider -- switch later with /model
  pick the provider you have a key or subscription for
  ...

Garbled: the step line and the intro line were compressed onto one row.

Agent configuration / provider picker, after fix

height=24: full 8-provider list shown, footer (up/down move, enter choose,
           ctrl+c cancel) intact, no overlap
height=16: 5 providers shown (incl. one wrapped label), footer intact
height=12: 3 providers shown, no overlap, chrome lines all distinct
height=8:  chrome only (no room for the list), no overlap, no overflow
height=6:  chrome only, bounded, no overflow

Example, height=24 after the fix:

  corbits code . setup
  connect an inference provider -- switch later with /model
  step 1 of 3 . provider
  pick the provider you have a key or subscription for

  > provider
    api key       --
    model         --

   > OpenAI ChatGPT -- Login via Browser -- ChatGPT Plus/Pro subscription
     OpenAI API -- API key -- Paste your OpenAI API key (sk-...)
     xAI Grok -- SuperGrok or X Premium+ subscription
     OpenCode Go -- OpenCode Go subscription -- paste your API key from
  https://opencode.ai/auth
     OpenCode Zen -- OpenCode Zen pay-as-you-go credits -- paste your API
  key from https://opencode.ai/auth
     Z.AI Coding Plan -- Paste your Z.AI Coding Plan API key from
  https://z.ai
     Anthropic -- Paste your Anthropic API key (sk-ant-...)
     Google -- Paste your Google AI Studio API key
     Custom -- any OpenAI-compatible endpoint -- you supply the name, base
  url and model

  up/down move . enter choose . ctrl+c cancel

Provider picker, failed-connection-test state (height=16, after fix)

The state that exposed the second flexShrink omission: statusLine and
guidance are both populated at once, and now render as two clean rows
instead of merging into each other.

  corbits code . setup
  connect an inference provider -- switch later with /model
  step 3 of 3 . model
  pick the model to start with

    provider      OpenAI API -- API key
    api key       ******
  > model

   > OpenAI API -- API key / gpt-5.4
     OpenAI API -- API key / gpt-5.4-mini
     OpenAI API -- API key / gpt-4.1
     OpenAI API -- API key / o3
  [ramp glyph]    connection refused
  the key was rejected or unreachable -- esc to re-enter it, enter to retry,

Closes CL-5412
Closes CL-5363

@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

CL-5412

CL-5363

Every direct child of the setup screen's root column needs flexShrink:
0. header, intro, step, instruction, statusLine, guidance, and footer
were all missing it while every sibling box already had it, so a
terminal too short for the full column let the flex algorithm
compress these unprotected single-line rows onto each other instead
of clipping from the bottom. Reproduces on the provider pick-list and
independently on the failed-connection-test screen, where statusLine
and guidance are populated together.

The list height budget also reserved more chrome rows than the picker
actually uses, so the footer could go missing even on terminals that
had room for it.
Investigation found the picker already routes through the shared
list-viewport/resolveGeometry machinery: a 50-entry catalog stays
bounded at 24, 16, 12, 8, and 6 terminal rows, the prompt box below
it stays intact, and moving the selection scrolls the window so the
active row is always visible. No sizing logic was missing, so this
adds the regression coverage without a second windowing path.
@TheGreatAxios
TheGreatAxios force-pushed the cl-5412-picker-scroll branch from e0544b2 to dd66dad Compare August 7, 2026 14:29
@TheGreatAxios

Copy link
Copy Markdown
Collaborator Author

Review summary — two rounds, held open for human review

Ready for your call. Not merged.

CL-5363 — the real fix

The provider setup screen garbled text on short terminals because rows added as direct children of root lacked flexShrink: 0, letting the flex algorithm compress them into each other. Reproduced verbatim on unmodified main at height 12:

stepe1tofn3i·fproviderrovider — switch later with /model

The first round fixed header/intro/step/instruction. Review then found the identical bug still live on statusLine/guidance/footer, reachable in the failed-connection-test state that the tests did not drive:

the█key░was rejectedoorrunreachable — esc to re-enter it, enter to retry,

Both rounds are now in. Independently verified: all twelve direct children of root carry flexShrink: 0header, intro, step, instruction, summary, listBox, loginBox, inputFrame, statusLine, guidance, telemetry, footer. That includes summary and telemetry, which neither round named explicitly.

Regression coverage now drives the flow through provider pick → API key → failed connection test, reaching the state the first round's tests missed.

CL-5412 — no source change, coverage only

Independently confirmed: the branch's palette-paint.test.ts, unmodified, passes 18/18 against clean origin/main. The slash-command picker already bounds a 50-entry catalog at 24/16/12/8/6 rows and scrolls the active row into view. The claim is accurate.

Other renderers ruled out with evidence

landing.ts mounts as children of the shell's resolveGeometry-managed root rather than a standalone tree, so it inherits chrome-collapse handling; rendered at all five heights it clips cleanly. stream.ts has no renderable tree of its own. Neither reproduces the bug — stated rather than silently skipped.

Architectural note, filed separately as CL-5599

Routing provider-setup.ts through the shared resolveGeometry was examined and rejected on real grounds: that resolver operates over the shell's ZoneId height model, so reusing it pre-shell would mean a degenerate shell instance or a second zone taxonomy.

The narrower convergence is a shared helper computing the chrome budget from an explicit list of fixed rows. That matters beyond tidiness — it would have surfaced the missing flexShrink on statusLine/guidance/footer as "these rows aren't in the fixed-row list" instead of as garbled output two review rounds later. The 12 in listHeight() remains a documented estimate rather than a derivation; degradation is graceful down to height 6 (clipping, never garbling), so it is not blocking.

Typecheck and build clean. 4050 pass / 0 fail, identical under --randomize --seed 42. Commit hygiene clean — two verb-first subjects, no over-length body lines, no ticket references in bodies, nothing under vendor/.

listHeight() used a flat constant for the rows above and below the
pick-list, re-guessed by hand whenever a row was added or removed.
Compute it from the same building blocks the tree is made of instead:
the seven always-visible single-line rows, root and listBox padding,
the summary box's per-step rows, and the telemetry notice when shown.
The derived chromeRows computation (singleLineRows, rootPadding,
listBoxPadding, summaryRows) reads as principled but is still a
hand-maintained count of the layout, now spread across four
constants instead of one. Nothing catches it going stale when a
row is added, since the tests cover height bounding, not the
count, and the Math.max hedge covers a PRESET/OAUTH steps
divergence that does not exist.

Restored rows - 14 with a comment stating plainly that it is a
guess: listHeight() runs before root is constructed, so there is
no OpenTUI layout pass yet to measure a real chrome height from.
Renderable.height and scrollHeight only reflect the last completed
layout, populated post-mount, so they cannot help here. A shared,
derived chrome budget for this and shell.ts's picker is tracked
separately.
@TheGreatAxios
TheGreatAxios merged commit 0978d6b into main Aug 7, 2026
3 checks passed
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