Show the Copilot model lineup to users without a license - #2778
Conversation
The docs named Copilot Plus Flash as "a built-in model" and stopped there, while the relay serves eight: Flash, DeepSeek V4 Pro and V4 Flash 0731, GLM-5.2, Kimi K2.6 and K2.7 Code, MiMo V2.5, and MiniMax M2.7. A reader had no way to learn that the other seven come with what they already pay for. List them in two tables — the three on by default, and the five waiting to be switched on — with the relay's own one-line descriptions, then say where they appear: the Quick Chat picker, and inside each Agent Mode agent that can run them (OpenCode; Claude Code and Codex bring their own models). Also fixes the chat-model table in models-and-parameters.md, which listed one Copilot row labelled "Plus exclusive" — the label predates Lite sharing these models, and seven rows were missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Without a license the Copilot provider is unregistered, which cascade-removes its configured models and their enrollment — so the pickers have nothing Copilot to render and a new user has no way to learn from the product that a license would add Copilot Plus Flash, DeepSeek V4 Pro, GLM-5.2, and five more to the very picker they are looking at. The only Plus upsell in Agent Mode pitches a different feature, and the settings list's "Copilot license required" tooltip decorates a provider group that does not exist without a license. Synthesize the offer instead. `lockedCopilotEntries` builds render-time rows from the hardcoded lineup — never `Provider` or `ConfiguredModel` rows, so nothing can select, enroll, or default to one — and both pickers lead with them when `isPaidUser` is false: in Agent Mode at the top of each agent that can route them, in Quick Chat above the user's own models, including the brand-new-user case where there are no other models at all. It previews exactly the three a license switches on rather than all eight, so the preview matches the outcome and the user's own models keep their place above the fold of a 288px picker. Each row carries the relay's own one-line description as its subtitle, so it says what the model is for. A lock icon beside the name explains the state, with "Copilot license required" on instant hover; the right-side reason label is suppressed for these rows, since printing the same sentence down the whole group is noise. Nothing moves when a license activates — the lock simply falls off the rows already there. Eligibility is an explicit `routesCopilotModels` on the descriptor rather than a derived one: without a license there is no Copilot provider to inspect, so the question cannot be answered from the configured models. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Documents the behavior added in the previous commit, in the same section that says where the models appear once a license is active. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`!isPaidUser` and "no Copilot provider" agree in practice, but only the second
is the question these rows answer. A license state that had not resolved while
the provider was still registered would have put locked copies beside the very
models they advertise; asking about the provider cannot produce that.
Also gives the picker-helper test fixture the providers map real settings always
carry — the bare `{} as CopilotSettings` cast left it undefined.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b5284152a8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The table named seven models that no longer exist (openai/gpt-5.4, openai/gpt-5-mini, gpt-5.4, gpt-5-mini, claude-opus-4-6, claude-sonnet-4-5-20250929, grok-4-1-fast) and omitted eight that do, including claude-sonnet-4-6, claude-opus-4-7, claude-haiku-4-5, grok-4.3, and both SiliconFlow DeepSeek models. Refreshing it would buy accuracy until the next model bump; three landed in this branch's own base. Name the providers covered instead and point at the live list in Settings, which cannot go stale. This supersedes the Copilot rows added to that table earlier in this branch — the lineup is documented in copilot-plus-and-self-host.md, which this now links to. The embedding table is left as it stands; its drift is smaller and one entry needs a decision rather than a doc edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two halves of this doc described UI that v4 removed, which is worse than the stale model names — a reader following them looks for tabs that are not there. "Embedding Models" sent people to Settings → Copilot → QA → Embedding Model. There is no QA tab and nothing anywhere selects an embedding model; v4 gets semantic search through Miyo instead. "Model Parameters" documented temperature, max tokens, conversation turns, auto-compact, reasoning effort, verbosity, top P, and frequency penalty as living in Settings → Copilot → Model. There is no Model tab either, and no editor for any of those keys remains in the settings UI. What is left is accurate and still linked from three other docs: which models ship, the capability badges, adding and importing your own, and where the default model lives. So the file stays, retitled "Models" since the parameters half is gone, and the index entry and the two inbound links follow. The pointer from Vault Search that existed only to explain choosing an embedding model is dropped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3379af8293
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…t too The pickers advertised the lineup but the settings list did not, so a user without a license opened Settings → Basic → Agents → opencode and saw OpenRouter, OpenAI, Anthropic, Gemini, lmstudio — and no Copilot at all. That list groups by provider, and without a license there is no Copilot provider to group, so the group vanished rather than locking. Synthesize it when no Copilot provider is registered, tagged `copilot-plus` so the existing highlight, privacy badge, "Copilot license required" tooltip, and float-to-top pass apply to it unchanged — a locked group is the same group minus the ability to act on it. Rows carry a lock beside the label and a disabled toggle, and honour the search query like any other row. The settings list shows the full lineup where the pickers show three: a picker is 288px tall and would bury the user's own models, while browsing the whole catalog is exactly what the settings list is for. Also updates the welcome modal's feature list, which still advertised chat context, PDF/image support, and "exclusive chat and embedding models" — the models are no longer Plus-exclusive and nothing sets an embedding model. It now names Copilot exclusive models, cross-agent skills, multi-agent features, and symposium.md doc sharing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"You have unlocked the full power of Copilot" is true of no single tier, and multi-agent is gated on tier >= Plus (`canUseMultiAgent`), so a Lite subscriber was being told they have a feature the code will refuse them. The line now states what a license includes rather than what it unlocks, and drops multi-agent. A test asserts the modal mentions no capability a lower paid tier may lack, so the next copy edit cannot quietly over-promise again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quick Chat renders the flat `ModelSelector`, whose `DropdownMenuItem` carries `data-[disabled]:tw-pointer-events-none`. `pointer-events` inherits, so on a locked Copilot row neither the lock's tooltip nor the row's native `title` could be hovered — and since the row also suppresses the right-side reason, an unlicensed user got a greyed row with no way to learn a license is what is missing. Agent Mode was fine: `ModelEffortPicker` rows are plain divs. The reset lives on `LicenseRequiredIcon` rather than on the one row that exposed the bug, because the lock only ever sits on a disabled row — whatever renders it next will disable pointer events too. The same rows also lost their subtitles there: `lockedCopilotEntries` sets `_subtitle` on every row, but only `ModelEffortPicker` read it, so the Quick Chat rows were bare names where the issue asks for the same rows as Agent Mode. The flat row now renders the second line the same way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a3cb9d7651
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The lock's reason lived only in a hover tooltip. A disabled `DropdownMenuItem` stays in the accessibility tree but can take neither hover nor focus, and the right-side reason is suppressed on a locked row, so a screen reader announced "Copilot Plus Flash", its subtitle, and nothing about the license. `LicenseRequiredIcon` now carries the same sentence as `tw-sr-only` text, which folds it into the row's accessible name. `aria-label` would be one attribute instead of one span, but it is what makes Obsidian attach its own native tooltip and throw `isShown is not a function` here. Not making the trigger focusable: the row must stay unreachable by keyboard selection, so a tab stop inside it is the one thing that cannot be added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 99b22d66ae
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rows The settings model list decided to advertise the locked Copilot lineup by checking whether it had just built a Copilot group. That answers a different question than the pickers ask. `registerPlusProvider` writes the provider row, then the key, then reconciles the models — and `syncCopilotPlusProvider` swallows a failure rather than rethrowing it, so a licensed user can end up holding the provider with no configured models under it. In that state the list built no Copilot group, so it synthesized a locked one and told a paying user a license was required, over eight toggles they had already bought. `buildModelEnableGroups` now takes the answer from its caller, which reads the provider rows through the same `shouldPreviewCopilotModels` both pickers use. That predicate now takes the providers record rather than whole settings: the settings list already subscribes to `providersAtom`, and the narrower parameter also tightens the Quick Chat picker's memo, which was invalidating on every unrelated settings write. The `isOpencode` half of the gate stays inside the grouping function so it keeps its test; only the provider question, which the function cannot see, moves out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e5998f0ee8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| export function shouldPreviewCopilotModels(providers: CopilotSettings["providers"]): boolean { | ||
| return !Object.values(providers).some((provider) => provider.origin.kind === "copilot-plus"); |
There was a problem hiding this comment.
Check entitlement before advertising a missing license
When a valid Self-Host entitlement is active while isPaidUser remains false—a supported state explicitly handled by buildAgentSystemPrompt()—syncCopilotPlusProvider() unregisters the Plus provider, so this predicate returns true. Quick Chat, the OpenCode picker, and model settings then tell an already-licensed self-host user “Copilot license required.” Provider absence proves only that hosted model rows are unavailable, not that the user lacks a license; include entitlement/self-host status in this decision or use an availability reason that does not claim the license is missing.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Won't fix — the state is unreachable. Every writer of isPaidUser: applyEntitlement sets it from tier !== "free" (a tier granting self_host is not free), markPaidPendingEntitlement sets it true with an empty token, and clearEntitlement (the only false) clears the token too — and hasVerifiedFeature requires verified.token === settings.entitlementToken. So isSelfHostModeValid() cannot be true while isPaidUser is false; a self-host user is registered and toggleable today. Reopen if the server can issue a free-tier token carrying self_host.
Fixes logancyang/obsidian-copilot-preview#283
Why
Open the Agent Mode model picker without a license and you see the models OpenCode reports, plus anything you added with your own API key. Nothing else. Not a greyed row, not a badge — the Copilot models are simply absent, because
syncCopilotPlusProviderregisters the Copilot provider only for a paying user and unregisters it otherwise, which cascade-removes its configured models and their enrollment. A picker built from settings has nothing Copilot left to draw.So the eight models a license includes are invisible to exactly the people who have not bought one. The only Plus upsell in Agent Mode pitches a different feature ("Mention multiple agents with @"), and the
"Copilot license required"tooltip that already exists in the settings model list decorates a provider group that, without a license, does not exist.The docs did not cover for it either: they named Copilot Plus Flash as "a built-in model" and stopped there, while the relay serves eight. The built-in chat-model table in
models-and-parameters.mdwas worse — it named seven models that no longer exist and omitted eight that do.What
The pickers now advertise the lineup when it is absent.
Each locked row shows the model's name, the relay's own one-line description as a subtitle, and a lock icon whose hover reads "Copilot license required". The rows are non-selectable: clicking does nothing, and keyboard navigation skips them.
Three deliberate choices are worth the reviewer's attention:
It previews the three a license switches on, not all eight. The preview then matches the outcome — activate, and the locks come off the same rows in the same order rather than being replaced by a different set — and the user's own models keep their place above the fold of a 288px picker.
Rows are synthesized per render and never persisted. They are not
ProviderorConfiguredModelrows, so nothing can select, enroll, seed, or default to one; a licensed user never sees them because the real models are there instead.The gate is "no Copilot provider registered", not
!isPaidUser. The two agree in practice, but only the first is the question these rows answer, and a license state that had not resolved while the provider was still registered would otherwise put locked copies beside the models they advertise.The docs now list all eight models, name the three that are on by default, say where they appear in each picker, and describe the locked state.
models-and-parameters.mdalso loses the parts v4 removed. The built-in chat-model table is gone rather than refreshed — a hand-maintained list of 25 names cannot stay true, and three model bumps landed in this branch's own base — so the doc names the providers covered and points at the live list in Settings → Models (BYOK). Its "Embedding Models" and "Model Parameters" sections are gone too: they sent readers to a QA tab and a Model tab that no longer exist, for an embedding picker and parameter editors that no longer exist either. What remains is accurate, so the file stays, retitled Models, with its index entry and inbound links updated.Non goal
ModelSelector, which every picker in the app consumes.vault-search-and-indexing.mdstill discusses embedding models and their cost; auditing what semantic search means under Miyo is its own change.Screenshot
No image embedded — attaching one needs GitHub's browser editor rather than the API. The states are reviewable in the component gallery instead, which is where the change was designed:
npm run gallery:vault # then open the gallery view and pick UI/Model SelectorUI/Model Selectorcarries three stories:Licensed(your models, nothing locked),Unlicensed(the locked rows leading the section — open the picker, then hover a lock), andUnlicensedWithNoModelsOfTheirOwn(the brand-new-user case). Story fixtures are literal rather than built from the production helper, so the states stay put if the lineup changes.Risk
Low
lockedCopilotEntries.test.tscovers the row contents and the provider gate;agentModelPickerHelpers.test.tscovers placement, per-agent eligibility, the licensed case, and that locked rows do not suppress the loading placeholder;ModelSelector.test.tsxopens the menu and asserts the lock renders with no right-side labelBackendDescriptor.routesCopilotModelsis internal to Agent Mode and all three implementers are updated in this diff;_needsLicenseis an optional field on an internal picker typeReview: skim
src/lib/lockedCopilotEntries.tsfor what a row is and when it appears, then run Verification step 1. If you only read one hunk, read the splice inbuildPickerEntries— its position after the readiness and self-host passes is what keeps a locked row from being relabelled with the wrong reason.Verification
docs/copilot-plus-and-self-host.mdand confirm all eight models are listed with the three defaults called out.docs/models-and-parameters.mdnow describes — providers, not a fixed list of names.Note
The lock says what is missing but cannot say where to fix it — a disabled row is unclickable by construction (
ModelEffortPickerfilters selection on!_disabledReason). Making the row navigate to the plans page is the obvious next step for conversion and is listed as a non-goal here because it changes a component every picker shares. If the funnel wants it, the cheaper half is the settings model list, which renders rows rather than a select and can carry a real link without touching shared components.