Skip to content

feat(models): offer the models the installed CLIs can actually reach - #538

Open
Jeelsblog wants to merge 1 commit into
chaitanyagiri:mainfrom
Jeelsblog:feat/live-model-catalog
Open

Jeelsblog wants to merge 1 commit into
chaitanyagiri:mainfrom
Jeelsblog:feat/live-model-catalog

Conversation

@Jeelsblog

@Jeelsblog Jeelsblog commented Sep 16, 2026

Copy link
Copy Markdown

What & why

The model pickers are fed entirely by curated lists — src/shared/modelCatalog.json baked into the build, overlaid by docs/model-catalog.json. Neither knows what the machine in front of the user can actually reach, which depends on their account, plan and keys. On this machine opencode models reports 384 slugs and agy models reports 14; the pickers offered 8 and 12. A model you hold a key for was simply absent until someone hand-added it upstream.

So ask the CLIs, and append their answer to the curated list. The curated list stays the floor and wins every collision — it carries the readable labels, the editorial ordering (recommended picks first) and any version bounds, and the CLI only knows the raw id. Only the appended tail is sorted, because a CLI emits its list in no order a human scrolling 380 rows can predict. A CLI that is missing, logged out or slow contributes nothing, degrading to exactly the behaviour that ships today.

Three things this had to get right, each of which is a test in test/model-catalog-cli.test.cjs:

  • agy --model takes the DISPLAY NAME, not the slug — which is why every curated antigravity entry already carries an id like Gemini 3.1 Pro (High). Reading the slug column yields both a value agy rejects and a dedupe that never matches its curated twin, so the picker fills with duplicates that cannot be selected. A row with no display name is dropped rather than backfilled from its slug, since the slug is precisely the value agy refuses.
  • A packaged app does not inherit the login shell's PATH. Launched from the desktop, an .AppImage or .deb never found agy or opencode and the probe failed silently, so the user's real PATH is injected via the existing userShellPath().
  • agy models round-trips to Google and takes ~6s, so the 5s default timeout returned empty on a healthy machine. Raised to 15s, and the answer is cached for 10 minutes — cached only when non-empty, so one transient failure doesn't pin the pickers short for the whole window.

The CLI probing, parsing and merge live in src/main/modelCatalog.ts beside the remote-catalog loader as pure exported functions, with the IPC handler reduced to wiring — that's what makes them testable without spawning anything.

On "one change per PR"

This touches main and renderer, so it's worth saying why it isn't splittable. The picker could not physically take the result: AddAgentModal / EditAgentModal rendered one chip per model into an unbounded flex-wrap row with no scroll cap, so 391 entries push the rest of the form off the modal. Landing the fetch alone would ship a broken Add Agent dialog. The chip grid therefore moves to a shared ModelChipPicker that grows a filter box and a scroll cap past 24 options and is byte-identical below that threshold, so every curated provider looks exactly as it does today.

Happy to split it if you'd rather review them separately — say the word and the picker change becomes the prerequisite PR.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Docs
  • Build / CI

Evidence

Both shots are the Add Agent → Engine step: same window size, same theme, same harness config, taken from npm run dev on this branch's parent and on the branch.

Before

Screenshot from 2026-09-16 10-15-39 Screenshot from 2026-09-16 10-15-47

After

Screenshot from 2026-09-16 10-16-34 Screenshot from 2026-09-16 10-16-47

How this was tested

OS: Zorin OS 18.1, kernel 7.0.0-31-generic, X11. Electron 32.3.3, Node 22.22.3. Linux only — I have no macOS or Windows machine to verify on, and the Windows path is reasoned about below rather than run.

  • npm run typecheck — clean (both node and web TS projects).
  • npm run test:focused849 pass, 0 fail (834 existing + 15 new).
  • npm run build — clean.
  • Against the real CLIs on this machine: 7.0s cold probe, 0ms cached; opencode 8 curated + 383 appended = 391; antigravity 12 curated + 6 appended = 18; zero duplicate ids in either.
  • Built the AppImage and ran it from ~/.local/bin to confirm the packaged-PATH fix works end to end — this is the path that was silently failing, so it needed a packaged run, not a dev run.

New i18n keys are added to all three locales (en, ar, zh-CN).

Agent review of this PR

Run as asked, against the brief in CONTRIBUTING (cross-platform, paths with spaces, provider neutrality, hot paths, security). What it flagged, including what I chose not to change:

Fixed as a result

  • parseAgyModels had a || slug fallback for rows with a blank display name that the line-level .trim() made unreachable — and had it fired, it would have put a slug on the command line, which is the exact value agy rejects. The bug this PR exists to fix, rebuilt in the error path. Rows with no display name are now dropped explicitly.
  • Two native <select>s (CommandCenterPanel, OnboardingWizard) were keyed on m.label. Safe only until a live list merges in and two entries share a label; keyed on m.id now.

Flagged, not changed — with reasons

  • Paths with spaces: antigravity ids are display names containing spaces and parens (Gemini 3.1 Pro (High)). That's pre-existing and correct — buildSpawnCommand quotes them and the shared tokenizer keeps them whole. This PR adds more such ids but no new handling, so it rides on the existing path rather than a new one.
  • Windows: probeCli passes shell: process.platform === 'win32' and resolves through the existing resolveCommand, matching how every other CLI spawn in main already works. Unverified on Windows — I can't run it. A reviewer with a Windows box should sanity-check the probe before this is relied on there.
  • Provider neutrality: only opencode and antigravity are probed, because those are the two CLIs whose live list I can actually verify. The probe table in loadCliModels is one line per provider, so cursor-agent models / grok models are a cheap follow-up — deliberately not in this PR.
  • Hot path: models:catalog is not hot, but it was spawning two processes per call and blocking ~7s on the network. Now cached for 10 minutes, and only non-empty answers are cached so a transient failure doesn't pin the pickers short.
  • Security: no new input reaches a shell. The probe runs a fixed models argument through resolveCommand, and CLI stdout becomes picker labels and --model values that already flow through the existing quoting. Worth a reviewer's eye regardless, since this is the first place third-party CLI stdout becomes a spawn argument.
  • Design tokens: ModelChipPicker uses --cth-* colour and font variables throughout, but its 168px scroll cap and 4px 8px input padding are literals copied from the chips it replaces rather than the space scale exported from tokens.ts. Consistent with the surrounding code, still a deviation from the DESIGN.md rule. Happy to convert if you'd prefer the scale used.

No new runtime dependencies. No reformatting — the diff is only lines this change needed. Debug logging added during development was removed before the commit.

The model pickers were fed entirely by curated lists — the catalog baked
into the build, overlaid by docs/model-catalog.json. Neither knows what
this machine can reach, which depends on the user's account, plan and
keys: `opencode models` reports ~380 slugs against the user's own
providers, and `agy models` reports whatever the logged-in Antigravity
account is entitled to. A model you hold a key for was simply absent
until someone hand-added it upstream.

So ask the CLIs, and append their answer to the curated list. The
curated list stays the floor: it carries the readable labels, the
editorial ordering and any version bounds, and it wins every collision.
A CLI that is missing, logged out or slow contributes nothing, which
degrades to exactly the behaviour that shipped before.

Three things this had to get right, each of which is a test:

- agy's `--model` takes the DISPLAY NAME, not the slug — which is why
  every curated antigravity entry carries an id like `Gemini 3.1 Pro
  (High)`. Reading the slug column yields a value agy rejects AND a
  dedupe that never matches its curated twin, so the picker fills with
  duplicates that cannot be selected.
- A packaged app does not inherit the login shell's PATH. Launched from
  the desktop, an .AppImage or .deb never found `agy` or `opencode` and
  the probe failed silently, so the user's real PATH is injected.
- `agy models` round-trips to Google and takes ~6s, so the 5s default
  timeout returned empty on a healthy machine. Raised to 15s, and the
  answer is cached for 10 minutes — only when non-empty, so one
  transient failure does not pin the pickers short for the whole window.

The picker itself could not take the result. It rendered one chip per
model into an unbounded flex-wrap row with no scroll cap, so 391 entries
pushed the rest of the form off the modal. The chip grid moves to a
shared ModelChipPicker that grows a filter box and a scroll cap past 24
options and is byte-identical below it, so every curated provider looks
untouched. The remaining pickers are native selects, which handle long
lists on their own; two were keyed on label, which is safe only until a
live list merges in, and are now keyed on id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Evidence received. Before and after are both attached. Thanks — this is what makes a PR reviewable in one pass.

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