fix(settings): show a way back to local accounts when a remote server owns provider-account scope (#8188)

Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
This commit is contained in:
BingZ 2026-07-26 17:51:11 +08:00 committed by GitHub
parent d8af1d2196
commit ca8481ec73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 125 additions and 4 deletions

View File

@ -23,7 +23,7 @@ function renderPane(
describe('AccountsPane', () => {
beforeEach(async () => {
await i18n.changeLanguage('en')
useAppStore.setState({ settingsSearchQuery: '' })
useAppStore.setState({ settingsSearchQuery: '', runtimeEnvironments: [] })
})
it('hides the WSL account location controls on platforms without WSL support', () => {
@ -116,6 +116,14 @@ describe('AccountsPane', () => {
expect(markup).toContain(
'Showing accounts managed by the remote server. Add or re-authenticate accounts on that server.'
)
// Both the Claude and Codex sections must say local accounts are intact and
// link the default-runtime control, so the scoped list never reads as loss.
expect(markup.split('Accounts managed on this desktop are unchanged').length - 1).toBe(2)
expect(markup.split('Open Remote Servers').length - 1).toBe(2)
// Before the saved-server list loads there is no name to interpolate, so the
// scope label must stay bare instead of stuttering the prose fallback.
expect(markup).toContain('Account scope: Remote server<')
expect(markup).not.toContain('Remote server: the remote server')
// The WSL account-location toggle is a local concern; a remote owner hides it.
expect(markup).not.toContain('aria-label="Account location"')
const addAccountIndex = markup.indexOf('Add Account')
@ -125,10 +133,34 @@ describe('AccountsPane', () => {
)
})
it('omits the scope control on the web client, which cannot select Local desktop', () => {
const webGlobal = globalThis as { window?: { __ORCA_WEB_CLIENT__?: boolean } }
const hadWindow = 'window' in webGlobal
webGlobal.window = { ...webGlobal.window, __ORCA_WEB_CLIENT__: true }
try {
const markup = renderPane({
...getDefaultSettings('/tmp'),
activeRuntimeEnvironmentId: 'env-1'
})
// The web client has no desktop-managed accounts to switch back to, so
// this copy would promise a move it cannot make.
expect(markup).not.toContain('Accounts managed on this desktop are unchanged')
expect(markup).not.toContain('Open Remote Servers')
// The server-scope copy itself still applies.
expect(markup).toContain('Showing accounts managed by')
} finally {
if (!hadWindow) {
delete webGlobal.window
}
}
})
it('keeps local copy and enabled sign-in actions when no remote server is active', () => {
const markup = renderPane(getDefaultSettings('/tmp'))
expect(markup).toContain('Showing accounts for this device. New accounts are added there.')
expect(markup).not.toContain('Open Remote Servers')
const addAccountIndex = markup.indexOf('Add Account')
expect(addAccountIndex).toBeGreaterThan(0)
expect(

View File

@ -52,6 +52,8 @@ import {
getAccountsPaneSearchEntries
} from './accounts-search'
import { GrokAccountsSection } from './GrokAccountsSection'
import { getRemoteAccountsPaneScope } from './provider-account-scope'
import { ProviderHostScopeControl } from './ProviderHostScopeControl'
import { SearchableSetting } from './SearchableSetting'
import { SettingsRow, SettingsSegmentedControl } from './SettingsFormControls'
import { matchesSettingsSearch } from './settings-search'
@ -77,6 +79,7 @@ import {
} from './provider-account-visibility'
import { translate } from '@/i18n/i18n'
import { cn } from '@/lib/utils'
import { isWebClientLocation } from '@/lib/web-client-location'
import {
emptyClaudeAccountsState,
emptyCodexAccountsState,
@ -350,9 +353,14 @@ export function AccountsPane({
// (see #7973); every list/select/remove below must scope to it, not host/WSL.
const isRemoteAccountScope = hasRemoteProviderAccountOwner(settings)
const activeRuntimeEnvironmentId = settings.activeRuntimeEnvironmentId?.trim() || null
const remoteServerLabel = isRemoteAccountScope
// Why: keep the real name separate from the prose fallback below; the scope
// label must not interpolate the fallback.
const remoteServerName = isRemoteAccountScope
? (runtimeEnvironments.find((environment) => environment.id === activeRuntimeEnvironmentId)
?.name ??
?.name ?? null)
: null
const remoteServerLabel = isRemoteAccountScope
? (remoteServerName ??
translate('auto.components.settings.AccountsPane.remoteServerFallback', 'the remote server'))
: null
const accountRuntime: LocalAccountRuntime = isRemoteAccountScope
@ -369,6 +377,21 @@ export function AccountsPane({
localAccountRuntime.runtime === 'host' && !navigator.userAgent.includes('Windows')
? `${localAccountRuntime.label.charAt(0).toLocaleLowerCase()}${localAccountRuntime.label.slice(1)}`
: localAccountRuntime.label
// Why: users read the remote-scoped list as their desktop accounts being
// deleted (#8186); say they are intact and link the default-runtime control.
// The web client has no desktop-owned accounts and cannot select Local
// desktop, so promising a switch back would be a dead end there.
const remoteAccountScopeNotice =
isRemoteAccountScope && !isWebClientLocation() ? (
<ProviderHostScopeControl
labelPrefix={translate(
'auto.components.settings.AccountsPane.accountScopePrefix',
'Account scope'
)}
scope={getRemoteAccountsPaneScope(remoteServerName)}
className="text-xs"
/>
) : null
const [codexAccounts, setCodexAccounts] =
useState<CodexRateLimitAccountsState>(emptyCodexAccountsState)
@ -886,6 +909,7 @@ export function AccountsPane({
) : null}
</div>
</div>
{remoteAccountScopeNotice}
<div className="space-y-2">
<button
@ -1208,6 +1232,7 @@ export function AccountsPane({
{translate('auto.components.settings.AccountsPane.b0e948a4f9', 'Add Account')}
</Button>
</div>
{remoteAccountScopeNotice}
<div className="space-y-2">
<button

View File

@ -1,6 +1,10 @@
import { describe, expect, it } from 'vitest'
import { getExecutionHostLabel } from '../../../../shared/execution-host'
import { getProviderAccountScope, getProviderRateLimitScope } from './provider-account-scope'
import {
getProviderAccountScope,
getProviderRateLimitScope,
getRemoteAccountsPaneScope
} from './provider-account-scope'
const LOCAL_HOST_LABEL = getExecutionHostLabel('local')
@ -34,3 +38,24 @@ describe('getProviderAccountScope', () => {
})
})
})
describe('getRemoteAccountsPaneScope', () => {
const LOCAL_ACCOUNTS_KEPT =
'Accounts managed on this desktop are unchanged. Switch the default runtime back to Local desktop to view them.'
it('names the owning server once the saved-server list resolves', () => {
expect(getRemoteAccountsPaneScope(' build-box ')).toEqual({
label: 'Remote server: build-box',
description: LOCAL_ACCOUNTS_KEPT
})
})
it('keeps the label bare before the server name is known', () => {
for (const unnamed of [null, '', ' ']) {
expect(getRemoteAccountsPaneScope(unnamed)).toEqual({
label: 'Remote server',
description: LOCAL_ACCOUNTS_KEPT
})
}
})
})

View File

@ -12,6 +12,30 @@ export type ProviderRateLimitScope = {
description: string
}
/** Accounts-pane scope while a remote server owns the roster (#8186). */
export function getRemoteAccountsPaneScope(serverName: string | null): ProviderAccountScope {
const name = serverName?.trim()
return {
// Why: the saved-server list is still empty on first paint, and the generic
// fallback is already a noun phrase — interpolating it would render
// "Remote server: the remote server".
label: name
? translate(
'auto.components.settings.providerAccountScope.remoteServer',
'Remote server: {{value0}}',
{ value0: name }
)
: translate(
'auto.components.settings.AccountsPane.accountScopeRemoteServerUnnamed',
'Remote server'
),
description: translate(
'auto.components.settings.AccountsPane.remoteScopeLocalAccountsKept',
'Accounts managed on this desktop are unchanged. Switch the default runtime back to Local desktop to view them.'
)
}
}
export function getProviderAccountScope(
settings: Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> | null | undefined
): ProviderAccountScope {

View File

@ -5084,6 +5084,9 @@
"remoteServerFallback": "the remote server",
"loadAccountsFailed": "Could not load provider accounts.",
"remoteScopeAccounts": "Showing accounts managed by {{value0}}. Add or re-authenticate accounts on that server.",
"accountScopePrefix": "Account scope",
"accountScopeRemoteServerUnnamed": "Remote server",
"remoteScopeLocalAccountsKept": "Accounts managed on this desktop are unchanged. Switch the default runtime back to Local desktop to view them.",
"remoteScopeAuthContext": "Each account keeps its own sign-in context on {{value0}}.",
"remoteEmptyClaudeAccounts": "No managed Claude accounts on {{value0}}. It uses its system default Claude login; add accounts on that server.",
"remoteEmptyCodexAccounts": "No managed Codex accounts on {{value0}}. It uses its system default Codex login; add accounts on that server.",

View File

@ -5061,6 +5061,9 @@
"remoteServerFallback": "el servidor remoto",
"loadAccountsFailed": "No se pudieron cargar las cuentas de proveedor.",
"remoteScopeAccounts": "Mostrando las cuentas administradas por {{value0}}. Agrega o vuelve a autenticar cuentas en ese servidor.",
"accountScopePrefix": "Alcance de la cuenta",
"accountScopeRemoteServerUnnamed": "Servidor remoto",
"remoteScopeLocalAccountsKept": "Las cuentas administradas en este escritorio no cambian. Vuelve a cambiar el runtime predeterminado a Escritorio local para verlas.",
"remoteScopeAuthContext": "Cada cuenta mantiene su propio contexto de inicio de sesión en {{value0}}.",
"remoteEmptyClaudeAccounts": "No hay cuentas de Claude administradas en {{value0}}. Usa su inicio de sesión de Claude predeterminado del sistema; agrega cuentas en ese servidor.",
"remoteEmptyCodexAccounts": "No hay cuentas de Codex administradas en {{value0}}. Usa su inicio de sesión de Codex predeterminado del sistema; agrega cuentas en ese servidor.",

View File

@ -5046,6 +5046,9 @@
"remoteServerFallback": "リモートサーバー",
"loadAccountsFailed": "プロバイダーアカウントを読み込めませんでした。",
"remoteScopeAccounts": "{{value0}} が管理するアカウントを表示しています。アカウントの追加や再認証はそのサーバー上で行ってください。",
"accountScopePrefix": "アカウントのスコープ",
"accountScopeRemoteServerUnnamed": "リモートサーバー",
"remoteScopeLocalAccountsKept": "このデスクトップで管理されているアカウントはそのまま残っています。デフォルトランタイムをローカルデスクトップに戻すと表示されます。",
"remoteScopeAuthContext": "各アカウントは {{value0}} 上に独自のサインインコンテキストを保持します。",
"remoteEmptyClaudeAccounts": "{{value0}} に管理対象の Claude アカウントはありません。システム既定の Claude ログインを使用します。アカウントの追加はそのサーバー上で行ってください。",
"remoteEmptyCodexAccounts": "{{value0}} に管理対象の Codex アカウントはありません。システム既定の Codex ログインを使用します。アカウントの追加はそのサーバー上で行ってください。",

View File

@ -5046,6 +5046,9 @@
"remoteServerFallback": "원격 서버",
"loadAccountsFailed": "프로바이더 계정을 불러오지 못했습니다.",
"remoteScopeAccounts": "{{value0}}에서 관리하는 계정을 표시하고 있습니다. 계정 추가나 재인증은 해당 서버에서 진행하세요.",
"accountScopePrefix": "계정 범위",
"accountScopeRemoteServerUnnamed": "원격 서버",
"remoteScopeLocalAccountsKept": "이 데스크탑에서 관리되는 계정은 그대로 유지됩니다. 기본 런타임을 로컬 데스크탑으로 다시 전환하면 확인할 수 있습니다.",
"remoteScopeAuthContext": "각 계정은 {{value0}}에 자체 로그인 컨텍스트를 유지합니다.",
"remoteEmptyClaudeAccounts": "{{value0}}에 관리되는 Claude 계정이 없습니다. 해당 서버의 시스템 기본 Claude 로그인을 사용하며, 계정 추가는 그 서버에서 진행하세요.",
"remoteEmptyCodexAccounts": "{{value0}}에 관리되는 Codex 계정이 없습니다. 해당 서버의 시스템 기본 Codex 로그인을 사용하며, 계정 추가는 그 서버에서 진행하세요.",

View File

@ -5046,6 +5046,9 @@
"remoteServerFallback": "远程服务器",
"loadAccountsFailed": "无法加载提供商账户。",
"remoteScopeAccounts": "正在显示由 {{value0}} 管理的账户。请在该服务器上添加或重新验证账户。",
"accountScopePrefix": "账户范围",
"accountScopeRemoteServerUnnamed": "远程服务器",
"remoteScopeLocalAccountsKept": "此桌面端管理的账户并未改变。将默认运行时切换回本地桌面即可查看。",
"remoteScopeAuthContext": "每个账户在 {{value0}} 上保留自己的登录上下文。",
"remoteEmptyClaudeAccounts": "{{value0}} 上没有受管理的 Claude 账户。它使用其系统默认的 Claude 登录;请在该服务器上添加账户。",
"remoteEmptyCodexAccounts": "{{value0}} 上没有受管理的 Codex 账户。它使用其系统默认的 Codex 登录;请在该服务器上添加账户。",