feat(extension): model picker parity with the app — search, favorites, sections - #4810
Conversation
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryIncremental diff adds a single test-only wait (boot hydration gate) in Files Reviewed (1 file)
Previous Review Summaries (2 snapshots, latest commit 9e63002)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 9e63002)Status: No Issues Found | Recommendation: Merge Executive SummaryAll four previously flagged findings are resolved: the Files Reviewed (3 files)
Previous review (commit 36f3895)Status: 4 Issues Found | Recommendation: Address before merge Executive SummaryFavorite add/remove mutations silently drop Overview
Issue Details (click to expand)CRITICAL
WARNING
Files Reviewed (20 files)
Reviewed by claude-sonnet-5 · Input: 22 · Output: 2.4K · Cached: 415.3K Review guidance: REVIEW.md from base branch |
What changed
The extension's footer model control is upgraded from a native
<select>to a full picker overlay with parity to the mobile app picker:FAVORITES(when any favorite matches the filter),RECOMMENDED(gatewayisPreferred, preferred order),ALL MODELS(remainder, gateway order). Each model appears in exactly one section (A3). Plain headers, no provider grouping — same as the app'sgroupForModelbehaviour for gateway catalogs.aria-pressed+ explicit action label) that adds/removes the model from the user's server-side favorites viamodelPreferences.get/addFavorite/removeFavorite. These are the same per-user favorites the web and mobile apps read and write — same tRPC router, sameuser_model_preferencesrows, keyed by gateway model id — so a star in the extension shows up in the app and vice versa (A5/A6). Favorites are per user, not per conversation and not per organization; switching organization refetches (the router filters by the org's allowed models).Freechip (isFree && !byok),BYOKchip, and theData collectedicon (BookOpenCheck, accessible name) whenmayTrainOnYourPrompts(A4).aria-current="true", and the overlay opens with the selected row scrolled into view (A8, verified by E2E).Retry, models stay selectable), optimistic toggle rollback ("Couldn't update favorites."), and a terminal per-token+organization state for 401/403 / tRPCUNAUTHORIZED/FORBIDDEN("Favorites aren't available here.", no CTA, stars hidden) that clears on any successfulgetand never latches session-wide.Model, visible textLoading models...while empty/loading, same disabled flags, and the existingRetry modelsfooter error row is unchanged (A1b/A9). Existing selectors keep resolving; Chrome and Firefox behave identically with no new manifest permission (A10).Simpler-shape decisions (S1–S4)
/api/trpc/modelPreferences.*— the endpoint has no transformer, so a query isGET …/modelPreferences.get?input=<url-encoded JSON>and a mutation isPOSTwith the raw input JSON body. Two small functions + zod parsing instead of pulling@trpc/clientand the server router's types into the MV3 bundle. Org scoping goes in the procedure input (input.organizationId), never in thex-kilocode-organizationidheader (whichgetUserFromAuthvalidates as a UUID and would 400 into the terminal state for non-UUID E2E orgs).fixed inset-0,role="dialog", labelled close button) — no popover layer, no new dependency, no new dialog semantics (close button only; focus moves to search on open).Test infrastructure
tests/e2e/kilo-api-fixture.tsnow mocks/api/trpc/modelPreferences.*with path-prefix matching, real tRPC success envelopes, stateful favorites, and opt-in 401/403/500; sharedmodel-picker-e2e-helpers.ts(selectModelByIdwaits for the row inside the dialog;expectSelectedModelIdasserts the trigger'sdata-model-id) replaces everyselectOption/toHaveValueon the old control, mirrored in the Selenium harness.tests/e2e/model-picker.test.tscovers happy path, row chrome, search, no-match +Clear search, favorites toggle + section placement, retryable 500, terminal 403 (no CTA, no stars), toggle rollback, empty catalog, the org-scoped wire assertion (org selected →input.organizationId; personal → noinput), and scroll-into-view on a long catalog. New Firefox scenario covers open/search/select.tests/e2e/local-backend-live.test.ts: live device-auth sign-in now resolves the stack's canonical origin by probing/users/after-sign-in(no-redirect) before navigating — the shared stack may run withAPP_URL_OVERRIDEat a LAN IP, and cookies set off-origin made the post-login redirect silently drop authentication. The live favorites round-trip is personal-only, ensure-favorite (reads prior state, never blind-toggles), and restores prior state infinallybecausefl@fl.flis a shared account.tests/e2e/design-tokens.test.ts: the seeded-transcript wait gets an explicit 30s timeout; under full-suite load on a busy machine the first reload can exceed the 5s Playwright default (observed flaking locally in-suite while passing in isolation; style assertions unchanged).Verification
pnpm format;pnpm --filter kilo-extension verify(448 vitest) ✓pnpm --filter kilo-extension build,build:firefox✓pnpm --filter kilo-extension e2e:chrome: 91 passed, 11 skipped, 0 failed ✓pnpm --filter kilo-extension e2e:firefox: 34/34 workflows passed ✓pnpm --filter kilo-extension e2e:localagainst the shared local stack: 11/11 passed, including the personal favorites round-trip (star → persisted across reload →FAVORITESsection) against the realmodelPreferencesrouter ✓Notes
/api/trpc(getUserFromAuthhonorsAuthorization: Bearer), so no backend change was needed.agent-chat-panel.tsx. Whichever PR merges second integratesmainand re-runs the extension gate.