Menu

#68 feat(tui): filter the model picker by typing, and stop it leaving stray glyphs

closed
nobody
None
2026-08-07
2026-08-07
Anonymous
No

Originally created by: sosidudku1

Companion to [#67], which raises the catalogs to 337 and 305 models. Based on that branch; merge [#67] first and this retargets to main.

Filtering

Typing in the picker filters the list by substring, case-insensitively. Backspace trims the query, arrows and Enter operate on the filtered rows, and the cursor resets to the top on every keystroke so it can never point at a row that was just filtered away. Backspace with an empty query is a no-op, so it cannot reset the cursor. Ctrl and Meta combos are not typed into the filter; the modal swallows them like every other key, and Ctrl+C keeps working only because handleAppKey runs before the modal handler ever sees the key.

The counter reads 3/12 of 337 when a filter is active, and no match on an empty result set.

Two repaint bugs

Both were visible in a screenshot of the panel before this change: model rows rendering as toolss and toolsls, and the header reading Press ←/→ to switch modeternal llama.cpp.

  1. The box changed height between renders. Ink repaints a frame by overwriting the previous one line for line, so a shorter frame leaves the tail of the taller one on screen. The window is now fixed height and pads with blank rows when the result set is shorter.

  2. Rows were keyed by model id. Ids repeat across a re-filter, and duplicate keys made Ink reuse the previous row's text, which left half-erased names behind (reported as "the last letter of the model sticks"). Rows are now keyed by slot.

The window-start computation is also clamped defensively for lists shorter than the window. The previous expression already produced correct slices in that case; the clamp just makes the invariant explicit instead of implicit.

Testing

12 new tests: query narrows the rows and resets the cursor, case-insensitive substring matching, empty query shows everything, no-match yields an empty list, printable keys dispatch a query update, backspace trims, backspace on an empty query is a no-op that keeps the cursor put, arrows wrap within the filtered list, Enter selects from the filtered list, Enter on an empty result does nothing, Esc closes with a query active, Ctrl combos are not typed.

src/tui/providers: 39 passing. tsc clean. Full suite shows the same pre-existing failures as main.

Related

Tickets: #67
Tickets: #83

Discussion

  • Anonymous

    Anonymous - 2026-08-07

    Originally posted by: sosidudku1

    Thanks for the careful read, all four points addressed in the latest commit.

    1. You were right on both counts. Ids are unique within a render, so the key explanation was wrong and is gone. We read through Ink 7's renderer to confirm the real mechanism: the standard log-update path repaints by erasing the previous frame's line count and rewriting, and shouldClearTerminalForFrame in ink.js switches to a full clear-terminal rewrite once a frame outgrows the viewport, since eraseLines cannot reach rows that scrolled past the top. The artifact showed up exactly when the unwindowed 337-row catalog pushed frames into that regime. The code comment now states what we verified and no more.
    2. Loading and error now pad to the same line count as the ready branch, so no transition changes the frame height, and long ids/errors are truncate-end so wrapping cannot either. The window is also sized from the tab's row budget now (3 to 12 rows), so short terminals get a smaller but still constant window. Covered by height-equality tests at two budgets.
    3. Agreed on the wizard picker. A typeable filter for it is already written and being tested locally; it lands in the next series of model-UX PRs rather than widening this one.
    4. Done. The guard now excludes Tab, Enter, Esc, arrows, PgUp/PgDn and backspace/delete by flag, and rejects C0 controls and DEL by code point before anything reaches the query. Tests cover Tab, PgDn (including a raw [6~ input), arrows and control bytes. Also added a test pinning the trim behavior: whitespace stays visible in the filter line but is ignored when matching.
     
  • Anonymous

    Anonymous - 2026-08-07

    Ticket changed by: Ooooze

    • status: open --> closed
     

Log in to post a comment.