Fix WSL agent runtime selection (#6394)
This commit is contained in:
parent
b651b4be6d
commit
4cb25abe24
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
@ -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' }),
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue