Skip to content

Feed the launcher model picker from the server catalog - #909

Merged
realtonyyoung merged 4 commits into
mainfrom
claude-tyoung/launcher-model-catalog
Sep 11, 2026
Merged

realtonyyoung merged 4 commits into
mainfrom
claude-tyoung/launcher-model-catalog

Conversation

@realtonyyoung

Copy link
Copy Markdown
Collaborator

Closes #900 — AI-2718

What & why

The desktop launcher's model picker offered a curated list for only Claude and Codex; every other vendor showed just Default + Custom. It now fetches GET api/agents/model-options — the same server catalog the web UI's dropdown reads — and offers it per vendor, so the desktop reaches web-UI parity. No kcap-server change: the endpoint already exists, and the app already has an authenticated HTTP read path. There is no context-window selector because the web UI has none (context window is display-only).

Where to look

ServerVendorModelCatalog fetches once and pushes an IObservable; HomeViewModel.ModelChoicesFor(vendor) prefers the fetched list and falls back to HostedHarnessCatalog's curated one per vendor, so an empty or unreachable catalog never blocks a launch. The launcher flyout and the agent chip both read through it. Fetch is best-effort: signed-out / offline / non-success leaves the fallback in place.

Verification

dotnet build Capacitor.slnx → 0 warnings; CLI dotnet publish -c Release → no IL2026/IL3050. Tests: ServerVendorModelCatalogTests 2/2 (publish on success; empty snapshot on null/throw), HomeViewModelTests 62/62 (new: server catalog preferred, curated fallback, empty when neither has the vendor), HomeViewSmokeTests 13/13, AppStartupTests 24/24.

Fetches api/agents/model-options once (the web UI's source) and offers it per vendor, falling back to the curated list when a vendor is absent or the fetch fails, so a launch is never blocked. The Default and Custom rows are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Populate launcher model picker from server catalog

✨ Enhancement 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Fetches per-vendor model options from the authenticated server catalog at startup.
• Prefers server choices and labels while retaining curated per-vendor fallbacks.
• Tests successful publication, failed fetches, and vendor-level fallback behavior.
Diagram

sequenceDiagram
    participant App as App Startup
    participant Catalog as Model Catalog
    participant Http as Auth HTTP
    participant Api as Server API
    participant VM as Home ViewModel
    participant Fallback as Curated Catalog
    participant UI as Launcher UI
    App->>Catalog: Load once
    Catalog->>Http: Authenticated GET
    Http->>Api: Request model-options
    Api-->>Catalog: Return vendor options
    Catalog-->>VM: Publish snapshot
    UI->>VM: Request vendor models
    alt Server vendor has choices
        VM-->>UI: Return server choices
    else Missing or unavailable
        VM->>Fallback: Read curated choices
        Fallback-->>VM: Return curated choices
        VM-->>UI: Return fallback choices
    end
Loading
High-Level Assessment

The current approach is optimal because it reuses the web UI's established server endpoint and the desktop application's authenticated HTTP path while preserving launch availability through per-vendor curated fallback. Expanding only the hardcoded catalog would continue to drift from the server, while fetching on every flyout opening would add latency and unnecessary requests.

Files changed (10) +192 / -10

Enhancement (8) +131 / -10
App.axaml.csWire the server model catalog into application startup +14/-3

Wire the server model catalog into application startup

• Creates the catalog service during daemon graph construction, starts its best-effort one-time load, and passes its observable into the main window. The service is retained and disposed with other server clients.

src/Capacitor.App/App.axaml.cs

ServerVendorModelCatalog.csAdd the authenticated server model catalog service +64/-0

Add the authenticated server model catalog service

• Introduces a disposable observable catalog that fetches model options once and publishes successful snapshots. It maps server DTOs into launcher model choices while treating missing authentication, HTTP failures, and exceptions as non-blocking misses.

src/Capacitor.App/Services/ServerVendorModelCatalog.cs

HomeViewModel.csResolve model choices through server-first fallback logic +24/-1

Resolve model choices through server-first fallback logic

• Subscribes to server catalog snapshots on the UI scheduler and exposes them for rendering. Model choices are selected per vendor from the server when non-empty, otherwise from the curated catalog.

src/Capacitor.App/ViewModels/HomeViewModel.cs

LauncherPaneView.axamlBind server catalog changes into the agent chip +1/-0

Bind server catalog changes into the agent chip

• Adds the model catalog to the agent chip's multi-binding so labels refresh when the asynchronous server snapshot arrives.

src/Capacitor.App/Views/LauncherPaneView.axaml

LauncherPaneView.axaml.csUse server-backed choices and labels in the launcher UI +16/-6

Use server-backed choices and labels in the launcher UI

• Builds model picker rows and custom-model checks from the view model's server-first choices. The agent chip now resolves friendly labels from the server catalog before using curated labels or the raw model identifier.

src/Capacitor.App/Views/LauncherPaneView.axaml.cs

RemoteModelsJsonContext.csRegister model option responses for source-generated JSON +1/-0

Register model option responses for source-generated JSON

• Adds the per-vendor model options dictionary to the shared JSON serialization context, preserving trimming and native-AOT-compatible deserialization.

src/Capacitor.Remote.Models/RemoteModelsJsonContext.cs

RemoteWire.csDefine the model options API route +1/-0

Define the model options API route

• Adds the shared route constant for the existing server model-options endpoint.

src/Capacitor.Remote.Models/RemoteWire.cs

VendorModelOptionDto.csDefine the server model option wire DTO +10/-0

Define the server model option wire DTO

• Introduces the value-and-label response record used to deserialize each server-provided model option.

src/Capacitor.Remote.Models/VendorModelOptionDto.cs

Tests (2) +61 / -0
HomeViewModelTests.csTest server-first model selection and curated fallback +23/-0

Test server-first model selection and curated fallback

• Verifies that server choices override curated choices per vendor, curated entries remain available for absent vendors, and unsupported vendors remain empty.

test/Capacitor.App.Tests.Unit/HomeViewModelTests.cs

ServerVendorModelCatalogTests.csTest model catalog publication and failure handling +38/-0

Test model catalog publication and failure handling

• Covers successful snapshot publication and confirms that null or throwing fetches preserve the initial empty snapshot without propagating errors.

test/Capacitor.App.Tests.Unit/ServerVendorModelCatalogTests.cs

@qodo-code-review

qodo-code-review Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (1) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Catalog failures leave no diagnostics ✓ Resolved 🐞 Bug ◔ Observability
Description
HttpFetch and LoadAsync both catch every exception without logging or otherwise exposing the
failure. Because production also discards the one-shot task, authentication, transport,
deserialization, and mapping defects all silently appear as an ordinary curated fallback with no
information for diagnosis.
Code

src/Capacitor.App/Services/ServerVendorModelCatalog.cs[56]

+        } catch (Exception) { return null; }
Relevance

●●● Strong

Recent reviews accept surfacing swallowed startup and background-task failures instead of relying on
invisible stderr.

PR-#446

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both newly added catch blocks discard every exception, and the production caller neither awaits nor
observes the load task. Mapping can also throw—for example, the case-insensitive ToDictionary
rejects case-variant duplicate vendor keys—so failures beyond routine offline behavior are hidden by
these handlers.

src/Capacitor.App/Services/ServerVendorModelCatalog.cs[31-35]
src/Capacitor.App/Services/ServerVendorModelCatalog.cs[42-56]
src/Capacitor.App/Services/ServerVendorModelCatalog.cs[59-63]
src/Capacitor.App/App.axaml.cs[537-539]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
All catalog exceptions are silently converted into a missing snapshot, leaving no way to distinguish expected signed-out or non-success responses from defects in transport, deserialization, or mapping.

## Fix Focus Areas
- src/Capacitor.App/Services/ServerVendorModelCatalog.cs[31-56]
- src/Capacitor.App/App.axaml.cs[537-539]

## Recommended Fix
Keep expected signed-out and non-success results as best-effort misses, but record unexpected exceptions with their full exception details through the application's diagnostic channel. Handle cancellation separately so normal shutdown is not reported as an error, and avoid catching the same failure silently at two layers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Newly signed-in users miss models ✓ Resolved 🔗 Cross-repo conflict ≡ Correctness
Description
BuildDaemonGraph invokes LoadAsync only at startup and discards its task, while the
successful-sign-in subscription restarts the server lane without reloading the model catalog. When
the app starts signed out, HttpFetch returns null because the endpoint requires authorization,
leaving the empty snapshot in place after authentication and causing the launcher to keep showing
curated fallback choices for the process lifetime.
Code

src/Capacitor.App/App.axaml.cs[R537-539]

+        var modelCatalog = new ServerVendorModelCatalog(ServerVendorModelCatalog.HttpFetch(sessionHttp, profiles));
+        _modelCatalog = modelCatalog;
+        _ = modelCatalog.LoadAsync(_shutdown.Token);
Relevance

●●● Strong

Recent auth-lifecycle reviews accept refreshing stale state after sign-in or reauthentication.

PR-#740

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The citations show that graph construction performs the sole catalog load and that LoadAsync
retains an empty snapshot when HttpFetch returns null for a signed-out authentication state. They
also show that the existing successful-sign-in path notifies the home and server clients and
restarts the server lane, but never reloads the catalog, even though the model-options endpoint is
mounted under an API group that requires authorization.

src/Capacitor.App/App.axaml.cs[535-539]
src/Capacitor.App/App.axaml.cs[581-583]
src/Capacitor.App/Services/ServerVendorModelCatalog.cs[43-54]
src/Capacitor.App/Services/ServerVendorModelCatalog.cs[31-35]
src/Capacitor.App/Services/ServerVendorModelCatalog.cs[46-56]
src/Capacitor.App/App.axaml.cs[579-583]
src/Capacitor.App/App.axaml.cs[763-770]
External repo: kurrent-io/kcap-server, src/Capacitor.Server/Sessions/RouteGroups.cs [266-273]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The model-options endpoint requires authorization, but the catalog is fetched only during startup. If the app starts signed out, the initial fetch leaves the snapshot empty, and completing in-app sign-in restarts the server lane without retrying the catalog request.

## Fix Focus Areas
- src/Capacitor.App/App.axaml.cs[535-539]
- src/Capacitor.App/App.axaml.cs[581-583]
- src/Capacitor.App/Services/ServerVendorModelCatalog.cs[31-35]

## Recommended Fix
Keep the initial startup load for users who are already authenticated, and subscribe a catalog reload to the existing successful sign-in signal alongside the server-lane restart. Invoke `modelCatalog.LoadAsync` with the application shutdown token, and safely serialize or coalesce overlapping initial and post-sign-in loads.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. New model test skips temp injection 📘 Rule violation ▣ Testability
Description
Model_choices_prefer_the_server_catalog_and_fall_back_to_the_curated_list obtains app-state.json
through a locally disposed TempDir.WithPathTo(...) instead of a [TempDir] property on
HomeViewModelTests. Each run therefore manages this test's temporary state inside the method
rather than through the framework-injected fixture used for test classes.
Code

test/Capacitor.App.Tests.Unit/HomeViewModelTests.cs[55]

+            using var tmp = TempDir.WithPathTo("app-state.json", out var path);
Relevance

● Weak

Recent test reviews rejected the same framework-injected TempDir requirement for manually managed
temporary directories.

PR-#878
PR-#869

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 2808173 requires test classes needing temporary state to use a public required
[TempDir] property rather than manually managed temporary directories. The added test creates and
disposes its own TempDir at line 55.

Rule 2808173: Use injected [TempDir] public required property in test classes instead of manual fields
test/Capacitor.App.Tests.Unit/HomeViewModelTests.cs[55-55]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new model-catalog test manually creates its temporary directory, bypassing the test framework's required `[TempDir]` injection pattern.

## Fix Focus Areas
- test/Capacitor.App.Tests.Unit/HomeViewModelTests.cs[17-17]
- test/Capacitor.App.Tests.Unit/HomeViewModelTests.cs[55-55]

## Recommended Fix
Add a public required `TempDir` property annotated with `[TempDir]` to `HomeViewModelTests`, then derive the new test's `app-state.json` path from that injected fixture instead of calling `TempDir.WithPathTo(...)`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 64 rules
✅ Cross-repo context — repo relationships
  Explored: repo: kurrent-io/kcap-server (sha: f582e72a)
Review mode: ⚖️ Balanced: This is a cross-cutting behavioral change spanning authenticated server fetching, reactive state propagation, UI selection/display, serialization, startup lifecycle, and fallback behavior, so it warrants a careful single-pass review.

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/Capacitor.App/Services/ServerVendorModelCatalog.cs Outdated
Comment thread src/Capacitor.App/App.axaml.cs
@linear-code

linear-code Bot commented Sep 11, 2026

Copy link
Copy Markdown

AI-2718

realtonyyoung and others added 3 commits September 11, 2026 15:27
#900)

The endpoint needs auth, so a signed-out start left the catalog empty; it now also loads on the sign-in-completed signal. Expected misses (offline/non-success) stay quiet; an unexpected fault is logged instead of hidden behind the curated fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A generation counter drops a slow earlier fetch whose result lands after a newer load, so overlapping startup and per-sign-in reloads can't leave a stale snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
One lock spans the entry increment and the post-fetch check+OnNext, so a newer load cannot publish between an older load's check and its own publish.

Co-Authored-By: Claude Opus 4.8 <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.

Desktop launcher: per-vendor model selection from the server catalog

1 participant