fix(accounts): follow the runtime for WSL provider account detection (#9537) (#9611)

* fix(accounts): follow the runtime for WSL provider account detection (#9537)

On Windows + WSL, provider-account detection (usage recognition and the
status-bar account switcher) was pinned to the Windows host even when the
project runs in WSL, so WSL accounts were never recognized and the WSL
switcher group never appeared.

Root cause: `localAccountRuntime` hard-defaulted to 'host', which
short-circuited `getInitialClaude/CodexRateLimitTarget` before the existing
"follow the global Windows runtime default" branch could run. That branch was
therefore dead for every real user.

Fix: add an 'auto' value for `localAccountRuntime`, make it the default, and
migrate the untouched legacy 'host' default to 'auto' once (guarded by
`localAccountRuntimeDefaultedToAutoForAllUsers`; explicit 'wsl' is preserved).
'auto' resolves via a shared `resolveLocalAccountRuntimeTarget` helper: on a
windows-host default it stays host (no behavior change); on a WSL default it
follows WSL, so WSL accounts are recognized and the WSL group appears.

Wired the shared resolver into the managed-account default target, the
status-bar WSL-group gate, and the Accounts settings location toggle.

Note: detection follows the global Windows runtime default, not the live
active project's runtime (the fetch target is a single global value); the
latter is a larger follow-up.

* fix(accounts): align auto runtime consumers

* fix(accounts): keep runtime polling aligned with settings

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
OrcaWin 2026-07-20 21:35:14 -04:00 committed by GitHub
parent 0e7b0ac220
commit 6444be3a01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 648 additions and 15 deletions

View File

@ -3537,6 +3537,72 @@ describe('ClaudeRuntimeAuthService', () => {
})
})
it('uses the global WSL runtime for untargeted Claude preparation under auto', async () => {
setPlatform('win32')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => null,
toWindowsWslPath: (value: string) => value
}))
const ubuntuAuthPath = createManagedClaudeAuth(
testState.userDataDir,
'ubuntu-account',
createClaudeCredentialsJson('ubuntu@example.com', 'ubuntu-token')
)
const settings = createSettings({
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' },
claudeManagedAccounts: [
createClaudeAccount('ubuntu-account', ubuntuAuthPath, {
managedAuthRuntime: 'wsl',
wslDistro: 'Ubuntu',
wslLinuxAuthPath: '/home/alice/.local/share/orca/claude-accounts/ubuntu/auth'
})
],
activeClaudeManagedAccountId: null,
activeClaudeManagedAccountIdsByRuntime: {
host: null,
wsl: { Ubuntu: 'ubuntu-account' }
}
})
const store = createStore(settings)
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
const service = new ClaudeRuntimeAuthService(store as never)
const preparation = await service.prepareForClaudeLaunch()
expect(preparation).toMatchObject({
runtime: 'wsl',
wslDistro: 'Ubuntu',
provenance: 'managed:ubuntu-account:wsl:Ubuntu'
})
})
it('ignores a persisted WSL account-runtime pin on non-Windows hosts', async () => {
setPlatform('darwin')
vi.doMock('../wsl', () => ({
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => null,
toWindowsWslPath: (value: string) => value
}))
const settings = createSettings({
localAccountRuntime: 'wsl',
localAccountWslDistro: 'Ubuntu'
})
const store = createStore(settings)
const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service')
const service = new ClaudeRuntimeAuthService(store as never)
const preparation = await service.prepareForClaudeLaunch()
expect(preparation).toMatchObject({
runtime: 'host',
wslDistro: null,
provenance: 'system',
stripAuthEnv: false
})
})
it('keeps untargeted Claude preparation on host when account runtime is host', async () => {
setPlatform('win32')
vi.doMock('../wsl', () => ({

View File

@ -13,6 +13,7 @@ import {
writeClaudeManagedAuthFile
} from './managed-auth-path'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { resolveLocalAccountRuntimeTarget } from '../../shared/local-account-runtime'
import { getDefaultWslDistro, getWslHome, toWindowsWslPath } from '../wsl'
import { buildEncodedWslBashCommand } from '../wsl-bash-command'
import { hasLiveClaudePtys } from './live-pty-gate'
@ -683,9 +684,10 @@ export class ClaudeRuntimeAuthService {
private getDefaultAccountSelectionTarget(
settings = this.store.getSettings()
): ClaudeAccountSelectionTarget {
if (process.platform === 'win32' && settings.localAccountRuntime === 'wsl') {
// Why: auth defaults follow account runtime settings, not legacy terminal WSL settings that can outlive the Terminal UI.
return { runtime: 'wsl', wslDistro: settings.localAccountWslDistro ?? null }
// Why: Windows auth follows the resolved account runtime; stale cross-platform WSL pins must stay local-host.
const resolved = resolveLocalAccountRuntimeTarget(settings)
if (process.platform === 'win32' && resolved.runtime === 'wsl') {
return { runtime: 'wsl', wslDistro: resolved.wslDistro }
}
return { runtime: 'host' }
}

View File

@ -113,6 +113,7 @@ import { RateLimitService } from './rate-limits/service'
import { readMiniMaxSessionCookie } from './minimax/minimax-cookie-store'
import { getInitialClaudeRateLimitTarget } from './rate-limits/claude-rate-limit-target'
import { getInitialCodexRateLimitTarget } from './rate-limits/codex-rate-limit-target'
import { createAccountRuntimeTargetSettingsSync } from './rate-limits/account-runtime-target-sync'
import {
attachMainWindowServices,
ensureAutoUpdaterConfigured
@ -1820,6 +1821,16 @@ app.whenReady().then(async () => {
)
rateLimits.setCodexFetchTarget(getInitialCodexRateLimitTarget(store.getSettings()))
rateLimits.setClaudeFetchTarget(getInitialClaudeRateLimitTarget(store.getSettings()))
const syncAccountRuntimeTargets = createAccountRuntimeTargetSettingsSync(
rateLimits,
store.getSettings()
)
store.onSettingsChanged((updates, settings) => {
// Why: auto is a live policy; retarget only providers whose settings-derived runtime changed.
void syncAccountRuntimeTargets(updates, settings).catch((error) =>
console.warn('[rate-limits] Failed to apply account runtime target:', error)
)
})
rateLimits.setClaudeAuthPreparationResolver((target) =>
claudeRuntimeAuth!.prepareForRateLimitFetch(target)
)

View File

@ -549,6 +549,59 @@ describe('Store', () => {
})
})
it('migrates the legacy host account-runtime default to auto once', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {
localAccountRuntime: 'host'
}
})
const store = await createStore()
expect(store.getSettings().localAccountRuntime).toBe('auto')
expect(store.getSettings().localAccountRuntimeDefaultedToAutoForAllUsers).toBe(true)
store.flush()
const persisted = (readDataFile() as PersistedState).settings
expect(persisted.localAccountRuntime).toBe('auto')
expect(persisted.localAccountRuntimeDefaultedToAutoForAllUsers).toBe(true)
})
it('preserves an explicit WSL account-runtime pin through the migration', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {
localAccountRuntime: 'wsl',
localAccountWslDistro: 'Ubuntu'
}
})
const store = await createStore()
expect(store.getSettings().localAccountRuntime).toBe('wsl')
expect(store.getSettings().localAccountRuntimeDefaultedToAutoForAllUsers).toBe(true)
})
it('does not re-flip an explicit host pin chosen after migration', async () => {
writeDataFile({
schemaVersion: 1,
repos: [],
worktreeMeta: {},
settings: {
localAccountRuntime: 'host',
localAccountRuntimeDefaultedToAutoForAllUsers: true
}
})
const store = await createStore()
expect(store.getSettings().localAccountRuntime).toBe('host')
})
it('returns default settings when no data file exists', async () => {
const store = await createStore()
const settings = store.getSettings()
@ -565,6 +618,8 @@ describe('Store', () => {
expect(settings.terminalScrollSensitivity).toBe(1.15)
expect(settings.terminalFastScrollSensitivity).toBe(5)
expect(settings.terminalTuiScrollSensitivity).toBe(1)
expect(settings.localAccountRuntime).toBe('auto')
expect(settings.localAccountRuntimeDefaultedToAutoForAllUsers).toBe(true)
expect(settings.terminalTuiScrollSensitivityDefaultedToOne).toBe(true)
expect(settings.terminalUseSeparateLightTheme).toBe(true)
expect(settings.rightSidebarOpenByDefault).toBe(true)

View File

@ -2960,6 +2960,18 @@ export class Store {
) {
this.loadNeedsSave = true
}
// Why (#9537): migrate the indistinguishable legacy host default once so WSL-default users follow their runtime.
const localAccountRuntimeAlreadyMigrated =
parsed.settings?.localAccountRuntimeDefaultedToAutoForAllUsers === true
const migratedLocalAccountRuntime: GlobalSettings['localAccountRuntime'] =
localAccountRuntimeAlreadyMigrated
? (parsed.settings?.localAccountRuntime ?? defaults.settings.localAccountRuntime)
: parsed.settings?.localAccountRuntime === 'wsl'
? 'wsl'
: 'auto'
if (!localAccountRuntimeAlreadyMigrated) {
this.loadNeedsSave = true
}
if (!autoRenameBranchFromWorkDefaultedOn) {
this.loadNeedsSave = true
}
@ -3039,6 +3051,8 @@ export class Store {
terminalMacOptionAsAlt: migratedOptionAsAlt,
terminalMacOptionAsAltMigrated: true,
localWindowsRuntimeDefault: migratedWindowsRuntimeDefault,
localAccountRuntime: migratedLocalAccountRuntime,
localAccountRuntimeDefaultedToAutoForAllUsers: true,
floatingTerminalEnabled: migratedFloatingTerminalEnabled,
floatingTerminalDefaultedForAllUsers: true,
floatingTerminalCwd: migratedFloatingTerminalCwd,

View File

@ -0,0 +1,108 @@
import { describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../shared/constants'
import type { RateLimitState } from '../../shared/rate-limit-types'
import { createAccountRuntimeTargetSettingsSync } from './account-runtime-target-sync'
function createServiceTargets(
claudeTarget: RateLimitState['claudeTarget'],
codexTarget: RateLimitState['codexTarget']
) {
const state = { claudeTarget, codexTarget } as RateLimitState
return {
getState: vi.fn(() => state),
refreshClaudeForTarget: vi.fn(async () => state),
refreshCodexForTarget: vi.fn(async () => state)
}
}
describe('createAccountRuntimeTargetSettingsSync', () => {
it('retargets only Claude and Codex when auto changes to WSL', async () => {
const service = createServiceTargets(
{ runtime: 'host', wslDistro: null },
{ runtime: 'host', wslDistro: null }
)
const settings = {
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto' as const,
localWindowsRuntimeDefault: { kind: 'wsl' as const, distro: 'Ubuntu' }
}
const syncSettings = createAccountRuntimeTargetSettingsSync(
service,
getDefaultSettings('/tmp'),
'win32'
)
await syncSettings(
{ localWindowsRuntimeDefault: settings.localWindowsRuntimeDefault },
settings
)
const expectedTarget = { runtime: 'wsl', wslDistro: 'Ubuntu' }
expect(service.refreshClaudeForTarget).toHaveBeenCalledOnce()
expect(service.refreshClaudeForTarget).toHaveBeenCalledWith(expectedTarget)
expect(service.refreshCodexForTarget).toHaveBeenCalledOnce()
expect(service.refreshCodexForTarget).toHaveBeenCalledWith(expectedTarget)
})
it('does no work for unrelated settings updates', async () => {
const service = createServiceTargets(
{ runtime: 'host', wslDistro: null },
{ runtime: 'host', wslDistro: null }
)
const settings = getDefaultSettings('/tmp')
const syncSettings = createAccountRuntimeTargetSettingsSync(service, settings, 'win32')
await syncSettings({ theme: 'dark' }, settings)
expect(service.getState).not.toHaveBeenCalled()
expect(service.refreshClaudeForTarget).not.toHaveBeenCalled()
expect(service.refreshCodexForTarget).not.toHaveBeenCalled()
})
it('preserves a manual runtime when the settings-derived policy does not change', async () => {
const service = createServiceTargets(
{ runtime: 'wsl', wslDistro: 'Ubuntu' },
{ runtime: 'wsl', wslDistro: 'Ubuntu' }
)
const initialSettings = {
...getDefaultSettings('/tmp'),
localAccountRuntime: 'host' as const
}
const settings = {
...initialSettings,
localWindowsRuntimeDefault: { kind: 'wsl' as const, distro: 'Ubuntu' }
}
const syncSettings = createAccountRuntimeTargetSettingsSync(service, initialSettings, 'win32')
await syncSettings(
{ localWindowsRuntimeDefault: settings.localWindowsRuntimeDefault },
settings
)
expect(service.getState).not.toHaveBeenCalled()
expect(service.refreshClaudeForTarget).not.toHaveBeenCalled()
expect(service.refreshCodexForTarget).not.toHaveBeenCalled()
})
it('refreshes only the provider whose current target differs', async () => {
const service = createServiceTargets(
{ runtime: 'host', wslDistro: null },
{ runtime: 'wsl', wslDistro: 'Ubuntu' }
)
const initialSettings = {
...getDefaultSettings('/tmp'),
localWindowsRuntimeDefault: { kind: 'wsl' as const, distro: 'Ubuntu' }
}
const settings = getDefaultSettings('/tmp')
const syncSettings = createAccountRuntimeTargetSettingsSync(service, initialSettings, 'win32')
await syncSettings(
{ localWindowsRuntimeDefault: settings.localWindowsRuntimeDefault },
settings
)
expect(service.refreshClaudeForTarget).not.toHaveBeenCalled()
expect(service.refreshCodexForTarget).toHaveBeenCalledOnce()
expect(service.refreshCodexForTarget).toHaveBeenCalledWith({ runtime: 'host' })
})
})

View File

@ -0,0 +1,70 @@
import type { GlobalSettings } from '../../shared/types'
import type { RateLimitState } from '../../shared/rate-limit-types'
import type { RateLimitService } from './service'
import { getInitialClaudeRateLimitTarget } from './claude-rate-limit-target'
import { getInitialCodexRateLimitTarget } from './codex-rate-limit-target'
type AccountRuntimeRateLimitService = Pick<
RateLimitService,
'getState' | 'refreshClaudeForTarget' | 'refreshCodexForTarget'
>
type RuntimeTarget = {
runtime?: 'host' | 'wsl'
wslDistro?: string | null
}
export function createAccountRuntimeTargetSettingsSync(
rateLimits: AccountRuntimeRateLimitService,
initialSettings: GlobalSettings,
platform: NodeJS.Platform = process.platform
): (updates: Partial<GlobalSettings>, settings: GlobalSettings) => Promise<void> {
let settingsTargets = getSettingsTargets(initialSettings, platform)
return async (updates, settings): Promise<void> => {
if (!containsAccountRuntimeTargetUpdate(updates)) {
return
}
const nextSettingsTargets = getSettingsTargets(settings, platform)
const claudePolicyChanged = !isSameTarget(settingsTargets.claude, nextSettingsTargets.claude)
const codexPolicyChanged = !isSameTarget(settingsTargets.codex, nextSettingsTargets.codex)
settingsTargets = nextSettingsTargets
if (!claudePolicyChanged && !codexPolicyChanged) {
return
}
const current = rateLimits.getState()
const refreshes: Promise<RateLimitState>[] = []
if (claudePolicyChanged && !isSameTarget(current.claudeTarget, nextSettingsTargets.claude)) {
refreshes.push(rateLimits.refreshClaudeForTarget(nextSettingsTargets.claude))
}
if (codexPolicyChanged && !isSameTarget(current.codexTarget, nextSettingsTargets.codex)) {
refreshes.push(rateLimits.refreshCodexForTarget(nextSettingsTargets.codex))
}
await Promise.all(refreshes)
}
}
function getSettingsTargets(settings: GlobalSettings, platform: NodeJS.Platform) {
return {
claude: getInitialClaudeRateLimitTarget(settings, platform),
codex: getInitialCodexRateLimitTarget(settings, platform)
}
}
function containsAccountRuntimeTargetUpdate(updates: Partial<GlobalSettings>): boolean {
return (
'localAccountRuntime' in updates ||
'localAccountWslDistro' in updates ||
'localWindowsRuntimeDefault' in updates
)
}
function isSameTarget(current: RuntimeTarget, next: RuntimeTarget): boolean {
return (
(current.runtime ?? 'host') === (next.runtime ?? 'host') &&
(current.wslDistro ?? null) === (next.wslDistro ?? null)
)
}

View File

@ -103,6 +103,49 @@ describe('getInitialClaudeRateLimitTarget', () => {
).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' })
})
it('auto follows the global WSL project runtime default', () => {
expect(
getInitialClaudeRateLimitTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
},
'win32'
)
).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' })
})
it('auto resolves to host when the global project runtime is windows-host', () => {
expect(
getInitialClaudeRateLimitTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'windows-host' }
},
'win32'
)
).toEqual({ runtime: 'host' })
})
it('does not let a stale WSL-only selection override an auto host target', () => {
expect(
getInitialClaudeRateLimitTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'windows-host' },
activeClaudeManagedAccountIdsByRuntime: {
host: null,
wsl: { Ubuntu: 'wsl-account-1' }
}
},
'win32'
)
).toEqual({ runtime: 'host' })
})
it('keeps explicit host runtime on host', () => {
expect(
getInitialClaudeRateLimitTarget(
@ -120,4 +163,17 @@ describe('getInitialClaudeRateLimitTarget', () => {
)
).toEqual({ runtime: 'host' })
})
it('ignores a stale explicit WSL runtime on non-Windows hosts', () => {
expect(
getInitialClaudeRateLimitTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'wsl',
localAccountWslDistro: 'Ubuntu'
},
'linux'
)
).toEqual({ runtime: 'host' })
})
})

View File

@ -1,4 +1,5 @@
import type { GlobalSettings } from '../../shared/types'
import { resolveLocalAccountRuntimeTarget } from '../../shared/local-account-runtime'
import {
getClaudeWslSelectionKey,
normalizeClaudeRuntimeSelection,
@ -29,6 +30,9 @@ export function getInitialClaudeRateLimitTarget(
return { runtime: 'host' }
}
if (settings.localAccountRuntime === 'wsl') {
if (platform !== 'win32') {
return { runtime: 'host' }
}
return {
runtime: 'wsl',
wslDistro:
@ -36,7 +40,14 @@ export function getInitialClaudeRateLimitTarget(
getSingleSelectedWslDistro(settings)
}
}
if (settings.localAccountRuntime === 'auto') {
const target = resolveLocalAccountRuntimeTarget(settings, platform)
return target.runtime === 'wsl'
? { runtime: 'wsl', wslDistro: target.wslDistro }
: { runtime: 'host' }
}
// Why: pre-setting profiles used account selection as their startup fallback.
const projectRuntimeTarget = getProjectRuntimeRateLimitTarget(settings, platform)
if (projectRuntimeTarget) {
return projectRuntimeTarget

View File

@ -103,6 +103,49 @@ describe('getInitialCodexRateLimitTarget', () => {
).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' })
})
it('auto follows the global WSL project runtime default', () => {
expect(
getInitialCodexRateLimitTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
},
'win32'
)
).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' })
})
it('auto resolves to host when the global project runtime is windows-host', () => {
expect(
getInitialCodexRateLimitTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'windows-host' }
},
'win32'
)
).toEqual({ runtime: 'host' })
})
it('does not let a stale WSL-only selection override an auto host target', () => {
expect(
getInitialCodexRateLimitTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'windows-host' },
activeCodexManagedAccountIdsByRuntime: {
host: null,
wsl: { Ubuntu: 'wsl-account-1' }
}
},
'win32'
)
).toEqual({ runtime: 'host' })
})
it('keeps explicit host runtime on host', () => {
expect(
getInitialCodexRateLimitTarget(
@ -120,4 +163,17 @@ describe('getInitialCodexRateLimitTarget', () => {
)
).toEqual({ runtime: 'host' })
})
it('ignores a stale explicit WSL runtime on non-Windows hosts', () => {
expect(
getInitialCodexRateLimitTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'wsl',
localAccountWslDistro: 'Ubuntu'
},
'darwin'
)
).toEqual({ runtime: 'host' })
})
})

View File

@ -1,4 +1,5 @@
import type { GlobalSettings } from '../../shared/types'
import { resolveLocalAccountRuntimeTarget } from '../../shared/local-account-runtime'
import {
getWslSelectionKey,
normalizeCodexRuntimeSelection,
@ -29,6 +30,9 @@ export function getInitialCodexRateLimitTarget(
return { runtime: 'host' }
}
if (settings.localAccountRuntime === 'wsl') {
if (platform !== 'win32') {
return { runtime: 'host' }
}
return {
runtime: 'wsl',
wslDistro:
@ -36,7 +40,14 @@ export function getInitialCodexRateLimitTarget(
getSingleSelectedWslDistro(settings)
}
}
if (settings.localAccountRuntime === 'auto') {
const target = resolveLocalAccountRuntimeTarget(settings, platform)
return target.runtime === 'wsl'
? { runtime: 'wsl', wslDistro: target.wslDistro }
: { runtime: 'host' }
}
// Why: pre-setting profiles used account selection as their startup fallback.
const projectRuntimeTarget = getProjectRuntimeRateLimitTarget(settings, platform)
if (projectRuntimeTarget) {
return projectRuntimeTarget

View File

@ -51,6 +51,39 @@ describe('AccountsPane', () => {
expect(markup).toContain('role="radio" aria-checked="true" disabled=""')
})
it('selects the WSL account location under auto when the global project runtime is WSL', () => {
// Why: navigator.userAgent is a read-only prototype getter, so shadow it with
// a configurable own property and remove that shadow afterward to restore it.
const originalOwnUserAgent = Object.getOwnPropertyDescriptor(
globalThis.navigator,
'userAgent'
)
Object.defineProperty(globalThis.navigator, 'userAgent', {
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
configurable: true
})
try {
const markup = renderPane(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
},
{ wslSupportedPlatform: true, wslCapabilitiesLoading: true }
)
expect(markup).toContain('aria-label="Account location"')
// The resolved WSL radio is the checked option (disabled while capabilities load).
expect(markup).toContain('role="radio" aria-checked="true" disabled=""')
} finally {
if (originalOwnUserAgent) {
Object.defineProperty(globalThis.navigator, 'userAgent', originalOwnUserAgent)
} else {
delete (globalThis.navigator as { userAgent?: string }).userAgent
}
}
})
it('keeps the runtime label inside the localized account copy', () => {
const markup = renderPane(getDefaultSettings('/tmp'))

View File

@ -10,6 +10,8 @@ import type {
CodexSystemDefaultIdentity,
GlobalSettings
} from '../../../../shared/types'
import { resolveLocalAccountRuntimeTarget } from '../../../../shared/local-account-runtime'
import { getRendererAppPlatform } from '../../lib/renderer-app-platform'
import { Badge } from '../ui/badge'
import { Button } from '../ui/button'
import { Input } from '../ui/input'
@ -290,14 +292,16 @@ function getSelectedAccountRuntime(
wslDistros: string[],
wslCapabilitiesLoading: boolean
): LocalAccountRuntime {
if (wslSupportedPlatform && settings.localAccountRuntime === 'wsl') {
// Why: the two-option control displays the concrete target behind the persisted auto policy.
const resolvedRuntime = resolveLocalAccountRuntimeTarget(settings, getRendererAppPlatform())
if (wslSupportedPlatform && resolvedRuntime.runtime === 'wsl') {
if (!wslAvailable && !wslCapabilitiesLoading) {
return {
runtime: 'wsl',
label: translate('auto.components.settings.AccountsPane.8619f9afa9', 'WSL')
}
}
const configuredDistro = settings.localAccountWslDistro?.trim() || null
const configuredDistro = resolvedRuntime.wslDistro?.trim() || null
const selectedDistro =
configuredDistro && (wslCapabilitiesLoading || wslDistros.includes(configuredDistro))
? configuredDistro

View File

@ -48,6 +48,8 @@ import type {
RateLimitRuntimeTarget,
RateLimitWindow
} from '../../../../shared/rate-limit-types'
import { resolveLocalAccountRuntimeTarget } from '../../../../shared/local-account-runtime'
import { getRendererAppPlatform } from '../../lib/renderer-app-platform'
import {
ProviderIcon,
ProviderPanel,
@ -208,17 +210,24 @@ function toCodexStatusRuntimeTarget(
export function getStatusBarPreferredWslDistro(
settings: GlobalSettings | null | undefined,
wslDistros: string[]
wslDistros: string[],
platform: NodeJS.Platform = getRendererAppPlatform()
): string | null {
const configuredDistro = settings?.localAccountWslDistro?.trim() || null
if (configuredDistro) {
return configuredDistro
if (settings) {
const target = resolveLocalAccountRuntimeTarget(settings, platform)
if (target.runtime === 'wsl' && target.wslDistro) {
return target.wslDistro
}
}
return wslDistros.length === 1 ? wslDistros[0] : null
}
function shouldIncludeSettingsWslRuntime(settings: GlobalSettings | null | undefined): boolean {
return settings?.localAccountRuntime === 'wsl'
if (!settings) {
return false
}
// Why: the fallback group must match the concrete runtime used for account polling.
return resolveLocalAccountRuntimeTarget(settings, getRendererAppPlatform()).runtime === 'wsl'
}
function getSingleConcreteCodexWslDistro(state: CodexRateLimitAccountsState): string | null {

View File

@ -116,10 +116,12 @@ describe('status bar runtime switch groups', () => {
expect(
getStatusBarPreferredWslDistro(
{
localAccountRuntime: 'wsl',
localAccountWslDistro: null,
terminalWindowsWslDistro: 'Debian'
} as GlobalSettings,
['Ubuntu']
['Ubuntu'],
'win32'
)
).toBe('Ubuntu')
})
@ -128,14 +130,30 @@ describe('status bar runtime switch groups', () => {
expect(
getStatusBarPreferredWslDistro(
{
localAccountRuntime: 'wsl',
localAccountWslDistro: 'Fedora',
terminalWindowsWslDistro: 'Debian'
} as GlobalSettings,
['Ubuntu']
['Ubuntu'],
'win32'
)
).toBe('Fedora')
})
it('uses the auto runtime distro instead of a stale account-runtime distro', () => {
expect(
getStatusBarPreferredWslDistro(
{
localAccountRuntime: 'auto',
localAccountWslDistro: 'Fedora',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
} as GlobalSettings,
['Fedora', 'Ubuntu'],
'win32'
)
).toBe('Ubuntu')
})
it('labels the host account group with the active remote server name', () => {
const state: CodexRateLimitAccountsState = {
accounts: [],

View File

@ -229,7 +229,8 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
terminalRightClickToPasteDefaultedForPlatform: true,
terminalWindowsShell: 'powershell.exe',
terminalWindowsWslDistro: null,
localAccountRuntime: 'host',
localAccountRuntime: 'auto',
localAccountRuntimeDefaultedToAutoForAllUsers: true,
localAccountWslDistro: null,
localWindowsRuntimeDefault: { kind: 'windows-host' },
// Why: prefer modern PowerShell when installed, falling back to inbox Windows PowerShell.

View File

@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from './constants'
import { resolveLocalAccountRuntimeTarget } from './local-account-runtime'
describe('resolveLocalAccountRuntimeTarget', () => {
it('honors an explicit host pin', () => {
expect(
resolveLocalAccountRuntimeTarget(
{ ...getDefaultSettings('/tmp'), localAccountRuntime: 'host' },
'win32'
)
).toEqual({ runtime: 'host', wslDistro: null })
})
it('honors an explicit WSL pin and its distro', () => {
expect(
resolveLocalAccountRuntimeTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'wsl',
localAccountWslDistro: 'Ubuntu'
},
'win32'
)
).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' })
})
it('auto follows the global WSL project runtime default', () => {
expect(
resolveLocalAccountRuntimeTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
},
'win32'
)
).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' })
})
it('auto resolves to host when the global project runtime is windows-host', () => {
expect(
resolveLocalAccountRuntimeTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'windows-host' }
},
'win32'
)
).toEqual({ runtime: 'host', wslDistro: null })
})
it('auto resolves to host on non-Windows platforms even with a WSL default', () => {
expect(
resolveLocalAccountRuntimeTarget(
{
...getDefaultSettings('/tmp'),
localAccountRuntime: 'auto',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
},
'linux'
)
).toEqual({ runtime: 'host', wslDistro: null })
})
})

View File

@ -0,0 +1,40 @@
import type { GlobalSettings } from './types'
import { normalizeGlobalWindowsRuntimeDefault } from './project-execution-runtime'
export type LocalAccountRuntimeTarget = {
runtime: 'host' | 'wsl'
wslDistro: string | null
}
type LocalAccountRuntimeSettings = Pick<
GlobalSettings,
'localAccountRuntime' | 'localAccountWslDistro' | 'localWindowsRuntimeDefault'
>
/** Resolves the persisted account policy to a concrete host or WSL target. */
export function resolveLocalAccountRuntimeTarget(
settings: LocalAccountRuntimeSettings,
platform: NodeJS.Platform = process.platform
): LocalAccountRuntimeTarget {
if (settings.localAccountRuntime === 'host') {
return { runtime: 'host', wslDistro: null }
}
if (settings.localAccountRuntime === 'wsl') {
return { runtime: 'wsl', wslDistro: normalizeDistro(settings.localAccountWslDistro) }
}
// 'auto' (or any unset legacy value): follow the global Windows runtime default.
if (platform !== 'win32') {
return { runtime: 'host', wslDistro: null }
}
const runtimeDefault = normalizeGlobalWindowsRuntimeDefault(settings.localWindowsRuntimeDefault)
if (runtimeDefault.kind === 'wsl') {
return { runtime: 'wsl', wslDistro: runtimeDefault.distro }
}
return { runtime: 'host', wslDistro: null }
}
function normalizeDistro(value: string | null | undefined): string | null {
const trimmed = value?.trim()
return trimmed ? trimmed : null
}

View File

@ -2672,9 +2672,11 @@ export type GlobalSettings = {
terminalWindowsShell: string
/** Pins the WSL distro for terminals/agent scans instead of WSL's current global default. */
terminalWindowsWslDistro?: string | null
/** Account/auth location independent from the terminal shell (e.g. WSL terminals but Windows-scoped accounts). */
localAccountRuntime: 'host' | 'wsl'
/** Account/auth location; auto follows the global Windows runtime while host/wsl pin it. */
localAccountRuntime: 'auto' | 'host' | 'wsl'
localAccountWslDistro?: string | null
/** One-shot guard for migrating the legacy host default to auto. */
localAccountRuntimeDefaultedToAutoForAllUsers?: boolean
/** Independent from the terminal shell so users can inspect Windows vs WSL agent PATH state without changing it. */
localAgentRuntime?: 'host' | 'wsl'
localAgentWslDistro?: string | null