Skip to content

chat: the model picker is a table with headed columns - #1107

Open
ZeroPoint95 wants to merge 23 commits into
devfrom
claude/model-selection-table-f6a1ba
Open

ZeroPoint95 wants to merge 23 commits into
devfrom
claude/model-selection-table-f6a1ba

Conversation

@ZeroPoint95

@ZeroPoint95 ZeroPoint95 commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

/model lists 600 models. You can't scan it. This makes it scannable.

Before

inference-net/schematron-v2-turbo        $0.03/$0.15 per M · 128k
~openai/gpt-astra-latest                       $10/$50 per M · 1M · sees
inclusionai/ling-3.0-flash-vl   $0.06/$0.18 per M · 131k · sees · watches
nex-agi/nex-n2.5-mini:free                                 262k · sees

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:

 › /model filter by name
  model ↓                                   in/M   out/M  window   elo  inputs
  deepseek/deepseek-chat-v3.1              $0.25   $0.95    163k  1135
  deepseek/deepseek-r1                      $0.7    $2.5     64k
  deepseek/deepseek-v3.2                   $0.27    $0.4    163k  1187
› deepseek/deepseek-v4-flash               $0.07   $0.14      1M  1220

alt+s sorts by the next column, alt+shift+s walks back. Every column is two
presses — its own direction, then reversed — so one key reaches every order:

  model                                     in/M  out/M ↓  window   elo  inputs
  ibm-granite/granite-4.0-h-micro         $0.017    $0.11    131k
  deepseek/deepseek-v4-flash-0731          $0.06    $0.12      1M  1252
  openai/gpt-oss-20b                       $0.03    $0.13    131k   947

opens a model's providers — two levels, their own table, their own sort:

  deepseek/deepseek-v4-flash  coreweave   0.4s      1M   24
    auto                                           auto-route based on /settings — coreweave now
    openrouter
      provider    first ↓  t/s    $/M    up  note
›     coreweave      0.4s   24  $0.28   99%  tail 3s
      cloudflare     0.8s   58   $1.3  100%  no tools
      deepinfra      1.2s   27  $0.18   99%  out ≤ 65k
      default

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 / speaks
were 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. esc closes.

auto and openrouter sit together — they're the two things you're choosing
between, and under simple routing 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. enter on openrouter opens it and
lands 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 had
to 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.3 and 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+f do it
without changing your query.

One order for the foot, at every level — grouped by what each key moves:

↑↓ pick · ← back · → providers · alt+s sort · enter <verb> · ctrl+t effort · esc

Four things were plain broken. → providers and ctrl+t did nothing on home's
model list while the UI advertised both. The provider heading was drawn but not
counted, so the fold could overflow. on default closed two levels at once.
And openrouter didn't name → providers when nothing was measured, though the key
worked.

Two bugs worth calling out

The sort read the wrong lane. via, first and t/s cells come from the lane a
row names; the sort asked bestLane instead. So rows with blank cells carried
real numbers into the sort and the blanks didn't group. One reading now serves both.

An alphabetical default buried the best match. gpt returned
anthropic/claude-gpt-echo above gpt-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-laws green. 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/codeaf update tests that failed on every macOS laptop and
passed in CI (t.TempDir gives /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

@CLAassistant

CLAassistant commented Sep 17, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

ZeroPoint95 and others added 14 commits September 17, 2026 18:56
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
ZeroPoint95 force-pushed the claude/model-selection-table-f6a1ba branch from 28c21cf to 04ee366 Compare September 17, 2026 23:05
ZeroPoint95 and others added 5 commits September 17, 2026 19:13
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
ZeroPoint95 marked this pull request as draft September 18, 2026 01:56
ZeroPoint95 and others added 3 commits September 17, 2026 22:17
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
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>
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.

2 participants