* 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:
parent
0e7b0ac220
commit
6444be3a01
|
|
@ -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', () => ({
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
@ -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' })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue