Skip to content

feat(models): user-managed model shortlist with /provider catalog - #107

Merged
grapeot merged 6 commits into
masterfrom
feat/model-shortlist
Sep 4, 2026
Merged

feat(models): user-managed model shortlist with /provider catalog#107
grapeot merged 6 commits into
masterfrom
feat/model-shortlist

Conversation

@grapeot

@grapeot grapeot commented Sep 4, 2026

Copy link
Copy Markdown
Owner

What

Replaces the hardcoded 9-model list with a user-managed model shortlist that mirrors the iOS mechanism:

  • The chat model picker shows only the shortlist.
  • A new Settings → 模型列表 (Model Shortlist) screen adds / removes / reorders / renames models.
  • The candidate catalog is built dynamically from GET /provider (connected providers, chat-capable models), with a fallback to config/providers (no connected filter) on older servers that lack the route.

Key changes

  • Data: ModelShortlistItem (persisted unit), ProviderRegistryResponse + ProviderModel.capabilities (chat-capable filter).
  • API: OpenCodeApi.getProviderRegistry() (@GET("provider")) + repository method.
  • Persistence: model selection now stored by model ID (providerId/modelId) instead of list index — required because the shortlist is user-mutable. One-time idempotent migration (schema version 2) + first-launch seeding of the existing 9 presets, so existing users are unaffected.
  • Auto-add: session-switch restore and message-history inference add a model to the shortlist when it's missing, keeping the picker in sync with what a session actually runs.
  • UI: ModelShortlistScreen (up/down reorder, edit short-name dialog, delete, catalog picker with search + multi-select), Settings entry row with count badge, chat-picker "Manage models" deep-link row.
  • Docs: RFC §4.5 / §5.14, PRD feature + decisions, working log.

Decisions (confirmed)

  • D1 Seed the 9 presets on first launch (existing-Android migration, not iOS's empty shortlist).
  • D2 Persist selection by model ID, not index (index breaks once the shortlist is mutable).
  • D3 Reorder via up/down buttons, no drag (iOS drag bug is a separate low-priority follow-up).
  • D4 /provider endpoint + config/providers fallback (no connected filter).
  • D5 No ongoing canonical-ID aging map; retired models simply drop out of the shortlist.
  • D6 displayName follows the catalog on refresh; user-edited shortName is preserved.

Verification

  • compileDebugKotlin, compileDebugUnitTestKotlin pass.
  • testDebugUnitTest: 348 tests, 0 failures (new ModelShortlistTest 16 pure-function tests; updated AppStateTest / MainViewModelTest / ForkSessionTest / NfcQuickPromptTest stubs + model-selection tests to the ID-based API).
  • lintDebug: 0 new errors from this feature (16 MissingTranslation fixed with zh strings); the only 3 remaining errors are pre-existing on master in MainViewModelSupport.kt, not in this diff.

Not merged — left open for manual testing.

Replace the hardcoded 9-model list with a user-managed shortlist that
mirrors the iOS mechanism: the chat picker shows only the shortlist, a
new Settings screen adds/removes/reorders/renames models, and the
candidate catalog is built dynamically from GET /provider (falling back
to config/providers without a connected filter on older servers).

Model selection is now persisted by model ID (providerId/modelId)
instead of list index, with a one-time idempotent migration guarded by
schema version 2 and first-launch seeding of the existing 9 presets so
existing users are unaffected. Auto-add paths (session switch + message
history inference) keep the picker in sync with what a session actually
runs.

Docs: RFC §4.5/§5.14, PRD feature + decisions, working log.
A reasoning_gpt code review of the shortlist PR surfaced 2 P0 and 6 P1
bugs; all are fixed here.

- Migration no longer double-applies the legacy index remap: the stored
  index is already normalized by migrateRemovedGpt56SolProModelIndices()
  before the ID migration runs, so map it straight onto the seed.
- Seed the shortlist only when the key is genuinely absent; a present-but
  undecodable list is left on disk untouched instead of being clobbered
  by the defaults and sealed at schema 2.
- launchLoadMessages now auto-adds the session's saved model (not just the
  history-inferred one) when it's missing from the shortlist, so the
  selected ID and index can't split and send a different model.
- removeModelShortlistItem persists the fallback selection to both the
  global and per-session stores; the selectedModelId setter now clears the
  key on null (putString(null) was a no-op) and a removeModelIdForSession
  helper is added.
- resolveModelCatalog returns null when both /provider and config/providers
  fail, so a previously loaded catalog is preserved instead of wiped.
- The migration drops malformed/out-of-range legacy per-session values
  (mapNotNull) instead of writing them as permanent invalid IDs.
- Catalog picker confirm uses the full catalog (cross-search selections no
  longer lost), shows the count, and is disabled when empty.
- Tablet "Manage models" now also expands a collapsed Sessions pane so the
  deep-link is actually consumed.
- Shortlist rows show the stable provider/modelId; buildCatalog falls back
  to the model id for blank names.

Tests: 3 new regression tests (remove-current-selection persistence,
missing saved-ID auto-add, blank model name). 351 unit tests pass; lint
adds no new errors.
The user-editable short name (简称) was only rendered in the toolbar
capsule and the edit dialog. After editing it, the change was invisible
in the two lists users actually look at: the Settings shortlist row and
the chat model dropdown both showed only the full display name.

Show the short name in both places so an edit is visible where it's made
and where models are picked:
- Settings shortlist row now shows display name / short name /
  provider-model id (three lines).
- Chat model dropdown shows the short name as a subtitle under each
  model's full name.

Display-only change; no state or persistence changes. 351 unit tests
pass.
User walkthrough feedback on the previous fix:

- The chat model dropdown didn't need changes - restore the original
  single-line display name per item.
- The Settings shortlist row was cluttered: three text lines crammed
  left of four inline icon buttons (~190dp of 48dp touch targets).

Redesign the row around the HostProfileRow pattern the RFC already
specified: the whole card is tappable and opens the short-name editor;
the display name and provider/model id subtitle get the full width with
ellipsis; the short name renders as a compact primary-tinted badge next
to the name (echoing the chat capsule label, capped at 120dp); all
management actions (edit / move up / move down / delete) collapse into
one 32dp MoreVert overflow menu with leading icons, move actions
disabled at the list edges, and delete tinted as destructive.
The chat model dropdown now displays two lines per entry:
- displayName (bodyLarge) on top
- modelId (bodySmall, muted) below

This gives users a stable identifier to cross-reference with
server logs and API calls, without cluttering the capsule badge.
P1 fixes:
- addModelsToShortlist now maintains the selectedModelId invariant:
  when the shortlist transitions from empty to non-empty, anchor to
  the first item and persist globally + per-session.
- resolveModelCatalog falls back to ModelPresets when both /provider
  and config/providers fail, so the Add Model catalog is never empty.
- launchLoadMessages only auto-adds a saved/inferred model when its
  provider is present in the loaded providers list, preventing stale
  or retired models from being resurrected.
- AddModelCatalogDialog uses LazyColumn with stable keys instead of
  eager Column.verticalScroll for large catalogs.
- ModelShortlist sub-screen uses rememberSaveable + BackHandler so
  system Back returns to Settings root instead of popping Settings.

P2 fixes:
- MoreVert IconButton uses default 48dp touch target (was 32dp).
- model_shortlist_count uses <plurals> for correct English singular.

Tests: 352 passing (new unknown-provider regression test).
@grapeot
grapeot merged commit bd689ea into master Sep 4, 2026
@grapeot
grapeot deleted the feat/model-shortlist branch September 4, 2026 07:20
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