Skip to content

feat(accounts): switch off any AI provider - #42

Merged
nplez1 merged 1 commit into
nplez1/mainfrom
nplez1/Disable-AI-Providers
Sep 29, 2026
Merged

nplez1 merged 1 commit into
nplez1/mainfrom
nplez1/Disable-AI-Providers

Conversation

@nplez1

@nplez1 nplez1 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner
Files Added Deleted Net
Test 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​195 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​194
Prod 26 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​739 $\color{#cf222e}{\Huge{\mathbf{−}}}$​155 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​584

ELI5

Each AI provider in Settings can now be switched off. While it is off, Orca stops asking that provider for your usage and stops reading its sign-in out of the apps already installed on this computer (Cursor, Codex, the GitHub CLI, Grok/Gemini/OpenCode, Claude). Flipping the switch back restores the old behaviour.

What Changed

Before: every provider Orca supports was always queried. If cursor-agent, codex, gh, or the Grok/Gemini CLI was installed and signed in, Orca read that sign-in and polled usage on its own, with no way to opt one provider out.

After: the top of each of the ten provider sections in Settings → AI Provider Accounts has a master switch ("Let Orca query Claude"). Turning it off:

  • stops all usage/quota polling for that provider;
  • stops every automatic local credential read for it — the cursor-agent Keychain and Cursor IDE DB, ~/.codex/auth.json, gh auth status, ~/.grok/auth.json, ~/.gemini/oauth_creds.json, OpenCode's auth.json/SQLite, the macOS Claude Keychain, and the MiniMax/DeepSeek/Fireworks/Copilot stores;
  • hides its status-bar meter and drops any cached snapshot (mobile and paired clients get null too);
  • collapses the section to an "off" notice, so its controls — and their mount-time reads — never run.

Providers stay on by default. The switched-off set persists in GlobalSettings.disabledUsageProviders (a profile that predates the key reads it as empty, i.e. everything on).

Why

Automatic pickup is the convenient default, but it is not always wanted: a machine may be signed in to a work account, Cursor may be installed for an unrelated reason, or a user simply does not want Orca touching a provider. The ask was to be able to switch that off completely, per provider.

The gate lives in the rate-limit service rather than the UI because that is where the reads happen. The service takes an isUsageProviderDisabled probe read from settings; that one predicate covers the full cycle, the codex/claude/grok-only cycles, the inactive-account probes, the on-disk credential-presence flags in getState(), and the codex reset-credit redemption. The pane's cursor/grok status endpoints and the startup gh probe are gated the same way, and RuntimeAccountController returns an empty Claude/Codex roster so a disabled provider's system-default identity is never read. Disabled state slots are forced to null last, which also defeats Claude's live-session stale-preservation.

Considered and rejected: gating in the renderer only (misses the startup probes and the account snapshot), and a per-provider opt-in (default-on is what makes the other providers work out of the box).

Linked Issue

N/A — fork-only feature with no tracking issue.

Visual Proof

A before/after capture is not attached from this environment. The change is one master switch at the top of each provider section, plus an "off" notice replacing that section's controls. AccountsPane.test.tsx asserts the switch label for all ten providers and the notice/suppression when one is off. Happy to attach a capture if you want one before merging.

Testing

Verification was automated, not a manual app run:

  • pnpm typecheck clean.

  • pnpm run check:code-quality:changed, pnpm run check:max-lines-ratchet, localization extraction/coverage and the RPC-params catalog all pass.

  • Affected suites: 329 files / 2702 tests — shared normalization, status-bar disabled cases, rate-limit service "does not fetch or read credentials", codex reset-credit refusal, grok disabled status, AccountsPane switch/notice.

  • Full suite with ORCA_BACKGROUND_LAUNCH=1: only pre-existing environmental failures remain (missing pg in cloud/, missing git tag v1.4.211), both of which pass in isolation.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Authored with an AI coding agent (DeepSeek, model deepseek-flash) in the pi harness. The plan was reviewed before implementation, the diff was self-reviewed, and typecheck plus the changed-code quality gate and affected test suites were run locally.

Review

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Explicit use is not blocked. Running the agent, commit-message/text generation, and adding/selecting/re-authenticating an account stay available when a provider is off. That matches the chosen scope ("running that agent in a terminal still works normally"); say if those should be blocked too.
  • Remote-owned accounts. disabledUsageProviders is a local desktop setting, like every other provider setting (geminiCliOAuthEnabled, opencodeSessionCookie), and is not in the RPC settings projection. A remote Orca Server that owns the account polling must set it on its own store.
  • Kimi and Antigravity have no switch (they have no Accounts-pane section); Antigravity's usage derives from Gemini, so disabling Gemini hides it too.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

Settings -> AI Provider Accounts now has a master switch at the top of each
provider section. Turning a provider off stops Orca both from polling its
usage/quota and from reading its sign-in out of software installed on this
computer (cursor-agent Keychain, ~/.codex/auth.json, gh CLI, the Grok, Gemini
and OpenCode auth files, the macOS Claude Keychain), hides its status-bar
meter, and drops any cached snapshot. Providers stay on by default; the
switched-off set persists in GlobalSettings.disabledUsageProviders.

The rate-limit service takes an isUsageProviderDisabled probe read from
settings, so the gate covers the full cycle, the codex/claude/grok-only
cycles, the inactive-account probes, the on-disk credential-presence flags and
the codex reset-credit redemption. The pane's cursor/grok status endpoints and
the startup gh probe are gated the same way, and RuntimeAccountController
returns an empty Claude/Codex roster so a disabled provider's system-default
identity is never read.
@nplez1
nplez1 force-pushed the nplez1/Disable-AI-Providers branch from 5370e8b to c224a5f Compare September 29, 2026 10:24
@nplez1
nplez1 merged this pull request into nplez1/main Sep 29, 2026
3 checks passed
nplez1 added a commit that referenced this pull request Sep 29, 2026
Settings -> AI Provider Accounts now has a master switch at the top of each
provider section. Turning a provider off stops Orca both from polling its
usage/quota and from reading its sign-in out of software installed on this
computer (cursor-agent Keychain, ~/.codex/auth.json, gh CLI, the Grok, Gemini
and OpenCode auth files, the macOS Claude Keychain), hides its status-bar
meter, and drops any cached snapshot. Providers stay on by default; the
switched-off set persists in GlobalSettings.disabledUsageProviders.

The rate-limit service takes an isUsageProviderDisabled probe read from
settings, so the gate covers the full cycle, the codex/claude/grok-only
cycles, the inactive-account probes, the on-disk credential-presence flags and
the codex reset-credit redemption. The pane's cursor/grok status endpoints and
the startup gh probe are gated the same way, and RuntimeAccountController
returns an empty Claude/Codex roster so a disabled provider's system-default
identity is never read.
nplez1 added a commit that referenced this pull request Oct 1, 2026
Settings -> AI Provider Accounts now has a master switch at the top of each
provider section. Turning a provider off stops Orca both from polling its
usage/quota and from reading its sign-in out of software installed on this
computer (cursor-agent Keychain, ~/.codex/auth.json, gh CLI, the Grok, Gemini
and OpenCode auth files, the macOS Claude Keychain), hides its status-bar
meter, and drops any cached snapshot. Providers stay on by default; the
switched-off set persists in GlobalSettings.disabledUsageProviders.

The rate-limit service takes an isUsageProviderDisabled probe read from
settings, so the gate covers the full cycle, the codex/claude/grok-only
cycles, the inactive-account probes, the on-disk credential-presence flags and
the codex reset-credit redemption. The pane's cursor/grok status endpoints and
the startup gh probe are gated the same way, and RuntimeAccountController
returns an empty Claude/Codex roster so a disabled provider's system-default
identity is never read.
nplez1 added a commit that referenced this pull request Oct 6, 2026
Settings -> AI Provider Accounts now has a master switch at the top of each
provider section. Turning a provider off stops Orca both from polling its
usage/quota and from reading its sign-in out of software installed on this
computer (cursor-agent Keychain, ~/.codex/auth.json, gh CLI, the Grok, Gemini
and OpenCode auth files, the macOS Claude Keychain), hides its status-bar
meter, and drops any cached snapshot. Providers stay on by default; the
switched-off set persists in GlobalSettings.disabledUsageProviders.

The rate-limit service takes an isUsageProviderDisabled probe read from
settings, so the gate covers the full cycle, the codex/claude/grok-only
cycles, the inactive-account probes, the on-disk credential-presence flags and
the codex reset-credit redemption. The pane's cursor/grok status endpoints and
the startup gh probe are gated the same way, and RuntimeAccountController
returns an empty Claude/Codex roster so a disabled provider's system-default
identity is never read.
nplez1 pushed a commit that referenced this pull request Oct 19, 2026
…f the deprecated owner resolver; spawn refuses folder twins (stablyai#30497)

* refactor(renderer): fold launch, resume, note-send, paste and terminal-create callers off the deprecated owner resolver

Agent launch, AI-vault resume and reveal, note target and send, wake and creation seeds, native chat
owner, the shell menu and active-workspace terminal creation (#22, #31-#42, #44) read the owner
through runtimeEnvironmentIdWithLegacyFallbackUnlessTwin. Paste uploads images to the server its
PTY id names before falling back to the workspace owner. The folder and floating branch of the
terminal spawn route (#53) reads resolveOwnerWithLegacyFallback: spawn refuses a folder twin
instead of following the focused server, and teardown keeps tearing raw ids down locally.
Deprecated importers 31 -> 16.

* test: give the paste-listener and tab-bar probes the fields the folded callers read

* test: note why the folder-twin route fixture is cast

* test: type the paste-owner twin fixture
@nplez1
nplez1 deleted the nplez1/Disable-AI-Providers branch October 24, 2026 03:23
nplez1 added a commit that referenced this pull request Oct 31, 2026
…header (#256)

* feat(accounts): give every provider one switch in its own header

The per-provider switch added in #42 was bolted on outside each section: a
bordered "Let Orca query <name>" box above the provider's own header, wired by a
hand-maintained gateUsageProvider(...) call per section. Two consequences:

- the switch never read as part of the provider it belongs to;
- providers added later never got one. antigravity and zcode had no switch at
  all (service-configuration.ts said so in a comment), and kimi had no section.

AI Provider Accounts is now driven by PROVIDER_ACCOUNTS_SECTIONS: one entry per
provider (id, icon, title, description, search entries, header link, body
renderer). ProviderAccountsSection renders that header with the switch leading
the row, so a provider cannot reach the pane without one, and the shared header
is what fixes the inconsistency the user sees. The twelve section renderers lost
their duplicated <section>/<h3>/subtitle markup.

kimi, antigravity and zcode joined USAGE_PROVIDER_IDS and are gated in main's
cycle: Kimi's home resolver is not called while off, antigravity folds into
antigravityFetchAllowed, and resolveZcodePlanConfig returns before reading the
encrypted plan key. Kimi got a minimal section (switch plus where its usage
comes from; it has no credentials to configure).

Tests: provider-accounts-sections.test.ts holds the registry against
USAGE_PROVIDER_IDS in both directions, and AccountsPane.test.tsx now iterates
the registry instead of a copied label list. AccountsPane.fixture.tsx renders
the whole pane for a screenshot in both states.

* fix(accounts): re-read the provider registry per translation and cover the new gates

Review round 1 found two blocking gaps.

The registry was a module-level const, so every provider title, description, the
switch's accessible name and the off notice were translated once at import — a
language change with no reload left them stale, and a non-English catalog that
loads after the module could strand English headings. It is now a
`createLocalizedCatalog` getter, the pattern `accounts-search.ts` already uses,
and the deleted gate had guarded exactly this. The module-scope label comment
that went with the old per-render labels is restored in spirit.

None of the three new main-process gates had a test: kimi, antigravity and zcode
were gated by one `disabled.has(...)` each with nothing to fail if it were
dropped. `service-refresh-orchestration.test.ts` gains one case per provider
asserting the resolver, the fetcher and the presence flag, not just the state.

Also: a compile-time assertion that every `ProviderRateLimits['provider']` is in
`USAGE_PROVIDER_IDS`, so a future metered provider cannot ship without a switch;
two comments cited the wrong test filename; and the usage-row settings-target
comment still said Kimi and Antigravity have no account settings.
nplez1 added a commit that referenced this pull request Nov 1, 2026
… Source Control (#267)

* feat(right-sidebar): pick between a workspace's reviews in Checks and Source Control

A workspace can hold several review attachments while its compatibility slots
name only one active review, and both panes only ever showed that one. The
Checks pane's review number and the Source Control header's review link are now
split controls: the link keeps opening the review, and a chevron lists every
review the workspace holds (`PR #12` / `MR !7`, with the title) and switches
which one the pane describes.

Switching drives the panes' reads, not just their labels:

- Checks re-reads checks, comments and review metadata for the picked review's
  number, drops the previous review's rows while that read is in flight, and
  maps the review onto the PR shape the pane already keys its head-sha
  freshness gate and comment actions on.
- Source Control's header link, review actions and compare base follow the
  pick. The file list, commit area, push target and PR creation keep resolving
  through the workspace's own review: pointing those at another branch's
  review would retarget a push.

The selection is the panes' view, not the workspace's link. It lives in the
fork's session-only renderer slice, keyed by worktree and shared between the
two panes so they cannot disagree, and never moves `linkedPR` — that slot is
what push-target and PR-creation safety resolve through. It is keyed on
provider and number rather than the attachment key, which moves when a
background refresh fills in a title or URL.

Unlinking now removes the review the switcher shows instead of the workspace's
slot, so it cannot unlink the review the user is not looking at. A picked
review is read through its own hosted-review cache key, so switching does not
overwrite the branch's review entry, and the Checks Refresh button refreshes
the picked review rather than the branch's.

* fix(right-sidebar): close the two blockers the review found in the review switcher

Round 1 (Opus 5.5, high) found two blocking defects and six should-fix items.

The first blocker: the Checks pane's Refresh took its number from any picked review,
so a picked GitLab MR sent `!7` to GitHub as PR #7 and a picked Bitbucket PR reused the
branch PR's head. The picked path now branches on the provider the pane actually
renders — GitHub checks and comments for a GitHub review, `fetchGitLabDetails` for an
MR, and the branch path for anything this pane cannot render.

The second: both panes cleared the shared pick whenever their own default matched it.
Their defaults come from different async sources, so a transient null in one pane wiped
the other's pick. Nothing writes the selection from a derived condition any more; the
selection's flag stays a per-pane render decision, and a stored key that names another
pane's default is simply that pane's default.

Should-fix items closed:

- The Checks switcher lists only reviews this pane can render, and its checked row is
  the review the header shows, so a listed row is never a review that is not on screen.
- Every "is a pick shown" decision reads the same value: the PR-check fetch no longer
  passes this worktree's branch for a picked review, which had been writing another
  PR's check status onto this branch's cached PR.
- Refresh-after-mutation and the entry refresh follow the pick; the triage strip's
  worktree-acting AI actions are hidden for a pick, because they start an agent in
  *this* worktree and would fix another branch's checks by committing here.
- Unlinking removes the row that carries the shown review and tombstones a GitHub one,
  so a slot-only clear cannot leave the review behind, and a failed update now reports.
- The selection is memoized, rows clear with a loading state, and the Refresh of a pick
  keys its results on the refreshed head instead of discarding them.

Tests cover the decision everything downstream reads (`shown-review.test.ts`), both
provider branches of a picked Refresh, the picked unlink, the header with a pick, and
that the hook never writes the shared selection on its own.

* fix(right-sidebar): close what the delta re-check found, including one regression of its own

The delta review (Opus 5.5, high) confirmed both round-1 blockers resolved and found
one blocker and three should-fix defects introduced by those fixes.

The blocker: the picked Refresh's staleness guard compared `asyncResultKeyRef.current`
with itself, which always passes, so a switch mid-flight wrote the old pick's rows
under the new review's header. The key is now captured locally, assigned to the ref,
and compared as a local — and the loading flags are cleared only by the request that
still owns them.

Should-fix items closed:

- `findWorkspaceReviewAttachment` matched issues too. A GitLab issue and an MR have
  separate number spaces, so unlinking `!42` could remove the issue `#42`; the search
  is filtered to review attachments, with a test that shares a number.
- Unlinking a picked GitHub review overwrote the `suppressedGitHubPR` tombstone of a
  branch PR the user had already unlinked, letting that PR come back. The tombstone is
  written only for the workspace's own review, which is the only one the branch lookup
  can resolve.
- The row-clear effect raised the loading flags on mount and on every return to the
  workspace review, where nothing guarantees a matching clear. It now runs on a switch
  only, and raises the flags only for a pick.

Minor items closed: the shared pick is cleared only when it names the review being
unlinked; Unlink is disabled again for a review with no row and no slot, whose unlink
would do nothing; the switcher never checks a row the header is not showing, in either
pane; a picked GitHub read no longer clears this branch PR's refresh error; and a
picked MR is re-read on tab entry and after a mutation, like the branch path.

Two round-1 gaps in coverage are also closed: the picked Refresh's guard is now
exercised with a real comparison and a mid-flight switch, and the polling path is
pinned to pass no branch for a pick.

* fix(right-sidebar): stop a mid-refresh switch from claiming the next review's result key

Round 3 (Opus 5.5, high, on the R2..R3 delta) confirmed eight of the nine round-2
findings resolved and found that N1 was only half-closed.

The remaining hole: after `refreshSelectedReview()` resolved, the picked Refresh only
checked `isCurrentRequest()`, which a review switch does not invalidate. It then claimed
`asyncResultKeyRef.current` with the *old* pick's key, so the old review's rows landed
under the new header and the new review's own in-flight read was discarded. The GitLab
sub-path had the same shape, passing no currency predicate to `fetchGitLabDetails`.

The fix answers "is this still the review on screen" from the stored selection rather
than the result key: the result key cannot tell a switch from the refreshed review
landing with a new head, which is the refresh working. Both the claim and the loading
flags now sit behind that check, while the refresh latch stays on the request alone so
it is always released.

Also closed: a pick whose row no longer matches no longer falls through to the branch
slot path, which would have addressed a review that is not on screen (and could write
the pick's number over an earlier branch-PR tombstone); the duplicate dependency in the
entry-refresh hook; and the redundant provider guard, now that
`findWorkspaceReviewAttachment` returns the narrowed attachment type.

Coverage added for the round-2 items that had none: a switch inside
`refreshSelectedReview`, an issue and an MR sharing a number with no URLs (asserting the
issue survives the removal), no tombstone written for a pick, no slot path for a pick
with no row, and no branch-PR error retraction from a picked read.

* test(right-sidebar): make the picked-refresh and picked-unlink assertions able to fail

Round 4 (Opus 5.5, high, on the R3..R4 delta) came back clean: no blockers, no
should-fix, and all seven round-3 findings resolved. Its three NITs were all about
tests that could not fail, so they are worth closing even though a NIT never blocks.

- The tombstone assertion used `expect.not.objectContaining({ suppressedGitHubPR:
  expect.anything() })`, which passes for a write of `suppressedGitHubPR: null` —
  exactly the write that would clear an earlier branch-PR tombstone and let that PR
  come back. It now asserts the key's absence.
- The mid-refresh-switch test now also pins that the result key was never claimed and
  that the refresh latch was released; a regression gating the latch on the pick check
  would have left Refresh disabled for good with no test failing. A second case switches
  the selection after the key was claimed, so the post-read pick check has its own test
  instead of relying on the result-key half.
- The issue/MR number-collision test moved to the GitLab unlink block, where it belongs,
  and its comment was wrong.

Both new guards were checked against their own absence: removing the post-read pick
check fails the claimed-key test, and removing the pick-with-no-row guard fails the
no-row test. The issue/MR test already discriminated, since the removal is keyed on an
attachment identity that folds in the type.

One round-4 assumption was checked and left alone: the GitLab details path clears its
loading flags on an abandoned request, which looks like the round-3 flags bug but is
deliberate — a generation counter keeps it off a newer request, and an upstream test
pins that an abandoned relink must release them or the pane keeps a spinner forever.
Adding a currency guard there broke that test, so the change was reverted rather than
the contract.
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