Fix WSL agent runtime selection (#6394)

This commit is contained in:
Jinwoo Hong 2026-06-25 17:43:51 -07:00 committed by GitHub
parent b651b4be6d
commit 4cb25abe24
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 483 additions and 40 deletions

View File

@ -9,6 +9,12 @@ import {
import { AgentsPane } from '@/components/settings/AgentsPane'
import { useAppStore } from '@/store'
import { translate } from '@/i18n/i18n'
import {
getWindowsTerminalCapabilityOwnerKey,
useWindowsTerminalCapabilities
} from '@/lib/windows-terminal-capabilities'
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { isWebClientLocation } from '@/lib/web-client-location'
type AgentSettingsDialogProps = {
open: boolean
@ -21,6 +27,20 @@ export default function AgentSettingsDialog({
}: AgentSettingsDialogProps): React.JSX.Element | null {
const settings = useAppStore((s) => s.settings)
const updateSettings = useAppStore((s) => s.updateSettings)
const runtimeTarget = getActiveRuntimeTarget(settings)
const runtimeEnvironmentId = settings?.activeRuntimeEnvironmentId?.trim() || null
const capabilitiesOwnerKey = getWindowsTerminalCapabilityOwnerKey(runtimeEnvironmentId)
const isWindowsRenderer =
typeof navigator !== 'undefined' && navigator.userAgent.includes('Windows')
const isWebClient = isWebClientLocation()
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
open && (isWindowsRenderer || isWebClient || runtimeTarget.kind === 'environment'),
false,
capabilitiesOwnerKey,
runtimeTarget
)
const wslSupportedPlatform =
isWindowsRenderer || windowsTerminalCapabilities.hostPlatform === 'win32'
if (!settings) {
return null
@ -45,7 +65,14 @@ export default function AgentSettingsDialog({
</DialogDescription>
</DialogHeader>
<div className="scrollbar-sleek -mr-2 max-h-[70vh] overflow-y-auto pr-2">
<AgentsPane settings={settings} updateSettings={updateSettings} />
<AgentsPane
settings={settings}
updateSettings={updateSettings}
wslSupportedPlatform={wslSupportedPlatform}
wslAvailable={windowsTerminalCapabilities.wslAvailable}
wslDistros={windowsTerminalCapabilities.wslDistros}
wslCapabilitiesLoading={windowsTerminalCapabilities.isLoading}
/>
</div>
</DialogContent>
</Dialog>

View File

@ -0,0 +1,192 @@
import type { GlobalSettings } from '../../../../shared/types'
import type { GlobalWindowsRuntimeDefault } from '../../../../shared/project-execution-runtime'
import { normalizeGlobalWindowsRuntimeDefault } from '../../../../shared/project-execution-runtime'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
import { SettingsRow, SettingsSegmentedControl } from './SettingsFormControls'
import { translate } from '@/i18n/i18n'
type AgentRuntimeSegment = GlobalWindowsRuntimeDefault['kind']
type AgentRuntimeSettingProps = {
settings: Pick<GlobalSettings, 'localWindowsRuntimeDefault'>
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<unknown>
refresh: () => Promise<unknown>
wslSupportedPlatform?: boolean
wslAvailable?: boolean
wslDistros?: string[]
wslCapabilitiesLoading?: boolean
}
const EMPTY_WSL_DISTROS: string[] = []
const NO_DISTRO_VALUE = '__select_wsl_distro__'
function getHostRuntimeLabel(): string {
return typeof navigator !== 'undefined' && navigator.userAgent.includes('Windows')
? 'Windows'
: 'This device'
}
export function AgentRuntimeSetting({
settings,
updateSettings,
refresh,
wslSupportedPlatform = false,
wslAvailable = false,
wslDistros = EMPTY_WSL_DISTROS,
wslCapabilitiesLoading = false
}: AgentRuntimeSettingProps): React.JSX.Element | null {
if (!wslSupportedPlatform) {
return null
}
const runtimeDefault = normalizeGlobalWindowsRuntimeDefault(settings.localWindowsRuntimeDefault)
const nextWslDistro = getNextWslDistro(runtimeDefault, wslDistros)
const distroOptions = getVisibleDistroOptions(runtimeDefault, wslDistros)
const updateAgentRuntime = (updates: Partial<GlobalSettings>): void => {
void Promise.resolve(updateSettings(updates)).then(() => refresh())
}
const handleRuntimeChange = (value: AgentRuntimeSegment): void => {
if (value === 'windows-host') {
updateAgentRuntime({ localWindowsRuntimeDefault: { kind: 'windows-host' } })
return
}
if (nextWslDistro) {
updateAgentRuntime({
localWindowsRuntimeDefault: { kind: 'wsl', distro: nextWslDistro }
})
}
}
return (
<section className="space-y-3">
<SettingsRow
label={translate('auto.components.settings.AgentRuntimeSetting.label', 'Agent runtime')}
alignTop
description={getDescription(runtimeDefault, wslAvailable, wslCapabilitiesLoading)}
control={
<div className="flex w-52 flex-col items-stretch gap-2">
<SettingsSegmentedControl<AgentRuntimeSegment>
ariaLabel={translate(
'auto.components.settings.AgentRuntimeSetting.label',
'Agent runtime'
)}
value={runtimeDefault.kind}
onChange={handleRuntimeChange}
equalWidth
options={[
{
value: 'windows-host',
label: getHostRuntimeLabel()
},
{
value: 'wsl',
label: translate('auto.components.settings.AgentRuntimeSetting.wsl', 'WSL'),
disabled: wslCapabilitiesLoading || !wslAvailable || !nextWslDistro
}
]}
/>
{runtimeDefault.kind === 'wsl' ? (
<Select
value={runtimeDefault.distro ?? NO_DISTRO_VALUE}
onValueChange={(distro) => {
if (distro !== NO_DISTRO_VALUE) {
updateAgentRuntime({
localWindowsRuntimeDefault: { kind: 'wsl', distro }
})
}
}}
disabled={wslCapabilitiesLoading || !wslAvailable}
>
<SelectTrigger size="sm" className="w-full min-w-52">
<SelectValue
placeholder={
wslCapabilitiesLoading
? translate(
'auto.components.settings.AgentRuntimeSetting.loadingWsl',
'Loading WSL'
)
: translate(
'auto.components.settings.AgentRuntimeSetting.selectDistro',
'Select distro'
)
}
/>
</SelectTrigger>
<SelectContent>
{!runtimeDefault.distro ? (
<SelectItem value={NO_DISTRO_VALUE}>
{translate(
'auto.components.settings.AgentRuntimeSetting.selectDistro',
'Select distro'
)}
</SelectItem>
) : null}
{distroOptions.map((distro) => (
<SelectItem key={distro} value={distro}>
{distro}
</SelectItem>
))}
</SelectContent>
</Select>
) : null}
</div>
}
/>
</section>
)
}
function getNextWslDistro(
runtimeDefault: GlobalWindowsRuntimeDefault,
wslDistros: readonly string[]
): string | null {
if (runtimeDefault.kind === 'wsl' && runtimeDefault.distro?.trim()) {
return runtimeDefault.distro.trim()
}
return wslDistros.find((distro) => distro.trim().length > 0) ?? null
}
function getVisibleDistroOptions(
runtimeDefault: GlobalWindowsRuntimeDefault,
wslDistros: readonly string[]
): string[] {
const options = [...wslDistros]
if (
runtimeDefault.kind === 'wsl' &&
runtimeDefault.distro &&
!options.includes(runtimeDefault.distro)
) {
return [runtimeDefault.distro, ...options]
}
return options
}
function getDescription(
runtimeDefault: GlobalWindowsRuntimeDefault,
wslAvailable: boolean,
wslCapabilitiesLoading: boolean
): string {
if (runtimeDefault.kind === 'windows-host') {
return translate(
'auto.components.settings.AgentRuntimeSetting.windowsDescription',
'Detect and launch agents on Windows for projects that do not override their runtime.'
)
}
if (!wslAvailable && !wslCapabilitiesLoading) {
return translate(
'auto.components.settings.AgentRuntimeSetting.wslUnavailable',
'WSL is not available on this machine.'
)
}
if (!runtimeDefault.distro) {
return translate(
'auto.components.settings.AgentRuntimeSetting.distroRequired',
'Choose a WSL distro before projects can inherit WSL.'
)
}
return translate(
'auto.components.settings.AgentRuntimeSetting.wslDescription',
'Detect and launch agents in {{value0}} via WSL for projects that do not override their runtime.',
{ value0: runtimeDefault.distro }
)
}

View File

@ -11,6 +11,7 @@ import { getAgentGeneratedTabTitlesTitle } from './agent-generated-tab-title-cop
import { getAgentStatusHooksTitle } from './agent-status-hooks-copy'
import { getAgentAwakeDescription, getAgentAwakeTitle } from './agent-awake-copy'
import { AgentAwakeSetting } from './AgentAwakeSetting'
import { AgentRuntimeSetting } from './AgentRuntimeSetting'
import {
AgentAvailabilityControl,
AgentPermissionsSetting,
@ -91,6 +92,9 @@ function visit(node: unknown, cb: (node: ReactElementLike) => void): void {
if (element.props?.children) {
visit(element.props.children, cb)
}
if (element.props?.control) {
visit(element.props.control, cb)
}
}
function findSwitch(node: unknown, ariaLabel: string): ReactElementLike {
@ -123,6 +127,19 @@ function findSwitchRow(node: unknown, ariaLabel: string): ReactElementLike {
return found
}
function findSegmentedControl(node: unknown, ariaLabel: string): ReactElementLike {
let found: ReactElementLike | null = null
visit(node, (entry) => {
if (entry.props.ariaLabel === ariaLabel && typeof entry.props.onChange === 'function') {
found = entry
}
})
if (!found) {
throw new Error('segmented control not found')
}
return found
}
describe('AgentsPane', () => {
beforeEach(() => {
detectedAgentsMock.detectedIds = ['claude']
@ -139,7 +156,8 @@ describe('AgentsPane', () => {
const markup = renderPane(getDefaultSettings('/tmp'))
expect(markup).not.toContain('Agent location')
expect(markup).not.toContain('aria-label="Agent location"')
expect(markup).not.toContain('Agent runtime')
expect(markup).not.toContain('aria-label="Agent runtime"')
expect(markup).toContain('Keep computer awake while agents are working')
expect(markup).toContain(
'Keeps this computer and display awake while agents are working. Orca also asks this device to stay awake when the lid is closed, subject to its power policy.'
@ -147,18 +165,19 @@ describe('AgentsPane', () => {
expect(markup).toContain('aria-checked="false"')
})
it('does not render the legacy agent location control on Windows', () => {
it('renders the agent runtime control on Windows-class hosts', () => {
const markup = renderPane(
{
...getDefaultSettings('/tmp'),
terminalWindowsShell: 'wsl.exe'
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
},
{ wslSupportedPlatform: true, wslCapabilitiesLoading: true }
{ wslSupportedPlatform: true, wslAvailable: true, wslDistros: ['Ubuntu'] }
)
expect(markup).not.toContain('Agent location')
expect(markup).not.toContain('aria-label="Agent location"')
expect(markup).not.toContain('Show installed agents from WSL default.')
expect(markup).toContain('Agent runtime')
expect(markup).toContain('aria-label="Agent runtime"')
expect(markup).toContain('Detect and launch agents in Ubuntu via WSL')
})
it('hides the WSL agent location controls on platforms without WSL support', () => {
@ -170,9 +189,34 @@ describe('AgentsPane', () => {
expect(markup).not.toContain('Agent location')
expect(markup).not.toContain('aria-label="Agent location"')
expect(markup).not.toContain('Agent runtime')
expect(markup).not.toContain('aria-label="Agent runtime"')
expect(markup).not.toContain('WSL is not available on this machine.')
})
it('updates the global project runtime when changing agent runtime', async () => {
const updateSettings = vi.fn()
const element = AgentRuntimeSetting({
settings: getDefaultSettings('/tmp'),
updateSettings,
refresh: detectedAgentsMock.refresh,
wslSupportedPlatform: true,
wslAvailable: true,
wslDistros: ['Ubuntu'],
wslCapabilitiesLoading: false
})
const control = findSegmentedControl(element, 'Agent runtime')
const onChange = control.props.onChange as (value: 'windows-host' | 'wsl') => void
onChange('wsl')
await flushPromiseQueue()
expect(updateSettings).toHaveBeenCalledWith({
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
})
expect(detectedAgentsMock.refresh).toHaveBeenCalledTimes(1)
})
it('describes Windows lid behavior according to the device', () => {
expect(getAgentAwakeDescription('Windows')).toBe(
"Keeps this computer and display awake while agents are working. Lid-close behavior follows this device's power settings."
@ -375,10 +419,11 @@ describe('AgentsPane', () => {
})
})
it('does not include legacy agent location search metadata', () => {
expect(matchesSettingsSearch('agent location', getAgentsPaneSearchEntries())).toBe(false)
it('includes agent runtime search metadata', () => {
expect(matchesSettingsSearch('agent runtime', getAgentsPaneSearchEntries())).toBe(true)
expect(matchesSettingsSearch('agent location', getAgentsPaneSearchEntries())).toBe(true)
expect(matchesSettingsSearch('installed agents in wsl', getAgentsPaneSearchEntries())).toBe(
false
true
)
})

View File

@ -12,6 +12,7 @@ import { Input } from '../ui/input'
import { cn } from '@/lib/utils'
import { AgentAwakeSetting } from './AgentAwakeSetting'
import { AgentCacheTimerSection } from './AgentCacheTimerSection'
import { AgentRuntimeSetting } from './AgentRuntimeSetting'
import {
getAgentGeneratedTabTitlesDescription,
getAgentGeneratedTabTitlesTitle
@ -48,7 +49,6 @@ export { getAgentsPaneSearchEntries } from './agents-search'
type AgentsPaneProps = {
settings: GlobalSettings
updateSettings: (updates: Partial<GlobalSettings>) => void | Promise<void>
/** Deprecated: agent detection now follows the resolved project runtime. */
wslSupportedPlatform?: boolean
wslAvailable?: boolean
wslDistros?: string[]
@ -680,7 +680,14 @@ function DefaultAgentPill({ active, onClick, children }: DefaultAgentPillProps):
)
}
export function AgentsPane({ settings, updateSettings }: AgentsPaneProps): React.JSX.Element {
export function AgentsPane({
settings,
updateSettings,
wslSupportedPlatform,
wslAvailable,
wslDistros,
wslCapabilitiesLoading
}: AgentsPaneProps): React.JSX.Element {
const { detectedIds: detectedList, isRefreshing, refresh } = useDetectedAgents()
// Why: refresh re-spawns the user's login shell to re-capture PATH
// (preflight:refreshAgents on the main side). This handles the
@ -821,6 +828,16 @@ export function AgentsPane({ settings, updateSettings }: AgentsPaneProps): React
</div>
</section>
<AgentRuntimeSetting
settings={settings}
updateSettings={updateSettings}
refresh={refresh}
wslSupportedPlatform={wslSupportedPlatform}
wslAvailable={wslAvailable}
wslDistros={wslDistros}
wslCapabilitiesLoading={wslCapabilitiesLoading}
/>
<AgentStatusHooksSetting settings={settings} updateSettings={updateSettings} />
<AgentGeneratedTabTitlesSetting settings={settings} updateSettings={updateSettings} />

View File

@ -59,7 +59,13 @@ function expandAgentSearchText(value: string): string[] {
return spaced === value ? [value] : [value, spaced]
}
export const getAgentsPaneSearchEntries = createLocalizedCatalog(() => [
type AgentsPaneSearchOptions = {
includeAgentRuntime?: boolean
}
const AGENT_RUNTIME_SEARCH_ENTRY_ID = 'agent-runtime'
const getAllAgentsPaneSearchEntries = createLocalizedCatalog(() => [
{
title: translate('auto.components.settings.agents.search.bb9ad95777', 'Agents'),
description: translate(
@ -68,6 +74,33 @@ export const getAgentsPaneSearchEntries = createLocalizedCatalog(() => [
),
keywords: buildAgentSettingsKeywords()
},
{
title: translate('auto.components.settings.agents.search.agentRuntime', 'Agent Runtime'),
id: AGENT_RUNTIME_SEARCH_ENTRY_ID,
description: translate(
'auto.components.settings.agents.search.agentRuntimeDescription',
'Choose whether agents are detected and launched on Windows or in WSL by default.'
),
keywords: [
...translateSearchKeyword('auto.components.settings.agents.search.96ba2373b6', 'agent'),
...translateSearchKeyword('auto.components.settings.agents.search.runtime', 'runtime'),
...translateSearchKeyword('auto.components.settings.agents.search.d2952dfd74', 'location'),
...translateSearchKeyword(
'auto.components.settings.agents.search.agentLocation',
'agent location'
),
...translateSearchKeyword('auto.components.settings.agents.search.77c02fa3c3', 'windows'),
...translateSearchKeyword('auto.components.settings.agents.search.d608654c03', 'wsl'),
...translateSearchKeyword('auto.components.settings.agents.search.f622b8eb2a', 'linux'),
...translateSearchKeyword('auto.components.settings.agents.search.839e82c81f', 'detect'),
...translateSearchKeyword('auto.components.settings.agents.search.2814401339', 'installed'),
...translateSearchKeyword(
'auto.components.settings.agents.search.installedAgentsWsl',
'installed agents in wsl'
),
...translateSearchKeyword('auto.components.settings.agents.search.719f53350c', 'path')
]
},
{
title: getAgentStatusHooksTitle(),
description: getAgentStatusHooksDescription(),
@ -106,3 +139,13 @@ export const getAgentsPaneSearchEntries = createLocalizedCatalog(() => [
},
...getAgentCacheTimerSearchEntries()
])
export function getAgentsPaneSearchEntries({
includeAgentRuntime = true
}: AgentsPaneSearchOptions = {}) {
const entries = getAllAgentsPaneSearchEntries()
if (includeAgentRuntime) {
return entries
}
return entries.filter((entry) => !('id' in entry) || entry.id !== AGENT_RUNTIME_SEARCH_ENTRY_ID)
}

View File

@ -72,9 +72,10 @@ import { getExperimentalPaneSearchEntries } from '@/components/settings/experime
import { getRepositoryPaneSearchEntries } from '@/components/settings/repository-search'
import { isWebClientLocation } from '@/lib/web-client-location'
import {
getCachedWindowsTerminalCapabilities,
getWindowsTerminalCapabilityOwnerKey
getWindowsTerminalCapabilityOwnerKey,
useWindowsTerminalCapabilities
} from '@/lib/windows-terminal-capabilities'
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
import { translate } from '@/i18n/i18n'
export { isWebClientLocation } from '@/lib/web-client-location'
@ -114,7 +115,7 @@ export function buildSettingsNavigationMetadata({
'Manage AI agents, set a default, and customize commands.'
),
icon: Bot,
searchEntries: getAgentsPaneSearchEntries(),
searchEntries: getAgentsPaneSearchEntries({ includeAgentRuntime: isWindowsTerminalHost }),
group: 'capabilities'
},
{
@ -502,18 +503,21 @@ export function useSettingsNavigationMetadata(): SettingsNavSection[] {
// contents refresh on rerender without depending on i18n.language directly.
useTranslation()
const repos = useAppStore((state) => state.repos)
const activeRuntimeEnvironmentId = useAppStore(
(state) => state.settings?.activeRuntimeEnvironmentId
)
const settings = useAppStore((state) => state.settings)
const isMac = isMacUserAgent()
const isWindows = isWindowsUserAgent()
const isWebClient = isWebClientLocation()
const windowsTerminalCapabilityOwnerKey = getWindowsTerminalCapabilityOwnerKey(
activeRuntimeEnvironmentId
settings?.activeRuntimeEnvironmentId
)
const isWindowsTerminalHost =
isWindows ||
getCachedWindowsTerminalCapabilities(windowsTerminalCapabilityOwnerKey).hostPlatform === 'win32'
const runtimeTarget = getActiveRuntimeTarget(settings)
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
isWindows || isWebClient || runtimeTarget.kind === 'environment',
false,
windowsTerminalCapabilityOwnerKey,
runtimeTarget
)
const isWindowsTerminalHost = isWindows || windowsTerminalCapabilities.hostPlatform === 'win32'
// Why: Settings and Cmd+J share this metadata so platform/runtime visibility
// and search entries cannot drift. Keep this hook free of Settings pane UI

View File

@ -0,0 +1,25 @@
import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime'
type LocalPreflightContextKeyInput =
| {
wslDistro?: string | null
wslDefault?: boolean
runtimeContextKey?: string
projectRuntime?: ProjectExecutionRuntimeResolution
}
| undefined
export function localPreflightContextKey(context: LocalPreflightContextKeyInput): string {
if (context?.projectRuntime) {
return context.projectRuntime.status === 'resolved'
? context.projectRuntime.runtime.cacheKey
: context.projectRuntime.repair.cacheKey
}
if (context?.runtimeContextKey) {
return context.runtimeContextKey
}
if (context?.wslDistro) {
return `wsl:${context.wslDistro}`
}
return context?.wslDefault ? 'wsl:default' : 'host'
}

View File

@ -324,6 +324,65 @@ describe('local preflight context', () => {
expect(localPreflightContextKey(context)).toBe('repo-1:repair:wsl-distro-required:default')
})
it('uses the global WSL runtime for local agent checks without an active project', () => {
const state = {
...makeState({ repoPath: undefined }),
activeRepoId: null,
activeWorktreeId: null,
settings: {
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
}
} as unknown as AppState
const context = getLocalAgentPreflightContext(state, 'win32')
expect(context).toEqual({
wslDistro: 'Ubuntu',
projectRuntime: {
status: 'resolved',
runtime: {
kind: 'wsl',
hostPlatform: 'wsl',
projectId: 'local-project',
distro: 'Ubuntu',
reason: 'global-default',
cacheKey: 'local-project:wsl:Ubuntu'
}
}
})
expect(localPreflightContextKey(context)).toBe('local-project:wsl:Ubuntu')
})
it('uses the global runtime default over stale legacy agent location without an active project', () => {
const state = {
...makeState({ repoPath: undefined }),
activeRepoId: null,
activeWorktreeId: null,
settings: {
localAgentRuntime: 'host',
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
}
} as unknown as AppState
const context = getLocalAgentPreflightContext(state, 'win32')
expect(localPreflightContextKey(context)).toBe('local-project:wsl:Ubuntu')
})
it('does not use the global runtime default for active SSH projects', () => {
const state = {
...makeState({
repoPath: '/home/alice/repo',
repo: { connectionId: 'builder', executionHostId: 'ssh:builder' }
}),
settings: {
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
}
} as unknown as AppState
expect(getLocalAgentPreflightContext(state, 'win32')).toBeUndefined()
})
it('uses the project override over legacy agent location for local agent checks', () => {
const state = {
...makeState({ repoPath: 'C:\\Users\\alice\\repo' }),

View File

@ -14,6 +14,8 @@ import {
hasCachedWindowsTerminalCapabilities
} from './windows-terminal-capabilities'
export { localPreflightContextKey } from './local-preflight-context-key'
type LocalProjectRuntimeState = Pick<
AppState,
'activeRepoId' | 'activeWorktreeId' | 'projects' | 'repos' | 'settings' | 'worktreesByRepo'
@ -53,10 +55,6 @@ function getWslPreflightContext(wslDistro: string): NonNullable<LocalPreflightCo
return context
}
function getProjectRuntimeCacheKey(resolution: ProjectExecutionRuntimeResolution): string {
return resolution.status === 'resolved' ? resolution.runtime.cacheKey : resolution.repair.cacheKey
}
function getProjectRuntimeContextObjectCacheKey(
resolution: ProjectExecutionRuntimeResolution
): string {
@ -193,6 +191,25 @@ export function getLocalAgentPreflightContext(
return getProjectRuntimePreflightContext(projectRuntime)
}
if (
appPlatform === 'win32' &&
!state.activeRepoId &&
!state.activeWorktreeId &&
state.settings?.localWindowsRuntimeDefault
) {
// Why: Settings -> Agents is global and can mount before any project is
// active; still respect the Windows/WSL runtime default for PATH detection.
return getProjectRuntimePreflightContext(
resolveProjectExecutionRuntime({
appPlatform: 'win32',
projectId: getLocalPreflightProjectId(state),
projectRuntimePreference: { kind: 'inherit-global' },
globalWindowsRuntimeDefault: state.settings.localWindowsRuntimeDefault,
...wslContext
})
)
}
const explicitAgentRuntime = appPlatform === 'win32' ? state.settings?.localAgentRuntime : null
if (explicitAgentRuntime === 'host') {
return getProjectRuntimePreflightContext(
@ -315,16 +332,3 @@ function getLocalPreflightProjectId(
activeWorktree?.projectId ?? activeWorktree?.repoId ?? state.activeRepoId ?? 'local-project'
)
}
export function localPreflightContextKey(context: LocalPreflightContext): string {
if (context?.projectRuntime) {
return getProjectRuntimeCacheKey(context.projectRuntime)
}
if (context?.runtimeContextKey) {
return context.runtimeContextKey
}
if (context?.wslDistro) {
return `wsl:${context.wslDistro}`
}
return context?.wslDefault ? 'wsl:default' : 'host'
}

View File

@ -289,6 +289,33 @@ describe('createDetectedAgentsSlice WSL context', () => {
})
})
it('detects agents in the global WSL runtime when no project is active', async () => {
const store = createTestStore({
settings: {
localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }
} as AppState['settings'],
activeRepoId: null,
activeWorktreeId: null
})
await expect(store.getState().ensureDetectedAgents()).resolves.toEqual(['claude'])
expect(detectAgents).toHaveBeenCalledWith({
wslDistro: 'Ubuntu',
projectRuntime: {
status: 'resolved',
runtime: {
kind: 'wsl',
hostPlatform: 'wsl',
projectId: 'local-project',
distro: 'Ubuntu',
reason: 'global-default',
cacheKey: 'local-project:wsl:Ubuntu'
}
}
})
})
it('detects agents in the project override runtime instead of legacy agent location', async () => {
const store = createTestStore({
settings: {