Skip to content

Show the Copilot model lineup to users without a license - #2778

Merged
logancyang merged 11 commits into
v4-previewfrom
feat/copilot-model-discoverability
Aug 8, 2026
Merged

Show the Copilot model lineup to users without a license#2778
logancyang merged 11 commits into
v4-previewfrom
feat/copilot-model-discoverability

Conversation

@logancyang

@logancyang logancyang commented Aug 7, 2026

Copy link
Copy Markdown
Owner

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 syncCopilotPlusProvider registers 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.md was 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.

Surface Before After (no license) After (licensed)
Agent Mode picker, OpenCode section your models only three Copilot rows on top, greyed, locked unchanged — real Copilot rows
Agent Mode picker, Claude Code / Codex your models only unchanged unchanged
Quick Chat picker your models only same three rows on top unchanged
Quick Chat with no models at all "No models — enable in…" the three rows, then that same guidance unchanged
Locked row's right-side label none; the lock carries the reason

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.

Before: a new user opens the picker, sees two models they set up themselves, and has no way to learn a license would add more.

After: the same picker leads with Copilot Plus Flash, DeepSeek V4 Pro, and GLM-5.2 — locked, described, and unmistakably on offer.

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 Provider or ConfiguredModel rows, 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.md also 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

  • Locked rows are not clickable through to the plans page; that needs an action escape hatch in the shared ModelSelector, which every picker in the app consumes.
  • No provider or configured model is registered without a license — that would make unusable models selectable and let license-time seeding point at one.
  • Nothing changes for a licensed user in any picker.
  • The relay's lineup, model ids, and descriptions are untouched.
  • No upsell is added to Claude Code or Codex, whose models come from their own subscriptions.
  • vault-search-and-indexing.md still 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 Selector

UI/Model Selector carries three stories: Licensed (your models, nothing locked), Unlicensed (the locked rows leading the section — open the picker, then hover a lock), and UnlicensedWithNoModelsOfTheirOwn (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

Criterion Status Reason
No behavior change, or a cosmetic/copy/docs/config change visible where it renders, or deterministic tests cover the changed behavior lockedCopilotEntries.test.ts covers the row contents and the provider gate; agentModelPickerHelpers.test.ts covers placement, per-agent eligibility, the licensed case, and that locked rows do not suppress the loading placeholder; ModelSelector.test.tsx opens the menu and asserts the lock renders with no right-side label
A defect would fail CI or be obvious on first use Those tests run in CI, and a wrong result is a visible row in the first picker anyone opens
A revert fully restores prior state, including persisted data Nothing is written — the rows exist only for the duration of a render
No auth, permissions, secrets, or input-handling surface changes Reads the provider list; no credential path touched
No public API, plugin API, message, or on-disk contract changes BackendDescriptor.routesCopilotModels is internal to Agent Mode and all three implementers are updated in this diff; _needsLicense is an optional field on an internal picker type
No core-path concurrency, async-lifecycle, or state-machine changes Pure synthesis inside existing render paths
No hot-path behavior lacks deterministic coverage Every branch of the synthesizer, the gate, and the placement is covered
No new dependency Dependency manifests unchanged; the lock is a lucide icon already bundled
Human-only behavior stays in one feature area and surfaces quickly Confined to the two model pickers, visible immediately

Review: skim src/lib/lockedCopilotEntries.ts for what a row is and when it appears, then run Verification step 1. If you only read one hunk, read the splice in buildPickerEntries — its position after the readiness and self-host passes is what keeps a locked row from being relabelled with the wrong reason.

Verification

  1. In a vault with no license, open the Agent Mode composer's model picker. Copilot Plus Flash, DeepSeek V4 Pro, and GLM-5.2 lead the OpenCode section, greyed, each with a lock and a one-line description. Hover a lock: "Copilot license required". Click a locked row and arrow through the list — neither can select one.
  2. Confirm the Claude Code and Codex sections show only their own models, with no locked rows.
  3. Open the Quick Chat model picker and confirm the same three rows lead it.
  4. Enter a valid license key. Both pickers now show the real Copilot models — no locks, no duplicates, and the rows sit where the locked ones did.
  5. Remove the license key. The locked rows come back and the real ones go.
  6. Read the Models included with your license section of docs/copilot-plus-and-self-host.md and confirm all eight models are listed with the three defaults called out.
  7. Open Settings → Copilot → Models (BYOK) and confirm the built-in models it offers match what docs/models-and-parameters.md now describes — providers, not a fixed list of names.
  8. Read that doc end to end and confirm every settings path it names exists: there is no QA tab, no Model tab, and no embedding-model picker to send anyone to.

Note

The lock says what is missing but cannot say where to fix it — a disabled row is unclickable by construction (ModelEffortPicker filters 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.

logancyang and others added 4 commits August 7, 2026 15:48
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>
@logancyang
logancyang marked this pull request as ready for review August 7, 2026 23:11

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/components/ui/ModelSelector.tsx
logancyang and others added 2 commits August 7, 2026 16:14
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/components/ui/ModelSelector.tsx Outdated
logancyang and others added 3 commits August 7, 2026 17:15
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/components/ui/LicenseRequiredIcon.tsx
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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/settings/v2/components/configuredModelGrouping.ts Outdated
…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>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +39 to +40
export function shouldPreviewCopilotModels(providers: CopilotSettings["providers"]): boolean {
return !Object.values(providers).some((provider) => provider.origin.kind === "copilot-plus");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@logancyang
logancyang merged commit 58d0aa2 into v4-preview Aug 8, 2026
2 checks passed
@logancyang
logancyang deleted the feat/copilot-model-discoverability branch August 8, 2026 01:41
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.

1 participant