From 4cb25abe247ea810e39ee3920b89b5b0e26ebc17 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Thu, 25 Jun 2026 17:43:51 -0700 Subject: [PATCH] Fix WSL agent runtime selection (#6394) --- .../components/agent/AgentSettingsDialog.tsx | 29 ++- .../settings/AgentRuntimeSetting.tsx | 192 ++++++++++++++++++ .../components/settings/AgentsPane.test.tsx | 63 +++++- .../src/components/settings/AgentsPane.tsx | 21 +- .../src/components/settings/agents-search.ts | 45 +++- .../hooks/useSettingsNavigationMetadata.ts | 24 ++- .../src/lib/local-preflight-context-key.ts | 25 +++ .../src/lib/local-preflight-context.test.ts | 59 ++++++ .../src/lib/local-preflight-context.ts | 38 ++-- .../src/store/slices/detected-agents.test.ts | 27 +++ 10 files changed, 483 insertions(+), 40 deletions(-) create mode 100644 src/renderer/src/components/settings/AgentRuntimeSetting.tsx create mode 100644 src/renderer/src/lib/local-preflight-context-key.ts 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: {