feat(cowork): shown-but-disabled locked models for free tier (ENG-531)#327
feat(cowork): shown-but-disabled locked models for free tier (ENG-531)#327Dav1dF wants to merge 10 commits into
Conversation
Free (Starter) users see frontier models (Opus, Gemini, OpenAI) in the model picker but shown disabled, with an "Upgrade to Pro" CTA; MindsHub Air (the free model) is selectable and surfaced at the top. Applied to both the composer model picker and the Settings > Agent model dropdown. - modelEntitlement: isFrontierModel / isModelLocked / orderUnlockedFirst, with server `locked`-flag priority and an interim client heuristic until the StatSig gate exposes the flag (backend owned by Lucas). - Shared DEV-only tier simulation (useDevTier) to review pre-flag; dormant in prod. - Composer: inert locked rows, single "Upgrade to Pro" header CTA, pinned header with an internally-scrolling model list. - Settings dropdown: disabled locked options + single upgrade link; select border matched to text inputs. - Theme-aware --link-strong token so links/checks keep contrast in light and dark. - Rename latest:kimi -> "MindsHub Air"; recommendedModelOptions forward-compat for the server locked flag. - Surface the model picker on Home (remove hideModel). - First unit tests in the repo (vitest): entitlement + label/options, 16 passing. Co-Authored-By: Claude <noreply@anthropic.com>
Follow-up to the first-pass code review of PR #327. Correctness: - Free tier no longer runs a locked model by default: a guard falls the active selection back to the first unlocked model (MindsHub Air) when the selection is locked, covering the composer pill and every send path; the prepended selection also carries a computed `locked` flag. (finding 1) - Settings routes its model list through recommendedModelOptions, so it handles both id-strings and the future {id, locked} object shape instead of breaking on it. (finding 2) - Settings shows MindsHub Air as the effective selection when the saved role model is locked, rather than a disabled frontier model. (finding 3) - Home model picker: documented as an intentional un-hide; fixed the stale hideModel comment. (finding 4) Cleanups: - Settings reuses orderUnlockedFirst and precomputes each model's lock once (removes the divergent inline sort and repeated isModelLocked calls). (5, 7) - Shared UpgradeToProLink component replaces the duplicated upsell CTA. (6) - effectiveTier() centralizes the "only minds-cloud is gated" rule. (8) - Dropped dead `desc: ''` model field. (9) Tests: +3 for effectiveTier (19 passing). Co-Authored-By: Claude <noreply@anthropic.com>
- Normalize removeProvider's model fallback through recommendedModelOptions so
it survives the future {id, locked} backend shape (same class as the earlier
SettingsView fix).
- Finish removing the dead `desc` field: dropped from the remaining
setSelectedModel constructions and the Composer render branch.
- Simplify savedLocked (removed the unreachable ternary else branch).
- Memoize modelOptions (was recomputing isModelLocked + reallocating on every
render during live streams) and order the prepended selection so a transient
locked selection can't render above the unlocked models.
Selection-guard effect was re-reviewed for loop/regression risk and cleared.
Co-Authored-By: Claude <noreply@anthropic.com>
Open decision for the second pass (Lucas / Paul)Self-review is done (two passes; fixes pushed in Settings shows MindsHub Air, but the saved override still points at the locked model. In Whether the client should also persist the fallback depends on how the M1 entitlement service treats a persisted but locked model:
Migrating on the client means calling Everything else from both review passes is resolved. The composer runtime selection is already guarded (a locked active selection falls back to MindsHub Air), so a task sent from the composer always uses an allowed model regardless of this Settings decision. |
Drop the 12px marginBottom on the Settings upgrade link — it only existed in the section with a locked model (Planning), pushing its Reasoning effort control lower than Coding's. Both now share the same 6px rhythm. Co-Authored-By: Claude <noreply@anthropic.com>
Move the 12px above Reasoning effort onto the effort control itself so both Planning and Coding get it, instead of on the (Planning-only) upgrade link. Preserves the separation and aligns the two sections. Co-Authored-By: Claude <noreply@anthropic.com>
Add a Planning | Coding segmented toggle to the home composer's model menu so both agent roles are selectable from the composer, not only in Settings. Each tab drives its own role and writes through to the global agent config (planningModel / codingModel) via saveSettings, keeping Settings > Agent in sync both ways. Planning continues to drive the per-message model. Gated behind a new `roles` prop so ChatView and the Projects quick-launch picker keep the single-model API. Per-tab ENG-531 lock treatment: each role's list dims locked frontier rows with one "Upgrade to Pro" CTA, unlocked models first. Also stop hardcoding the free model's "MindsHub Air" label in the client — recommendedModelOptions now prefers a config-provided label, so the display name lives in the backend model config (source of truth) and falls back to the id-derived label. Per-task role overrides (vs global settings) tracked as ENG-554. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Pull the Planning/Coding model-list derivation out of App.jsx into a pure lib/roleModels.js (settingKeyForRole, buildRoleModels, withSelectionFirst), removing the planning/coding duplication and making the logic testable without a React harness. App wires them into the same memos. Adds roleModels.test.js (10 tests): role→settings-key mapping, tier-based locking + unlocked-first ordering, config-label as display name, and the selection-prepend/no-op/reorder cases. Suite now 30 passing. Co-Authored-By: Claude <noreply@anthropic.com>
|
Hi @minura, quick orientation since this PR grew past the original ENG-531 scope. What it does now
Where to look
One thing to weigh in on |
Reconcile ENG-531 with #326 (per-model availability) + #328 (Settings polish), both of which landed on staging and overlap the model picker. - Adopt the server per-model availability signal (settings.modelEnabled) as the source of truth for locking, on BOTH surfaces. New helper isRoleModelLocked(model, tier, modelEnabled): server opinion wins both ways (false=locked, true=unlocked); the client tier heuristic is the fallback only when the server has no entry for that id (dev sim / direct providers). Wired through buildRoleModels / withSelectionFirst and the App selection guard. - Keep our lock treatment + framing (per product decision): dimmed disabled rows + one "Upgrade to Pro" link, no per-option suffix, no "No credits" wording, on both the composer picker and Settings. - Take staging's Settings polish (#328): provider key-input/status-pill layout, fieldLabel styling (applied to the effort label too, keeping our 12px alignment fix). Keep staging's separate no-credits notice (402/429 provider exhaustion, distinct from tier locking). Conflict was confined to SettingsView.jsx; App.jsx + globals.css auto-merged. Renderer build clean; 34 unit tests pass.
…iders Two small cleanups to the Settings polish taken from #328: - Drop the dotted underline on the agent-model field labels (Provider / Model / Reasoning effort). A dotted underline reads as a "hover for a tooltip" affordance, and none is wired; keep the plain bold label. - Remove the vertical divider (borderLeft) between the label and control columns in every Section; the horizontal row separators are enough. Co-Authored-By: Claude <noreply@anthropic.com>
|
@torrmal heads-up: this PR (ENG-531) merges your Settings polish from #328 and adjusts two small visual details in it. Wanted to explain the reasoning and get your take. 1. Vertical dividers (the left border between the label column and the controls). Removed them. Each row already separates the label from its control through the two-column layout and spacing, and the rows are divided by the horizontal lines. The vertical rule repeats that same separation, so running down every row it reads as extra visual noise and competes with the horizontal rhythm. Dropping it keeps the structure just as clear with less clutter. 2. Dotted underline on the field labels (Provider, Model, Reasoning effort). Removed the underline, kept the bold label. A dotted underline is a common convention for "hover me for a definition," but nothing is wired to it here, so it signals an interaction that does not exist. Plain bold labels keep the hierarchy without the false cue. Both are one-line changes and easy to revert. If you feel strongly about either, happy to put it back or find a middle ground. |
|
@lucas-koontz pulling you in as a reviewer since this PR now builds directly on your #326 1. Open decision (your call, depends on the entitlement service). In Settings, when the saved model is locked for the tier, we show the first unlocked model (MindsHub Air) as the effective selection but do not rewrite the persisted 2. One semantic to confirm. #326 treats |
MinuraPunchihewa
left a comment
There was a problem hiding this comment.
@Dav1dF Overall, this looks fine to me.
I also created a PR on top of this to add the display labels: #343
I do have one question though; from what I can see, in the model pickers you have added the models listed are those that are supported by the provider of the planning and coding models that the user has currently chosen. In situations where a user has configured multiple models, is this still how we want it to be? Or do we want the models from all configured providers to be displayed?
What
Implements the shown-but-disabled UX for free (Starter) users attempting a blocked frontier model, decided in ENG-525 and tracked in ENG-531.
Free users see frontier models (Opus, Gemini, OpenAI) in the model picker but shown disabled, with an "Upgrade to Pro" CTA. The free model — MindsHub Air (Kimi K2) — is selectable and surfaced at the top so the user doesn't scroll past locked models. Applied to both the composer model picker and the Settings › Agent model dropdown.
Before / After
Left: today (all models selectable, no gating). Middle/right: free-tier treatment in light and dark — MindsHub Air on top, frontier models disabled with a single "Upgrade to Pro" CTA, contrast preserved in both themes.
How it works
lib/modelEntitlement.js—isFrontierModel/isModelLocked/orderUnlockedFirst. A server-providedlockedflag always wins; until the StatSig tier gate exposes it (backend owned by Lucas / Max A.), an interim client heuristic locks frontier families on the free tier.lib/useDevTier.js— a DEV-only tier simulation so the treatment is reviewable before the backend flag lands (__setModelTier('free'|'pro'|null)in DevTools). Dormant in production — nothing locks until a real tier/flag exists.<select>border matched to the text inputs.--link-strongtoken so links and selection checks keep contrast in light (dark teal) and dark (lighter green).latest:kimi→ "MindsHub Air";recommendedModelOptionsforward-compat to pass a serverlockedflag through.hideModel).Backend dependency
The full gate depends on the tier-gated model list including locked frontier models with a per-model
lockedflag (not stripped). Lucas confirmed he'll make this change in the StatSig config; this client reads that flag as soon as it lands. Until then the treatment only activates via the DEV tier simulation.Tests
First unit tests in the repo (vitest, node env, no jsdom): entitlement logic + label/options, 16 passing (
npm test).Notes
docs/eng-531/before-after.pngis a representative render for this PR; drop it before merge if the team prefers attachments over a committed asset.