diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md
index c86576ba277..dee522f517b 100644
--- a/notes/host-context-implementation-checklist.md
+++ b/notes/host-context-implementation-checklist.md
@@ -84,9 +84,9 @@ Done means: scheduled or manual automations behave predictably when a project ex
## 6. Settings, Accounts, And Host Ownership
-- [~] Clarify which settings are client-owned versus host-owned.
-- [~] Add host/account selectors to provider settings where credentials may differ by host.
-- [~] Make provider rate limits, diagnostics, and “current account” views show their host/source context.
+- [x] Clarify which settings are client-owned versus host-owned.
+- [x] Add host/account selectors to provider settings where credentials may differ by host.
+- [x] Make provider rate limits, diagnostics, and “current account” views show their host/source context.
- [x] Audit source-control AI, agent defaults, command defaults, and model selections for whether they should be client-global or host-scoped.
- [x] Preserve existing Windows/WSL-style setting patterns where they already solve host-specific settings.
- [x] Ensure remote server version/capability information is visible enough to explain unavailable controls.
@@ -258,3 +258,4 @@ Done means: reviewers and future agents can understand the vision, implementatio
- [x] 2026-06-12: Extracted and tested external automation source availability copy so local missing tooling, disconnected SSH sources, connecting SSH sources, and connected SSH hosts with missing remote tooling get distinct states; verified with focused automation availability tests, targeted oxlint, and full `pnpm run typecheck`.
- [x] 2026-06-12: Marked repo-id/path compatibility adapters complete after hardening the renderer project-host selector to merge repo-derived compatibility rows when hydrated project/setup arrays are empty or partial, alongside existing shared projection, persistence backfill/sync, automation context migration, and pending worktree context coverage. Verified with selector/projection tests, persistence tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`.
- [x] 2026-06-12: Marked the Host copy audit complete after replacing remaining visible stale SSH/project wording in paired-web project creation errors, Settings SSH search entries, AI Vault SSH-host workspace copy, and SSH shutdown errors. Verified with a stale-copy scan showing only comments/tests/technical internals, focused SSH IPC tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`.
+- [x] 2026-06-12: Completed Settings ownership/account rows by adding a shared provider Host scope control with a Change Host action that opens Settings > Active Server from GitHub/GitLab/Linear/Jira account surfaces and GitHub/GitLab API budget panels. Existing ownership diagnostics still distinguish desktop-client versus remote-server credentials, and rate-limit panels show the budget Host scope. Verified with settings ownership, RuntimeEnvironmentsPane, provider account, source-control card, task-tracker card, Jira card, and rate-limit panel tests, targeted oxlint, full `pnpm run typecheck`, and `git diff --check`.
diff --git a/src/renderer/src/components/github/github-rate-limit-display.tsx b/src/renderer/src/components/github/github-rate-limit-display.tsx
index f05f7ac479e..38297503285 100644
--- a/src/renderer/src/components/github/github-rate-limit-display.tsx
+++ b/src/renderer/src/components/github/github-rate-limit-display.tsx
@@ -6,6 +6,7 @@ import { useAppStore } from '@/store'
import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import type { GetRateLimitResult, GitHubRateLimitSnapshot } from '../../../../shared/types'
import { getProviderRateLimitScope } from '@/components/settings/provider-account-scope'
+import { ProviderHostScopeControl } from '@/components/settings/ProviderHostScopeControl'
import { translate } from '@/i18n/i18n'
const REFRESH_INTERVAL_MS = 60_000
@@ -190,16 +191,14 @@ export function GitHubRateLimitPanel({ className }: { className?: string }): Rea
'Orca uses REST, Search, and GraphQL through the GitHub CLI.'
)}
-
-
- {translate(
- 'auto.components.github.github.rate.limit.display.budget_scope',
- 'Budget scope: {{value0}}',
- { value0: budgetScope.label }
- )}
- {' '}
- {budgetScope.description}
-
+
-
-
- {translate(
- 'auto.components.gitlab.gitlab.rate.limit.display.budget_scope',
- 'Budget scope: {{value0}}',
- { value0: budgetScope.label }
- )}
- {' '}
- {budgetScope.description}
-
+
state.openSettingsPage)
+ const openSettingsTarget = useAppStore((state) => state.openSettingsTarget)
+
+ const openActiveServer = (): void => {
+ openSettingsPage()
+ openSettingsTarget({ pane: 'servers', repoId: null })
+ }
+
+ return (
+
+
+
+
+ {translate(
+ 'auto.components.settings.ProviderHostScopeControl.scope_label',
+ '{{value0}}: {{value1}}',
+ { value0: labelPrefix, value1: scope.label }
+ )}
+
+
{scope.description}
+
+
+
+ {translate(
+ 'auto.components.settings.ProviderHostScopeControl.change_host',
+ 'Change Host'
+ )}
+
+
+
+ )
+}
diff --git a/src/renderer/src/components/settings/cli-source-control-integration-cards.test.tsx b/src/renderer/src/components/settings/cli-source-control-integration-cards.test.tsx
index 0b35b557fb8..34d3196a9fb 100644
--- a/src/renderer/src/components/settings/cli-source-control-integration-cards.test.tsx
+++ b/src/renderer/src/components/settings/cli-source-control-integration-cards.test.tsx
@@ -10,6 +10,8 @@ import {
type StoreState = {
settings: { activeRuntimeEnvironmentId: string | null }
+ openSettingsPage: () => void
+ openSettingsTarget: (target: { pane: string; repoId: string | null }) => void
}
const mocks = vi.hoisted(() => ({
@@ -74,7 +76,13 @@ describe('CLI source-control integration card account scope', () => {
})
it('shows local-client ownership for connected GitHub CLI credentials', async () => {
- mocks.store.current = { settings: { activeRuntimeEnvironmentId: null } }
+ const openSettingsPage = vi.fn()
+ const openSettingsTarget = vi.fn()
+ mocks.store.current = {
+ settings: { activeRuntimeEnvironmentId: null },
+ openSettingsPage,
+ openSettingsTarget
+ }
const rendered = await renderCard( )
@@ -84,10 +92,22 @@ describe('CLI source-control integration card account scope', () => {
expect(rendered.textContent).toContain(
'Credentials and account checks for this provider are owned by this desktop client. Choose a remote Host from Settings > Active Server to edit server-owned credentials.'
)
+ await act(async () => {
+ Array.from(rendered.querySelectorAll('button'))
+ .find((button) => button.textContent === 'Change Host')
+ ?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ })
+
+ expect(openSettingsPage).toHaveBeenCalledTimes(1)
+ expect(openSettingsTarget).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
})
it('shows remote-server ownership for GitLab CLI credential checks', async () => {
- mocks.store.current = { settings: { activeRuntimeEnvironmentId: 'runtime-1' } }
+ mocks.store.current = {
+ settings: { activeRuntimeEnvironmentId: 'runtime-1' },
+ openSettingsPage: vi.fn(),
+ openSettingsTarget: vi.fn()
+ }
mocks.preflight.statuses.glabStatus = 'not-authenticated'
const rendered = await renderCard( )
diff --git a/src/renderer/src/components/settings/cli-source-control-integration-cards.tsx b/src/renderer/src/components/settings/cli-source-control-integration-cards.tsx
index 3c3508d04ea..0007e594d02 100644
--- a/src/renderer/src/components/settings/cli-source-control-integration-cards.tsx
+++ b/src/renderer/src/components/settings/cli-source-control-integration-cards.tsx
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'
import { useAppStore } from '@/store'
import { IntegrationCardDetails, IntegrationCardShell } from './integration-card-shell'
import { getProviderAccountScope } from './provider-account-scope'
+import { ProviderHostScopeControl } from './ProviderHostScopeControl'
import { usePreflightCardStatuses } from './source-control-preflight-card-status'
import { translate } from '@/i18n/i18n'
@@ -16,16 +17,14 @@ function ProviderAccountScopeDetails({
return (
-
-
- {translate(
- 'auto.components.settings.cli.source.control.integration.cards.account_scope',
- 'Account scope: {{value0}}',
- { value0: accountScope.label }
- )}
-
-
{accountScope.description}
-
+
{children}
)
diff --git a/src/renderer/src/components/settings/jira-integration-card.test.tsx b/src/renderer/src/components/settings/jira-integration-card.test.tsx
new file mode 100644
index 00000000000..5c46467c312
--- /dev/null
+++ b/src/renderer/src/components/settings/jira-integration-card.test.tsx
@@ -0,0 +1,115 @@
+// @vitest-environment happy-dom
+
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context'
+import { JiraIntegrationCard } from './jira-integration-card'
+
+type StoreState = {
+ jiraStatus: {
+ connected: boolean
+ sites?: { id: string; displayName: string; siteUrl: string; email?: string }[]
+ }
+ jiraStatusChecked: boolean
+ jiraStatusContextKey: string | null
+ checkJiraConnection: () => Promise
+ disconnectJira: (siteId?: string) => Promise
+ testJiraConnection: (siteId: string) => Promise<{ ok: boolean; error?: string }>
+ settings: { activeRuntimeEnvironmentId: string | null }
+ openSettingsPage: () => void
+ openSettingsTarget: (target: { pane: string; repoId: string | null }) => void
+}
+
+const mocks = vi.hoisted(() => ({
+ store: { current: null as StoreState | null }
+}))
+
+vi.mock('@/store', () => ({
+ useAppStore: (selector: (state: StoreState) => unknown) => {
+ if (!mocks.store.current) {
+ throw new Error('Store state was not installed')
+ }
+ return selector(mocks.store.current)
+ }
+}))
+
+vi.mock('@/components/jira-connect-dialog', () => ({
+ JiraConnectDialog: ({ onConnected }: { onConnected?: () => void }) => (
+
+ Simulate Jira connected
+
+ )
+}))
+
+let root: Root | null = null
+let container: HTMLDivElement | null = null
+
+function installStore(settings: StoreState['settings']): StoreState {
+ const state: StoreState = {
+ jiraStatus: {
+ connected: true,
+ sites: [
+ {
+ id: 'site-1',
+ displayName: 'Acme Jira',
+ siteUrl: 'https://acme.atlassian.net',
+ email: 'jira@example.test'
+ }
+ ]
+ },
+ jiraStatusChecked: true,
+ jiraStatusContextKey: getProviderRuntimeContextKey(settings),
+ checkJiraConnection: vi.fn(async () => {}),
+ disconnectJira: vi.fn(async () => {}),
+ testJiraConnection: vi.fn(async () => ({ ok: true })),
+ settings,
+ openSettingsPage: vi.fn(),
+ openSettingsTarget: vi.fn()
+ }
+ mocks.store.current = state
+ return state
+}
+
+async function renderCard(): Promise {
+ container = document.createElement('div')
+ document.body.appendChild(container)
+ root = createRoot(container)
+ await act(async () => {
+ root?.render( )
+ })
+ return container
+}
+
+describe('JiraIntegrationCard account scope', () => {
+ afterEach(async () => {
+ if (root) {
+ await act(async () => {
+ root?.unmount()
+ })
+ }
+ root = null
+ container?.remove()
+ container = null
+ mocks.store.current = null
+ })
+
+ it('shows remote-server account ownership and opens Active Server host selection', async () => {
+ const state = installStore({ activeRuntimeEnvironmentId: 'runtime-1' })
+
+ const rendered = await renderCard()
+
+ expect(rendered.textContent).toContain('Account scope: Remote server: runtime-1')
+ expect(rendered.textContent).toContain('Acme Jira')
+ expect(rendered.textContent).toContain('https://acme.atlassian.net · jira@example.test')
+
+ await act(async () => {
+ Array.from(rendered.querySelectorAll('button'))
+ .find((button) => button.textContent === 'Change Host')
+ ?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ })
+
+ expect(state.openSettingsPage).toHaveBeenCalledTimes(1)
+ expect(state.openSettingsTarget).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
+ })
+})
diff --git a/src/renderer/src/components/settings/jira-integration-card.tsx b/src/renderer/src/components/settings/jira-integration-card.tsx
index a7e97b1ac05..32b1a472467 100644
--- a/src/renderer/src/components/settings/jira-integration-card.tsx
+++ b/src/renderer/src/components/settings/jira-integration-card.tsx
@@ -11,6 +11,7 @@ import {
import { useAppStore } from '@/store'
import { IntegrationCardDetails, IntegrationCardShell } from './integration-card-shell'
import { getProviderAccountScope } from './provider-account-scope'
+import { ProviderHostScopeControl } from './ProviderHostScopeControl'
import { translate } from '@/i18n/i18n'
type VerificationResult = { state: 'ok' | 'error'; error?: string }
@@ -108,16 +109,14 @@ export function JiraIntegrationCard(): React.JSX.Element {
) : null
}
>
-
-
- {translate(
- 'auto.components.settings.task.tracker.integration.cards.account_scope',
- 'Account scope: {{value0}}',
- { value0: accountScope.label }
- )}
-
-
{accountScope.description}
-
+
{connected && sites.length > 0 ? (
{sites.map((site) => {
diff --git a/src/renderer/src/components/settings/provider-rate-limit-scope-panels.test.tsx b/src/renderer/src/components/settings/provider-rate-limit-scope-panels.test.tsx
index 38139024482..8415afadece 100644
--- a/src/renderer/src/components/settings/provider-rate-limit-scope-panels.test.tsx
+++ b/src/renderer/src/components/settings/provider-rate-limit-scope-panels.test.tsx
@@ -5,10 +5,18 @@ import { GitLabRateLimitPanel } from '@/components/gitlab/gitlab-rate-limit-disp
type StoreState = {
settings: { activeRuntimeEnvironmentId: string | null }
+ openSettingsPage: () => void
+ openSettingsTarget: (target: { pane: string; repoId: string | null }) => void
}
const mocks = vi.hoisted(() => ({
- store: { current: { settings: { activeRuntimeEnvironmentId: null } } as StoreState }
+ store: {
+ current: {
+ settings: { activeRuntimeEnvironmentId: null },
+ openSettingsPage: vi.fn(),
+ openSettingsTarget: vi.fn()
+ } as StoreState
+ }
}))
vi.mock('@/store', () => ({
@@ -17,7 +25,11 @@ vi.mock('@/store', () => ({
describe('provider rate-limit panels account scope', () => {
it('shows the local host scope for GitHub API budget', () => {
- mocks.store.current = { settings: { activeRuntimeEnvironmentId: null } }
+ mocks.store.current = {
+ settings: { activeRuntimeEnvironmentId: null },
+ openSettingsPage: vi.fn(),
+ openSettingsTarget: vi.fn()
+ }
const markup = renderToStaticMarkup(
)
@@ -25,10 +37,15 @@ describe('provider rate-limit panels account scope', () => {
expect(markup).toContain(
'GitHub API budget is fetched from the CLI on this desktop client. Choose a remote Host from Settings > Active Server to view server-owned budgets.'
)
+ expect(markup).toContain('Change Host')
})
it('shows the remote server scope for GitLab API budget', () => {
- mocks.store.current = { settings: { activeRuntimeEnvironmentId: 'runtime-1' } }
+ mocks.store.current = {
+ settings: { activeRuntimeEnvironmentId: 'runtime-1' },
+ openSettingsPage: vi.fn(),
+ openSettingsTarget: vi.fn()
+ }
const markup = renderToStaticMarkup(
)
diff --git a/src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx b/src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx
index 2498c5cc170..307a2bff5d3 100644
--- a/src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx
+++ b/src/renderer/src/components/settings/task-tracker-integration-cards.test.tsx
@@ -18,6 +18,8 @@ type StoreState = {
checkLinearConnection: (force?: boolean) => Promise
testLinearConnection: (workspaceId: string) => Promise<{ ok: boolean; error?: string }>
settings: { activeRuntimeEnvironmentId: string | null }
+ openSettingsPage: () => void
+ openSettingsTarget: (target: { pane: string; repoId: string | null }) => void
}
const mocks = vi.hoisted(() => ({
@@ -68,7 +70,9 @@ function installStore(
disconnectLinearWorkspace: vi.fn(async () => {}),
checkLinearConnection: vi.fn(async () => {}),
testLinearConnection: vi.fn(async () => ({ ok: true })),
- settings
+ settings,
+ openSettingsPage: vi.fn(),
+ openSettingsTarget: vi.fn()
}
mocks.store.current = state
return state
@@ -106,6 +110,7 @@ describe('LinearIntegrationCard account scope', () => {
expect(rendered.textContent).toContain(
'Credentials and account checks for this provider are owned by this desktop client. Choose a remote Host from Settings > Active Server to edit server-owned credentials.'
)
+ expect(rendered.textContent).toContain('Change Host')
expect(rendered.textContent).toContain('Add access with a Personal API key')
await act(async () => {
@@ -126,6 +131,13 @@ describe('LinearIntegrationCard account scope', () => {
expect(rendered.textContent).toContain(
'Credentials and account checks for this provider are owned by this remote server. Choose a different Host from Settings > Active Server to edit another account scope.'
)
+ await act(async () => {
+ Array.from(rendered.querySelectorAll('button'))
+ .find((button) => button.textContent === 'Change Host')
+ ?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ })
+ expect(state.openSettingsPage).toHaveBeenCalledTimes(1)
+ expect(state.openSettingsTarget).toHaveBeenCalledWith({ pane: 'servers', repoId: null })
expect(rendered.textContent).toContain('Acme')
expect(rendered.textContent).toContain('Acme workspace · linear@example.test')
diff --git a/src/renderer/src/components/settings/task-tracker-integration-cards.tsx b/src/renderer/src/components/settings/task-tracker-integration-cards.tsx
index 40b7d2aa583..7c89a30584e 100644
--- a/src/renderer/src/components/settings/task-tracker-integration-cards.tsx
+++ b/src/renderer/src/components/settings/task-tracker-integration-cards.tsx
@@ -8,6 +8,7 @@ import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context'
import { useAppStore } from '@/store'
import { IntegrationCardDetails, IntegrationCardShell } from './integration-card-shell'
import { getProviderAccountScope } from './provider-account-scope'
+import { ProviderHostScopeControl } from './ProviderHostScopeControl'
import { translate } from '@/i18n/i18n'
type VerificationResult = { state: 'ok' | 'error'; error?: string }
@@ -214,16 +215,14 @@ export function LinearIntegrationCard(): React.JSX.Element {
function ProviderAccountScopeRow({ scope }: { scope: ReturnType }) {
return (
-
-
- {translate(
- 'auto.components.settings.task.tracker.integration.cards.account_scope',
- 'Account scope: {{value0}}',
- { value0: scope.label }
- )}
-
-
{scope.description}
-
+
)
}