chat: the model picker is a table with headed columns - #1107
Open
ZeroPoint95 wants to merge 23 commits into
Open
ZeroPoint95 wants to merge 23 commits into
ZeroPoint95 wants to merge 23 commits into
Conversation
A /model row drew its facts as one right-aligned `·` tail, so no two rows put their price, window or score in the same column and six hundred names could not be read down. The list is the one list on this surface that is read DOWN: the question in front of somebody scrolling it is "which of these is cheap", "which holds a million tokens", "which can see", and every one of those is a comparison between rows. So the facts get columns — via, first, in/M, out/M, window, t/s, elo, can — under one dim heading line that carries the units the rows used to spell on every one of them. The ranking is modelFields' own ranking laid out down the page instead of along the row, it gives up its columns from the same low-ranked end, and it falls back to the ranked tail wherever a frame has no room for columns (and always at tierPhone). Both shapes are one reading of the model now (modelFacts), so a figure cannot mean dollars per million in the table and something else in the tail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… own heading Two questions the table's first reader asked that the corpus could not answer well. The capability words: the page listed all six as though any of them could turn up on a model row, and three of them never can — a model publishing any non-text output modality is not a conversation model and is off the /model list entirely, so draws, speaks and films belong to the media slots. The page says which side each word reads, which three appear where, that the cell reports published modalities only and never infers from the id, and that the looking slot DOES fall back to a name — so a silent …-vl row can be offered there while showing nothing under `can`. The tilde: `why are some model names prefixed with a squiggle` and `why does this model start with ~` both missed the page that answers them, because the tilde shared a heading with `…-latest`. It has its own now, and the unmeasured-model paragraph that was buried in the unpinning section has one too. Six probes for the table's columns and two for the tilde are on the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The row's capability words were six invented verbs — sees, hears, watches, draws, speaks, films — and two things were wrong with them. A verb had to carry the SIDE as well as the thing (`sees` is "image, on the way in"), so six words had to be learned before a row could be read, and the words were the only place the side was written down. And they were lossy where the catalog is not: `speaks` folded speech, audio and music into one word, so a model that writes songs and one that reads a paragraph aloud drew the same row. A table has a head over every column and a head can say the side for the whole list. So the one `can` column becomes two — `reads` and `makes` — and the cells are the catalog's own nouns: image, audio, video, file, speech, music. Nothing has to be learned and nothing folds. The order is ours, because the catalog has none: it publishes the same set as `text, image, file`, `file, image, text` and `image, text, file` on neighbouring rows. A word this build has never seen is still drawn, after the ones it knows. `makes` is never drawn in /model — a model answering with anything but text cannot hold a conversation and is not on that list — so it costs nothing there and is the column the media slots exist for. modelNameWide drops from 44 to 38, measured: of 355 chat models in a live catalog 343 have ids of 36 cells or fewer, and sizing the name column for the twelve outliers cost every row the whole `reads` column. sees and draws survive only in the filter box, and the comment says why: `image` is a word dozens of model ids carry, so it cannot be a filter term without breaking the search for qwen/qwen-image-3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Asked why `makes` never appears, and the measurement answered more than the question did. Across every list this surface has, `makes` has exactly ONE distinct value: empty on /model, because a model you can converse with answers in text and nothing else; `image` on all 54 rows of the drawing slot; `speech` on all 18 of speaking; `video` on all 29 of filming. Four lists, four constants, and a head over each that a person reads before finding out it says nothing — the list's own definition written once per row. So a modality column is dropped when every row agrees on it, not only when nobody published it. Law 4 was the case where the one thing every row says is nothing. It is asked of `reads` and `makes` alone, and the restriction is what makes it safe: constancy means "this is the filter" only where a filter could have caused it. Two models that cost the same are a coincidence, and a price column that vanished because a short list agreed with itself would hide a figure somebody came to read. The emptiness law is about facts nobody published; it may not grow into hiding facts that were. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three changes the owner asked for, and one they did not have to. The heads are `inputs` and `outputs` rather than `reads` and `makes` — `reads image` was always slightly wrong, since you look at a picture rather than read one, and the pair reads as a pair. `text` is named. Leaving it out was the emptiness law applied one step too far: the cell it produced was BLANK under a head saying `inputs`, which does not read as "text, like everything else here", it reads as "nothing". A row that published no modalities says `text` too, which is the one silence law drawn where somebody can see it. The cells are joined by a space rather than `, `. Three cells on the widest row, and the widest row is what a column is sized by — with commas and `text` the column needed 101 cells of measure and would never have been drawn at all. And the one that fell out: the TAIL still leaves `text` out. Same reading, asked with withText false. A cell has to be true of every row under its head; a tail carries what tells rows apart, and `inputs text · outputs text` is the same nine words on five hundred rows, spending room the window and the price are queueing for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…wn keys Three things the owner asked for. `text` is out of the modality cells again — tried in for one wave on the argument that a blank cell under `inputs` reads as "nothing", taken back out because what a cell is for is what the model does BEYOND holding a conversation, and every model on the list reads and writes text. Two blank cells mean text in, text out. The heads stay `inputs` and `outputs`. And the keys the box promised: `→ providers` and `ctrl+t` did nothing whatever on home's model list. The door never armed the fold, so there was nothing to open, and it answered neither key — while the placeholder under the list named both. Home arms the fold now, enter on a provider pins it, and ctrl+t walks the rung. The rung is held on the DRAFT and spent when the next conversation attaches, because nothing behind home may be touched and an agent this window happens to be holding is a conversation nobody is looking at. The keys also moved off the placeholder and onto the foot. A placeholder is the one line on screen that disappears the moment somebody uses it — so `→ providers` was gone by the first typed character, which is exactly when a person has found their model and wants its machines. The foot stays, and it can say what each key does WHERE it does it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It read `baseten: first token 0.4s, steady 64 t/s, no tail — from the sheet` under a row that already read `baseten 0.4s · 64 t/s · $1.2/M · out ≤ 32k · 69%`. The same three numbers in prose, under a name the row had just said, one line further from the eye — and it cost the fold a row every time the cursor moved, on the one list where a row is a machine being compared against fifteen others. The only thing it added at the end was where the figures came from, which is one fact about the whole list rather than about the row the cursor happens to be on; the page that explains the ledger says it once instead. laneWhy goes with it. lineUnder keeps its other case — the line that stands where the providers would be when nothing behind a model has been measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… are Asked what the difference between the two rows is, and the honest answer was "under the row you ship with, none that a request can see" — both send no provider choice. They differ in two ways nothing on screen said: auto starts choosing the moment routing is latency or price, and openrouter also stops codeaf buying a speed probe. So the rows say what they ARE now. `auto` is `codeaf tries to pick the best provider`, and under `simple` it carries `— not while routing is simple`, because a sentence true only on a setting most people have not got is a sentence that lies on the default install. `openrouter` is `default routing`. They stand beside each other above the machines instead of at either end of them. They are the two rows a person is choosing between, and telling them apart means reading them side by side rather than a screen apart. And the bullets go. `filled chooses for you, hollow declines to` is not true under `simple`, where neither chooses — a mark that is wrong on the default install is worse than no mark, and the indent already says these two are not machines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`→` on a model now opens two answers and nothing else: `auto`, which reads `automatic routing according to /settings (recommended)`, and `openrouter`, which reads `default routing`. `→` on `openrouter` opens the machines it routes to and walks the cursor in; `←` closes one level at a time. The machines live there because every one of them is a machine that row routes to — naming one is a narrower answer inside it rather than a third thing beside it. The machines are drawn as a table with their own heading, by the same engine as the model list: modelTable is colTable now, built over a column set, and laneColumns is the second set. A `·` tail put each provider's price wherever that provider's note happened to end. And they are drawn alphabetically. The ledger's own order — fastest first — is still what auto, bestLane and the model row's `via` read; the picker sorts its own copy after taking the prediction, because an order that moves whenever a belief updates is an order the eye has to start over on. The `auto` sentence stopped naming `simple`. It is a value of a `routing` setting that nothing on that screen mentions, and the owner — who owns the program — read the row and asked what it meant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `openrouter` row said `default routing`, which described one of the things inside it rather than the row itself — so the sentence is gone and the answer is where it belongs: `default`, the last row after the machines, with empty cells. It is the same kind of choice as the machines above it — "serve this from here" — and somebody reading down that list should not have to leave it to pick the one that declines to pick. The cells are empty and honestly so: nothing has been measured about whichever machine the router feels like, because it is not one machine. `enter` on the container still writes the same thing, one press earlier. And the line about a model nobody has measured moved into that fold, where the machines would be — which is where a person went looking for them. The fold opens with nothing measured now; `→` on it used to do nothing at all, and `default` is always in there to be chosen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It closed on the press, which made every choice final and every comparison a round trip: pick a model, watch the list vanish, type /model again to see what the other one cost. The list is a table now — a thing built to be read down and compared — and a table that shuts the moment you touch a row is a table you can use once. So enter applies and leaves it up, and esc is the way out. Applying is safe to repeat: switching twice lands on the second, pinning twice writes the second row. All four doors moved together, because there is one list. The marks follow the choice (picker.restate). They stay snapshots against everything running underneath — that is what the freeze is for — but the person pressing enter is not running underneath, and a list still marking the model they had just left would be the one row on it that was false. The manual claimed esc gives the model in use back as it was. It does not any more: enter already changed it, and esc only closes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `auto` row is `auto-route based on /settings`. And a table's column heads are painted apart from their cells. Both were dim, so `provider first t/s $/M` beside `cloudflare 0.8s 58 $1.3` was the same text twice — nothing on screen said the first line was the labels rather than a provider whose numbers had gone missing. They are muted now, and italic besides: the hue does the work on a colour terminal and the italic does it on one whose two dim tiers land close together. Muted and not ink, because a heading is read once and the figures under it are read every time; a head louder than its own column is a label shouting over the thing it labels. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The picker's box carried a small query language beside the name search — `@cloudflare`, `<1s`, `>50t/s`, `$<0.3`, `fp8`, `tools`, `sees`, `draws`, and `fast`/`cheap` to reorder — and every one of those terms was asking about a FACT. The facts are columns now, so the question they answered is one a person can see the answer to, and a syntax nobody can discover is a feature only its author can use. So they are gone, not reinterpreted: `$<0.3` is looked for as a name, matches nothing, and the list comes back empty rather than quietly answering the old question. `sees` still finds deepseek-v4-flash, because those letters run through that id in order — and no longer because the model can see. `/model <one-word>` is always a slug now; the two-words-open-the-picker rule and `/model @name` are untouched. Typing inside an open provider fold still narrows that model's providers by name (#1022), which is a name search too. `app.setLevel` picks up the one door call that removing the grammar exposed: ctrl+t in the picker and home spending its draft's rungs were `SetReasoningFor` plus `keepLevel` written out twice, and the off-loop ratchet counts call sites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things the rebase onto dev made visible. The change entry was numbered 1106, which is somebody else's merged pull request — the wordmark (#1106) — so the one file whose whole job is to tell the next reader what moved was pointing at the wrong change. CI only checks that an entry was added, so nothing would have caught it. And `cmd/codeaf/models.go`'s comment still described the modality tail as `reads image, video · makes image`: the heads became `inputs`/`outputs` and the separator became a space two waves ago. The comment exists to say this line and the picker row have ONE spelling, so a stale example there is the drift it was written to prevent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ZeroPoint95
force-pushed
the
claude/model-selection-table-f6a1ba
branch
from
September 17, 2026 23:05
28c21cf to
04ee366
Compare
TestC9SourceBuildRefusesBeforeTheNetwork and TestC9AnInstallEndsWithTheNewBinarysVersionLine failed on every macOS laptop and passed in CI: `t.TempDir` answers under /var/folders/…, /var is a symlink to /private/var, and on Linux the two spellings are one. The product is right and is not touched. ExecutableTarget runs the path from os.Executable through filepath.EvalSymlinks on purpose — an installer has to replace the real file, and writing through a symlink replaces the link or lands somewhere nobody asked for. A test that hands that door an unresolved path and then demands its own spelling back is asserting the opposite of the law it is testing. So the path is canonical at its source: `updateTarget(t)` resolves the directory (the file does not exist yet, and EvalSymlinks needs what it is given to exist) and joins the name. All nine sites in the file use it, so the seven that do not compare paths today cannot start failing when they do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… openrouter TWO GESTURES SHARE ONE PAIR OF KEYS and position alone was the wrong way to break the tie. `→` and `←` were the tree's only at the very end and start of what was typed, so leaving an open fold cost one press per character in the box: with `deep` typed, `←` stepped through p, e, e, d while the fold stayed open. Time breaks it now as well. Somebody still editing is still pressing keys, so 600ms of quiet (pickerQuiet) hands the arrows back to the tree — chosen against typing cadence, which puts 100-200ms between keys, so it cannot land inside a word. The position rule is kept on top of it, which is what makes an empty box behave exactly as it always did. Coming back is any edit at all, and `ctrl+b`/`ctrl+f` are never the tree's, so they take the caret back without changing a letter of the query — the mode cannot trap anybody. The walk deliberately does not count: reading a filtered list must not quietly take the arrows away from the tree. And the decision is made by asking whether the text or the caret MOVED, not by keeping a second list of editing keys beside listNavigate's own switch. The foot now names whichever of the two keys actually works, because a foot saying `←` while `←` stepped through a word is what made the fold feel broken. Separately: enter on the bare `openrouter` row opens its list as well as choosing. It writes what it always wrote — the same answer `default` inside it writes, one press earlier — but shut, that read as "you have chosen openrouter", which sounds like a destination and hides that there was a list underneath. Open, the cursor lands on `default` wearing the mark and it reads as the true sentence. It needed no new state: marked already gave the container the mark only while shut, and cursorToMachine already walks onto the marked row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A paste reaches the filter box without going through picker.navigate, so the quiet window never saw it: text arrived and the box still read as untouched, which made a paste the one way to put characters in there that did not count as typing them. `←` straight after would have gone to the tree. picker.pasteFilter is the one door clipboard text now takes into either picker's box — chat's and the settings panel's — and it stamps. It also owns the insert-then-rank pair those two sites were each writing out, which is a pair that only has to be forgotten once to leave a filtered list showing rows the query excludes. Opening the list with a query already in it deliberately does NOT stamp. `/model deep flash`, and home's and a room's same command, finished the query before the list existed — the hands were never at this box — so the arrows are the tree's on the first frame, which is the whole reason that door exists. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
foldHere asked whether the cursor was on a row inside the open machines with `row.lane >= 0` — which is every real machine. `default`'s lane number is negative, like the two container rows' are, so it was missed when `default` stopped being a note beside `openrouter` and became a row under it: `←` there shut the model's whole fold instead of just the machines, and the way in and the way out were different lengths. Found by checking the press that follows the gesture in the commit before this one. `enter` on `openrouter` now lands the cursor on exactly that row, so `←` is what a person does next — the skip was reachable by the most likely route to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A table makes a screenful comparable by eye. Over six hundred rows the eye is not enough and ordering by the column is the only thing that is — so `alt+s` walks the columns and `alt+shift+s` turns one round, which is the tasks place's own pair of chords, read in picker.foldKey so all four doors get them. A chord rather than a bare `s` for that page's reason: `s` is one of the commonest letters a filter starts with. This is also where the filter box's `fast` and `cheap` went. The objection to them was never sorting — it was that a word typed into a name box is an undiscoverable way to ask for it, and that two words were two opinions about six columns. Four laws, three of them found by trying it against the real catalog: `list` is IN the cycle, because it is the name search's own best-match order. A sort you cannot undo would throw that away for the life of the list. A column this list published nothing in is SKIPPED. With nothing measured there is no `first` and no `t/s`, and the second press of the key moved no row and painted no arrow — indistinguishable from a broken key. Rows that published nothing sort LAST both ways round. A model with no price is not the cheapest, and turning the column round must not make it the dearest: it is out of the comparison. The `cheap` word this replaces got that right in one direction only, by calling an unknown price infinity. The cursor lands on the TOP, not back on the model in use. Reversing a column looked like it had done nothing, because rank() was pulling the cursor to the current model and the window stayed around it — showing somebody who asked for the cheapest model their own row's neighbourhood. And the arrow is MEASURED, not appended: colTable.fit takes it, so the column is two cells wider rather than the block being pushed past the measure and dragging every right-aligned row under it. picker.measure splits out of tableFit for the same reason those were never one question — the measurement is over the rows and the fit is over a width, and the cycle needs the first without the second. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ZeroPoint95
marked this pull request as draft
September 18, 2026 01:56
Four things, and a fifth the first of them broke. EVERY COLUMN IS TWO RUNGS — its own direction, then reversed — so alt+s alone reaches every order a table has. alt+shift+s retraces the cycle instead of meaning "reverse", which is what it meant while a column was one rung. Walking back off the first column wraps to the last, which is where walking forward came from; it was jumping to the last SORTABLE column from anywhere, so the two directions were not each other's opposite. EVERY TABLE IS ALWAYS SORTED and always shows its arrow. tableSort's zero value is each table's first column — the name — ascending, so there is no unsorted state to explain. THE PROVIDERS SORT TOO, with their own tableSort and their own arrow, and the key orders whichever table the cursor is in — the rule the fold's other keys already follow. The two orders are independent. note and last 8 do not sort: a note is whichever ONE thing is worth saying about a lane, so ordering by its text would rank "bad replies" against "tail 3s" alphabetically, and a sparkline has no single value. `up` STANDS BEFORE `note`, in laneColumns and laneCells together. Uptime is a figure and reads down its last digit with the figures before it; prose in the middle of a run of numbers breaks the run. It is the drop order too, so a narrow fold keeps the comparable figure over one row's own caveat. And the fifth: AN ALPHABETICAL DEFAULT BURIED THE BEST MATCH. `gpt` answered anthropic/claude-gpt-echo before gpt-5-classic, which is the search itself going wrong rather than a matter of taste. The name column reads queryScore first while the box has text and is alphabetical inside a tier; with an empty box every score is equal, so the arrow still means the whole column. Ten existing tests encoded the catalog's order while testing something else. Most now look their row up by name rather than by index. One was a real conflation: chatOnly stood for both "which models pass the chat question" and "what order a picker shows them", which made a sort look like a filter regression, and is two slices now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE FOOT READS THE SAME WAY AT EVERY LEVEL: ↑↓ pick · ← back · → providers · alt+s sort · enter <verb> · ctrl+t effort · esc grouped by what each key MOVES. The first two move the cursor — walking out of a fold is a cursor move, so `← back` belongs beside the walk rather than stranded on the far side of the only key that commits anything, which is where it was. The next two change the list. Then enter, where the eye stops. Then ctrl+t, the one key here that touches neither the cursor nor the list: it dials a setting on the row and leaves everything as it was. Home says `enter choose` now. Every other row of the same fold already said choose, so `use it` was a second word for one gesture on one rung of it. Two things the ordering pass exposed. The `openrouter` row did not name `→ providers` when nothing was measured, though the key worked — the hint still asked for a believed machine, which stopped being true the moment `default` became a row inside that fold. And a machine's row did not name the sort, though alt+s orders the providers from in there. Pinning a model at home no longer writes a line under the box. It read `model · <name> · for the next conversation you start here`, and the rule above the box already carries the pinned model — for as long as the pin lasts, rather than until the next note replaces it. And the `/model` a person typed stays in the box as the chip it was, with the filter after it: `› /model filter by name`. The list is a different editor from the one the command was typed into, so those words vanished from the one line they were looking at and left a `›` and a grey phrase that could have belonged to any list on this surface. draftBlockFull is the one function all four box shapes go through now, and the tack's width is paid by head — so the wrap room, the caret column and every continuation row's indent follow from it without anything downstream knowing a tack exists. It still fits whole at sixty cells. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y tie Two reasons the sparse columns did not behave. THE SORT ASKED THE WRONG LANE. pickerRankOf read bestLane — the quickest-feeling lane the ledger holds — while the `via`, `first` and `t/s` CELLS come from the lane the row NAMES: the pin, or the chooser's answer, or nothing at all under a routing row where codeaf does not choose. So a row whose cells were blank still carried real numbers into the sort, and the blanks did not land together. modelLaneReading is that one reading now, asked by modelFactsOf for the cells and by pickerRankOf for the order, with the pin read for the row in use alone exactly as the cells read it. AND THE TIES WERE LEFT TO CHANCE. A sparse column leaves a whole block of rows it cannot tell apart, and a stable sort leaves that block in whatever order the rung before it produced — so the same press drew a different screen depending on how it was reached, which reads as the sort being arbitrary at exactly the place a person is least sure of it. The name breaks every tie now, so the blanks come back alphabetically and a second press draws the same screen. tableColumn.words marks the columns holding names rather than figures. `via` is the only one, and it needed saying on the COLUMN: a pair that both published nothing fell past the name test and was compared as zeroes, which is one rule for the column and a different one for some of its pairs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ZeroPoint95
marked this pull request as ready for review
September 18, 2026 13:08
AbirAbbas
added a commit
that referenced
this pull request
Sep 18, 2026
…1195) modelConnectionStatus renamed every custom-id row to the connection's Written word, and the catalog template's Written is the bare id, so a person who had connected nothing yet saw `custom` where the manual, the README and #1107 say `Custom OpenAI-compatible API`. Only a connected instance is called what the person called it now; the contract test draws both rows and is red without the fix. Found verifying the staging build of #1194. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
dev landed the crew family, the Model Pool and custom model connections, so `/model` now draws a dim heading per service over the rows it holds. The one conflict was the hint grammar's table — both sides had rewritten the picker's foot — and the merge surfaced one real defect: the sort ignored the service groups, which moved a lonely connection above `openrouter` on a column it had published nothing in, and would have drawn one heading twice. The service order is the outer key of every sort now, so a column orders the rows inside a service and never the services; and `pickerRankOf` reads a price as both halves or neither, the way the cell does, so half a price sorts with the blanks rather than among the cheap rows with an empty cell. 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.
/modellists 600 models. You can't scan it. This makes it scannable.Before
Every figure correct. No two rows put the same fact in the same place.
After
Real renders, owner's live catalog, 108 cols. Opens alphabetical:
alt+ssorts by the next column,alt+shift+swalks back. Every column is twopresses — its own direction, then reversed — so one key reaches every order:
→opens a model's providers — two levels, their own table, their own sort:Narrow frames fall back to the old tail — it's better per row, only worse down the
page.
What else moved
Columns, not verbs.
sees/hears/watches/draws/films/speakswere six invented words each carrying a side and a thing. Now the catalog's own
nouns under
inputs/outputs. Blank = text in, text out.Enter no longer closes the list. It applies and stays open, so you can compare
two models and change back.
esccloses.autoandopenroutersit together — they're the two things you're choosingbetween, and under
simplerouting they send the same thing. Dropped the bullets(they claimed a distinction the wire doesn't make) and the dim paragraph that
re-printed each row's own numbers in prose.
enteronopenrouteropens it andlands on
default, which reads as "these exist, and you've chosen not to pick".The filter box searches names. Only names. It used to also take
@cloudflare,<1s,>50t/s,$<0.3,fp8,tools,fast,cheap— a query language you hadto read a manual page to know existed. Those were questions about facts, and the
facts are columns now, so you can just look. Type
$<0.3and you get an empty list,not a silent reinterpretation.
→/←stop fighting the caret. They're the tree's keys unless you're mid-typing(within 600ms of the last change to the box). Leaving an open fold used to cost one
press per character typed. Any edit takes the caret back, and
ctrl+b/ctrl+fdo itwithout changing your query.
One order for the foot, at every level — grouped by what each key moves:
Four things were plain broken.
→ providersandctrl+tdid nothing on home'smodel list while the UI advertised both. The provider heading was drawn but not
counted, so the fold could overflow.
←ondefaultclosed two levels at once.And
openrouterdidn't name→ providerswhen nothing was measured, though the keyworked.
Two bugs worth calling out
The sort read the wrong lane.
via,firstandt/scells come from the lane arow names; the sort asked
bestLaneinstead. So rows with blank cells carriedreal numbers into the sort and the blanks didn't group. One reading now serves both.
An alphabetical default buried the best match.
gptreturnedanthropic/claude-gpt-echoabovegpt-5-classic— the search itself going wrong.The name column now orders by relevance first, alphabetically only inside a tier.
Both found by trying it against the real 521-row catalog rather than a fixture.
Proof
make pr-ready+make test-lawsgreen. 51 new tests, 30 new manual probes.Manual updated, including a page that says the filter grammar is gone and points at
the columns instead — the old keystrokes are in its heading so they still retrieve.
Also fixes two
cmd/codeafupdate tests that failed on every macOS laptop andpassed in CI (
t.TempDirgives/var/..., which the update door resolves to/private/var/...on purpose). Product untouched.Still red on clean
origin/dev, not mine:TestTheOpeningHintNamesBothDoors,internal/release's ledger test (#1060).🤖 Generated with Claude Code