feat: shared model preferences across web and mobile#4388
Open
iscekic wants to merge 5 commits into
Open
Conversation
Introduce a user-scoped user_model_preferences table that persists model favorites and last-selected (model + optional variant) per signed-in user. A new tRPC model-preferences router exposes get/setLastSelected/ clearLastSelected/addFavorite/removeFavorite/setFavorites; get intersects results with the active org's available-model policy so disallowed models are silently dropped. Web: NewSessionPanel reads the server's lastSelected to seed the initial model and writes back on every model or variant change. The existing localStorage cache remains as a fallback. Mobile: new useModelPreferences + usePersistedAgentModel hooks layer the server value over a SecureStore cache. The agent-chat model picker shows a Favorites group with a 44dp star toggle per row, and new.tsx hydrates from server lastSelected then falls back to the local cache. Extension and CLI live in a separate repo and are tracked as a follow-up; the tRPC router is the stable contract they will consume. Decision record: .plans/shared-model-preferences-decisions.md.
Contributor
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryAll 4 previously reported issues (missing org-membership check in Files Reviewed (8 files)
Previous Review Summary (commit 618d2c6)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 618d2c6)Status: 4 Issues Found | Recommendation: Address before merge Executive Summary
Overview
Issue Details (click to expand)CRITICAL
WARNING
SUGGESTION
Files Reviewed (16 files)
Reviewed by claude-sonnet-5-20260630 · Input: 38 · Output: 9.2K · Cached: 1.1M Review guidance: REVIEW.md from base branch |
- Require org membership in modelPreferences.get before resolving an arbitrary organizationId (matches organizationMemberProcedure semantics) - Make addFavorite/removeFavorite atomic jsonb upserts so concurrent toggles from two devices no longer lose updates; cover with a test - Wait for the server preferences query in useAutoSelectModel so the shared lastSelected is not lost to a race against SecureStore on cold start; honor the server variant on web seeding too - Share one usePersistedAgentModel instance between new.tsx and the auto-select hook - Invalidate modelPreferences.get with a partial key so org-scoped and org-less caches stay in sync after mutations - Derive the query data type from the router output, drop the hand-copied LastSelected type and cast, trim the web hook to its used surface, dedupe the optimistic favorites logic, parallelize the get() reads
Mobile now mirrors web's getPreferredInitialModel precedence: server lastSelected -> local cache -> org default (/api/organizations/:id/defaults, same endpoint web uses) -> first available. Personal sessions are unchanged (no defaults fetch is made without an organizationId).
Composes #4383's pinned CLI model row with the favorites picker rows: the CLI row stays first (above FAVORITES), is excluded from the favorites and section groups, and does not render a favorite star.
Adopts #4387's per-context local model persistence (agent-model-preference store + usePersistedAgentModel) as the local cache layer, replacing this branch's simpler single-entry SecureStore hook and its storage key. useAutoSelectModel now resolves the local fallback via resolveModelForContext; the server lastSelected -> local -> org default -> first-available precedence is unchanged. Model changes in an existing session (session-detail-content) now also write through to the server preference, mirroring the new-session screen.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces a user-scoped
user_model_preferencestable that persists model favorites and last-selected (model + optional variant) per signed-in user. A new tRPCmodelPreferencesrouter exposesget,setLastSelected,clearLastSelected,addFavorite,removeFavorite, andsetFavorites. Thegetprocedure requires the caller to be a member of the given organization (viaensureOrganizationAccess, matchingorganizationMemberProceduresemantics) and intersects results with that org's available-model policy so disallowed models are silently dropped, matching the existing orgdefault_modelsemantics inapps/web/src/routers/organizations/organization-settings-router.ts.addFavorite/removeFavoriteare atomic jsonb upserts, so concurrent toggles from two devices don't lose updates.Architectural changes:
user_model_preferencestable (packages/db/src/schema.ts, generated migration0176_flowery_omega_flight.sql) withonDelete: 'cascade', onUpdate: 'cascade'tokilocode_users.idso user deletion cleans up the row.apps/web/src/routers/model-preferences-router.tsregistered inroot-router.ts.NewSessionPanel.tsxprefers the server'slastSelected(model and variant) for the initial model seed, falls back to localStorage, then org default, then first available. Picks and variant changes write through to the server in addition to localStorage.useModelPreferencesanduseAutoSelectModelhooks layer the server value over the local cache (feat(mobile): polish agent chat (persist model, attachments, copy, reasoning default) #4387's per-contextusePersistedAgentModelstore). The agent-chatmodel-pickershows a Favorites group with a 44dp star toggle per row (no star on the pinned CLI pseudo-model row from fix(remote): preserve CLI model for remote sessions #4383, which stays above FAVORITES).new.tsxhydrates from serverlastSelected→ local cache → org default (same/defaultsendpoint web uses) → first available, matching web'sgetPreferredInitialModelprecedence. Mid-session model changes also write through to the server preference.The deprecated
kilocode_users.default_modelcolumn and the existing web localStoragegetLastUsedModel/setLastUsedModelare intentionally left untouched (still referenced bysoftDeleteUserand as a local cache, respectively).Extension and CLI live in a separate repo (
~/Projects/kilocode) and are out of scope for this PR. The tRPC router is the stable contract they will consume. Tracked as a follow-up.Verification
Manual verification: not yet run on a live dev session. The work was verified by:
pnpm test:dbthenpnpm drizzle migrateapplied the migration cleanly;psql\d user_model_preferencesshowed the expected shape, indexes, and FK.pnpm --filter web test -- model-preferences-router— 14/14 router tests pass (including cascade-on-user-delete, non-memberUNAUTHORIZED, org intersection with a real org, and concurrent-add atomicity).pnpm --filter web test— full web test run, 8075 tests pass, 3 skipped.pnpm --filter kilo-app test— 380/380 mobile tests pass (includes the CLI-row + favorites interaction tests added while merging main).scripts/typecheck-all.sh --changes-only— clean.pnpm --filter web lint,pnpm --filter kilo-app lint,pnpm --filter @kilocode/db lint— all 0 errors.pnpm format— formatted the touched files; full repopnpm formatapplied to all 5932 files.Follow-ups: live in-browser smoke test of (a) favoriting a model on web and seeing it on mobile after a sign-in, (b) the inverse, and (c) verifying the org intersection on a non-enterprise org. Skipped here to keep the PR reviewable; recommend doing both in the extension/CLI follow-up PR.
Visual Changes
N/A
Reviewer Notes
apps/web/src/lib/hooks/use-model-preferences.tsexposes only what web consumes today (lastSelected+setLastSelected); the favorites surface (with optimistic updates) lives in the mobile hook and can be added to web when it grows a favorites UI.useAutoSelectModelwaits for the server preferences query (and the SecureStore cache) before locking in a selection, so the serverlastSelectedreliably wins on cold start; on error/offline the query settles and the local fallback applies. Mutations invalidatemodelPreferences.getwith a partial key so the org-scoped and org-less caches stay in sync.buildModelPickerRowssignature changed to requirefavoriteIds: Set<string>; the existing test was updated to match.usePersistedAgentModel(per-contextagent-model-preferencestore), adopted during the merge of main; this branch's earlier single-entry SecureStore hook was dropped in its favor. Model changes inside an existing session also write through to the server preference now.