diff --git a/src/renderer/src/components/agent/AgentSettingsDialog.tsx b/src/renderer/src/components/agent/AgentSettingsDialog.tsx
index dd33b771a..637ee48e8 100644
--- a/src/renderer/src/components/agent/AgentSettingsDialog.tsx
+++ b/src/renderer/src/components/agent/AgentSettingsDialog.tsx
@@ -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({
diff --git a/src/renderer/src/components/settings/AgentRuntimeSetting.tsx b/src/renderer/src/components/settings/AgentRuntimeSetting.tsx
new file mode 100644
index 000000000..220a380b9
--- /dev/null
+++ b/src/renderer/src/components/settings/AgentRuntimeSetting.tsx
@@ -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
+ updateSettings: (updates: Partial) => void | Promise
+ refresh: () => Promise
+ 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): 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 (
+
+
+
+ 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' ? (
+
+ ) : null}
+
+ }
+ />
+
+ )
+}
+
+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 }
+ )
+}
diff --git a/src/renderer/src/components/settings/AgentsPane.test.tsx b/src/renderer/src/components/settings/AgentsPane.test.tsx
index 0ed91de7a..d2915f198 100644
--- a/src/renderer/src/components/settings/AgentsPane.test.tsx
+++ b/src/renderer/src/components/settings/AgentsPane.test.tsx
@@ -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
)
})
diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx
index 1129b1863..fb02a5b58 100644
--- a/src/renderer/src/components/settings/AgentsPane.tsx
+++ b/src/renderer/src/components/settings/AgentsPane.tsx
@@ -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) => void | Promise
- /** 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
+
+
diff --git a/src/renderer/src/components/settings/agents-search.ts b/src/renderer/src/components/settings/agents-search.ts
index 61fc88eda..5e378d732 100644
--- a/src/renderer/src/components/settings/agents-search.ts
+++ b/src/renderer/src/components/settings/agents-search.ts
@@ -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)
+}
diff --git a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
index 64e36fc67..643443540 100644
--- a/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
+++ b/src/renderer/src/hooks/useSettingsNavigationMetadata.ts
@@ -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
diff --git a/src/renderer/src/lib/local-preflight-context-key.ts b/src/renderer/src/lib/local-preflight-context-key.ts
new file mode 100644
index 000000000..8ec0deeb1
--- /dev/null
+++ b/src/renderer/src/lib/local-preflight-context-key.ts
@@ -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'
+}
diff --git a/src/renderer/src/lib/local-preflight-context.test.ts b/src/renderer/src/lib/local-preflight-context.test.ts
index 16e5e393d..2307a5089 100644
--- a/src/renderer/src/lib/local-preflight-context.test.ts
+++ b/src/renderer/src/lib/local-preflight-context.test.ts
@@ -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' }),
diff --git a/src/renderer/src/lib/local-preflight-context.ts b/src/renderer/src/lib/local-preflight-context.ts
index c1a40511e..ee275f74f 100644
--- a/src/renderer/src/lib/local-preflight-context.ts
+++ b/src/renderer/src/lib/local-preflight-context.ts
@@ -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 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'
-}
diff --git a/src/renderer/src/store/slices/detected-agents.test.ts b/src/renderer/src/store/slices/detected-agents.test.ts
index 0ef08fccd..0d9575eee 100644
--- a/src/renderer/src/store/slices/detected-agents.test.ts
+++ b/src/renderer/src/store/slices/detected-agents.test.ts
@@ -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: {