mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
Support remote host Windows terminal UI (#4589)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -55,6 +55,7 @@ import {
|
||||
getWindowsTerminalCapabilityOwnerKey,
|
||||
useWindowsTerminalCapabilities
|
||||
} from '@/lib/windows-terminal-capabilities'
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { getShortcutPlatform } from '@/lib/shortcut-platform'
|
||||
import { keybindingMatchesAction } from '../../../../shared/keybindings'
|
||||
import {
|
||||
@@ -583,19 +584,29 @@ function Settings(): React.JSX.Element {
|
||||
const windowsTerminalCapabilityOwnerKey = getWindowsTerminalCapabilityOwnerKey(
|
||||
settings?.activeRuntimeEnvironmentId
|
||||
)
|
||||
// Why: General owns the Orca CLI controls, including WSL skill-location setup.
|
||||
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
|
||||
(isWindows || isWebClient) &&
|
||||
const runtimeTarget = useMemo(
|
||||
() => getActiveRuntimeTarget(settings),
|
||||
[settings?.activeRuntimeEnvironmentId]
|
||||
)
|
||||
const hasActiveRuntimeEnvironment = Boolean(settings?.activeRuntimeEnvironmentId?.trim())
|
||||
const shouldLoadWindowsTerminalCapabilities =
|
||||
hasActiveRuntimeEnvironment ||
|
||||
((isWindows || isWebClient) &&
|
||||
(neededSectionIds.has('terminal') ||
|
||||
neededSectionIds.has('general') ||
|
||||
neededSectionIds.has('accounts') ||
|
||||
neededSectionIds.has('agents')),
|
||||
neededSectionIds.has('agents')))
|
||||
// Why: General owns the Orca CLI controls, including WSL skill-location setup.
|
||||
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
|
||||
shouldLoadWindowsTerminalCapabilities,
|
||||
true,
|
||||
windowsTerminalCapabilityOwnerKey
|
||||
windowsTerminalCapabilityOwnerKey,
|
||||
runtimeTarget
|
||||
)
|
||||
// Why: WSL can be unsupported on macOS/Linux, or supported-but-unavailable on Windows.
|
||||
// Only the latter should render disabled WSL controls.
|
||||
const wslSupportedPlatform = isWindows || windowsTerminalCapabilities.hostPlatform === 'win32'
|
||||
const isWindowsTerminalHost = isWindows || windowsTerminalCapabilities.hostPlatform === 'win32'
|
||||
|
||||
if ([...neededSectionIds].some((id) => !mountedSectionIds.has(id))) {
|
||||
// Why: lazy Settings sections are remembered for the session; record newly
|
||||
@@ -1074,6 +1085,7 @@ function Settings(): React.JSX.Element {
|
||||
wslCapabilitiesLoading={windowsTerminalCapabilities.isLoading}
|
||||
pwshAvailable={windowsTerminalCapabilities.pwshAvailable}
|
||||
gitBashAvailable={windowsTerminalCapabilities.gitBashAvailable}
|
||||
isWindowsTerminalHost={isWindowsTerminalHost}
|
||||
/>
|
||||
) : null}
|
||||
</SettingsSection>
|
||||
|
||||
@@ -261,6 +261,30 @@ describe('TerminalPane PowerShell version setting', () => {
|
||||
expect(collectText(element)).toContain('WSL')
|
||||
})
|
||||
|
||||
it('shows Windows shell controls for a remote Windows host on a non-Windows client', () => {
|
||||
const element = TerminalPane({
|
||||
settings: {
|
||||
terminalScrollbackBytes: 10_000_000,
|
||||
terminalWindowsShell: 'powershell.exe',
|
||||
terminalWindowsPowerShellImplementation: 'auto',
|
||||
terminalWordSeparator: ''
|
||||
} as never,
|
||||
updateSettings: () => {},
|
||||
scrollbackMode: 'preset',
|
||||
setScrollbackMode: () => {},
|
||||
wslAvailable: true,
|
||||
wslDistros: ['Ubuntu'],
|
||||
pwshAvailable: false,
|
||||
gitBashAvailable: false,
|
||||
isWindowsTerminalHost: true
|
||||
})
|
||||
|
||||
const text = collectText(element)
|
||||
expect(text).toContain('Default shell for new terminal panes on Windows')
|
||||
expect(text).toContain('Command Prompt')
|
||||
expect(text).toContain('WSL')
|
||||
})
|
||||
|
||||
it('hides WSL as a Windows default shell option when unavailable', () => {
|
||||
const element = TerminalPane({
|
||||
settings: {
|
||||
|
||||
@@ -53,6 +53,8 @@ type TerminalPaneProps = {
|
||||
pwshAvailable?: boolean
|
||||
/** Whether Git for Windows bash.exe is installed on this machine. */
|
||||
gitBashAvailable?: boolean
|
||||
/** Whether the active terminal host is Windows, even if the client is not. */
|
||||
isWindowsTerminalHost?: boolean
|
||||
}
|
||||
|
||||
export function TerminalPane({
|
||||
@@ -64,10 +66,12 @@ export function TerminalPane({
|
||||
wslDistros = EMPTY_WSL_DISTROS,
|
||||
wslCapabilitiesLoading = false,
|
||||
pwshAvailable,
|
||||
gitBashAvailable = false
|
||||
gitBashAvailable = false,
|
||||
isWindowsTerminalHost
|
||||
}: TerminalPaneProps): React.JSX.Element {
|
||||
const searchQuery = useAppStore((state) => state.settingsSearchQuery)
|
||||
const isWindows = isWindowsUserAgent()
|
||||
const showWindowsHostSettings = isWindowsTerminalHost ?? isWindows
|
||||
const isMac = isMacUserAgent()
|
||||
const detectedLayout = useDetectedOptionAsAlt()
|
||||
const detectedLayoutLabel =
|
||||
@@ -90,11 +94,13 @@ export function TerminalPane({
|
||||
? [selectedWslDistroName, ...wslDistros]
|
||||
: wslDistros
|
||||
const powerShellImplementation = settings.terminalWindowsPowerShellImplementation ?? 'auto'
|
||||
const showWindowsPowerShellImplementation = isWindows && windowsShell === 'powershell.exe'
|
||||
const showWindowsPowerShellImplementation =
|
||||
showWindowsHostSettings && windowsShell === 'powershell.exe'
|
||||
const showGitBashOption = gitBashAvailable || windowsShell === WINDOWS_GIT_BASH_SHELL
|
||||
|
||||
const visibleSections = [
|
||||
isWindows && matchesSettingsSearch(searchQuery, TERMINAL_WINDOWS_SHELL_SEARCH_ENTRY) ? (
|
||||
showWindowsHostSettings &&
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_WINDOWS_SHELL_SEARCH_ENTRY) ? (
|
||||
<section key="windows-shell" className="space-y-3">
|
||||
<SettingsSubsectionHeader
|
||||
title="Windows Shell"
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
getWindowsTerminalCapabilityOwnerKey,
|
||||
useWindowsTerminalCapabilities
|
||||
} from '@/lib/windows-terminal-capabilities'
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
|
||||
type StatusBarProps = {
|
||||
floatingTerminalOpen: boolean
|
||||
@@ -602,10 +603,16 @@ function ClaudeSwitcherMenu({
|
||||
const inactiveClaudeAccounts = useAppStore((s) => s.rateLimits.inactiveClaudeAccounts)
|
||||
const claudeTarget = useAppStore((s) => s.rateLimits.claudeTarget)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const hasActiveRuntimeEnvironment = Boolean(settings?.activeRuntimeEnvironmentId?.trim())
|
||||
const runtimeTarget = useMemo(
|
||||
() => getActiveRuntimeTarget(settings),
|
||||
[settings?.activeRuntimeEnvironmentId]
|
||||
)
|
||||
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
|
||||
navigator.userAgent.includes('Windows'),
|
||||
navigator.userAgent.includes('Windows') || hasActiveRuntimeEnvironment,
|
||||
false,
|
||||
getWindowsTerminalCapabilityOwnerKey(settings?.activeRuntimeEnvironmentId)
|
||||
getWindowsTerminalCapabilityOwnerKey(settings?.activeRuntimeEnvironmentId),
|
||||
runtimeTarget
|
||||
)
|
||||
const claudeAccountSyncKey = useAppStore((s) => {
|
||||
const settings = s.settings
|
||||
@@ -1092,10 +1099,16 @@ function CodexSwitcherMenu({
|
||||
const inactiveCodexAccounts = useAppStore((s) => s.rateLimits.inactiveCodexAccounts)
|
||||
const codexTarget = useAppStore((s) => s.rateLimits.codexTarget)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const hasActiveRuntimeEnvironment = Boolean(settings?.activeRuntimeEnvironmentId?.trim())
|
||||
const runtimeTarget = useMemo(
|
||||
() => getActiveRuntimeTarget(settings),
|
||||
[settings?.activeRuntimeEnvironmentId]
|
||||
)
|
||||
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
|
||||
navigator.userAgent.includes('Windows'),
|
||||
navigator.userAgent.includes('Windows') || hasActiveRuntimeEnvironment,
|
||||
false,
|
||||
getWindowsTerminalCapabilityOwnerKey(settings?.activeRuntimeEnvironmentId)
|
||||
getWindowsTerminalCapabilityOwnerKey(settings?.activeRuntimeEnvironmentId),
|
||||
runtimeTarget
|
||||
)
|
||||
const codexAccountSyncKey = useAppStore((s) => {
|
||||
const settings = s.settings
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
getWindowsTerminalCapabilityOwnerKey,
|
||||
useWindowsTerminalCapabilities
|
||||
} from '@/lib/windows-terminal-capabilities'
|
||||
import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client'
|
||||
import { useShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import {
|
||||
type BuiltInWindowsTerminalShell,
|
||||
@@ -248,13 +249,18 @@ function TabBarInner({
|
||||
const windowsTerminalCapabilityOwnerKey = getWindowsTerminalCapabilityOwnerKey(
|
||||
activeRuntimeEnvironmentId
|
||||
)
|
||||
const runtimeTarget = useMemo(
|
||||
() => getActiveRuntimeTarget({ activeRuntimeEnvironmentId }),
|
||||
[activeRuntimeEnvironmentId]
|
||||
)
|
||||
const shouldProbeWindowsShellCapabilities =
|
||||
(isWindows || (isWebClient && activeRuntimeEnvironmentId !== null)) &&
|
||||
(isWindows || Boolean(activeRuntimeEnvironmentId?.trim()) || isWebClient) &&
|
||||
!worktreeHasRemoteConnection
|
||||
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
|
||||
shouldProbeWindowsShellCapabilities,
|
||||
false,
|
||||
windowsTerminalCapabilityOwnerKey
|
||||
windowsTerminalCapabilityOwnerKey,
|
||||
runtimeTarget
|
||||
)
|
||||
// Why: SSH-backed PTYs ignore local Windows shell overrides; showing these
|
||||
// entries there promises PowerShell/CMD/Git Bash but opens the remote shell.
|
||||
|
||||
@@ -428,6 +428,63 @@ describe('TabBar PowerShell launch wiring', () => {
|
||||
expect(findDropdownMenuItemByText(expandNode(element), 'New Terminal: WSL')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('uses the active remote host platform to show Windows shell rows in a Mac desktop client', async () => {
|
||||
vi.stubGlobal('navigator', { userAgent: 'Macintosh' })
|
||||
vi.stubGlobal('__ORCA_WEB_CLIENT__', false)
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
wsl: {
|
||||
isAvailable: vi.fn().mockResolvedValue(true),
|
||||
listDistros: vi.fn().mockResolvedValue(['Ubuntu'])
|
||||
},
|
||||
pwsh: { isAvailable: vi.fn().mockResolvedValue(false) },
|
||||
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) },
|
||||
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
|
||||
}
|
||||
})
|
||||
appStoreSnapshot.activeRuntimeEnvironmentId = 'desktop-env-1'
|
||||
const capabilities = await import('@/lib/windows-terminal-capabilities')
|
||||
await capabilities.loadWindowsTerminalCapabilities({
|
||||
force: true,
|
||||
ownerKey: 'runtime:desktop-env-1'
|
||||
})
|
||||
|
||||
const tabBarModule = await import('./TabBar')
|
||||
const candidate = tabBarModule.default ?? tabBarModule
|
||||
const TabBar =
|
||||
typeof candidate === 'function'
|
||||
? candidate
|
||||
: typeof (candidate as { type?: unknown }).type === 'function'
|
||||
? (candidate as { type: (props: Record<string, unknown>) => unknown }).type
|
||||
: null
|
||||
expect(TabBar).not.toBeNull()
|
||||
|
||||
const element = TabBar!({
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
worktreeId: 'wt-1',
|
||||
expandedPaneByTabId: {},
|
||||
onActivate: () => {},
|
||||
onClose: () => {},
|
||||
onCloseOthers: () => {},
|
||||
onCloseToRight: () => {},
|
||||
onNewTerminalTab: () => {},
|
||||
onNewTerminalWithShell: () => {},
|
||||
onNewBrowserTab: () => {},
|
||||
onSetCustomTitle: () => {},
|
||||
onSetTabColor: () => {},
|
||||
onTogglePaneExpand: () => {}
|
||||
})
|
||||
|
||||
expect(
|
||||
findDropdownMenuItemByText(expandNode(element), 'New Terminal: PowerShell')
|
||||
).not.toBeNull()
|
||||
expect(
|
||||
findDropdownMenuItemByText(expandNode(element), 'New Terminal: CMD Prompt')
|
||||
).not.toBeNull()
|
||||
expect(findDropdownMenuItemByText(expandNode(element), 'New Terminal: WSL')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('shows the Git Bash terminal row when shared Windows capabilities find bash.exe', async () => {
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
|
||||
@@ -68,6 +68,10 @@ import { STATS_PANE_SEARCH_ENTRIES } from '@/components/stats/stats-search'
|
||||
import { EXPERIMENTAL_PANE_SEARCH_ENTRIES } from '@/components/settings/experimental-search'
|
||||
import { getRepositoryPaneSearchEntries } from '@/components/settings/repository-search'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
getCachedWindowsTerminalCapabilities,
|
||||
getWindowsTerminalCapabilityOwnerKey
|
||||
} from '@/lib/windows-terminal-capabilities'
|
||||
|
||||
function OrcaLogoSettingsIcon({ className }: LucideProps) {
|
||||
return createElement('img', {
|
||||
@@ -88,17 +92,19 @@ export function isWebClientLocation(): boolean {
|
||||
export function buildSettingsNavigationMetadata({
|
||||
isMac,
|
||||
isWindows,
|
||||
isWindowsTerminalHost = isWindows,
|
||||
isWebClient,
|
||||
repos
|
||||
}: {
|
||||
isMac: boolean
|
||||
isWindows: boolean
|
||||
isWindowsTerminalHost?: boolean
|
||||
isWebClient: boolean
|
||||
repos: readonly Repo[]
|
||||
}): SettingsNavSection[] {
|
||||
const showDesktopOnlySettings = !isWebClient
|
||||
const terminalPaneSearchEntries = getTerminalPaneSearchEntries({
|
||||
isWindows,
|
||||
isWindows: isWindowsTerminalHost,
|
||||
isMac
|
||||
})
|
||||
const runtimeEnvironmentsSearchEntry = isWebClient
|
||||
@@ -366,15 +372,31 @@ export function buildSettingsNavigationMetadata({
|
||||
|
||||
export function useSettingsNavigationMetadata(): SettingsNavSection[] {
|
||||
const repos = useAppStore((state) => state.repos)
|
||||
const activeRuntimeEnvironmentId = useAppStore(
|
||||
(state) => state.settings?.activeRuntimeEnvironmentId
|
||||
)
|
||||
const isMac = isMacUserAgent()
|
||||
const isWindows = isWindowsUserAgent()
|
||||
const isWebClient = isWebClientLocation()
|
||||
const windowsTerminalCapabilityOwnerKey = getWindowsTerminalCapabilityOwnerKey(
|
||||
activeRuntimeEnvironmentId
|
||||
)
|
||||
const isWindowsTerminalHost =
|
||||
isWindows ||
|
||||
getCachedWindowsTerminalCapabilities(windowsTerminalCapabilityOwnerKey).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
|
||||
// imports; see docs/reference/cmd-j-settings-actions-plan.md.
|
||||
return useMemo(
|
||||
() => buildSettingsNavigationMetadata({ isMac, isWindows, isWebClient, repos }),
|
||||
[isMac, isWindows, isWebClient, repos]
|
||||
() =>
|
||||
buildSettingsNavigationMetadata({
|
||||
isMac,
|
||||
isWindows,
|
||||
isWindowsTerminalHost,
|
||||
isWebClient,
|
||||
repos
|
||||
}),
|
||||
[isMac, isWindows, isWindowsTerminalHost, isWebClient, repos]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -197,6 +197,64 @@ describe('windows terminal capabilities', () => {
|
||||
expect(runtimeGetStatus).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('loads remote runtime host capabilities through runtime RPC', async () => {
|
||||
const runtimeEnvironmentCall = vi.fn(async (args: { selector: string; method: string }) => {
|
||||
const resultByMethod: Record<string, unknown> = {
|
||||
'status.get': {
|
||||
hostPlatform: 'win32',
|
||||
runtimeProtocolVersion: 3,
|
||||
minCompatibleRuntimeClientVersion: 2
|
||||
},
|
||||
'host.wsl.isAvailable': true,
|
||||
'host.wsl.listDistros': ['Ubuntu'],
|
||||
'host.pwsh.isAvailable': true,
|
||||
'host.gitBash.isAvailable': false
|
||||
}
|
||||
return {
|
||||
id: args.method,
|
||||
ok: true,
|
||||
result: resultByMethod[args.method]
|
||||
}
|
||||
})
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
runtimeEnvironments: {
|
||||
call: runtimeEnvironmentCall
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await expect(
|
||||
loadWindowsTerminalCapabilities({
|
||||
ownerKey: 'runtime:env-win',
|
||||
target: { kind: 'environment', environmentId: 'env-win' }
|
||||
})
|
||||
).resolves.toEqual({
|
||||
wslAvailable: true,
|
||||
wslDistros: ['Ubuntu'],
|
||||
pwshAvailable: true,
|
||||
gitBashAvailable: false,
|
||||
hostPlatform: 'win32',
|
||||
isLoading: false
|
||||
})
|
||||
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ selector: 'env-win', method: 'host.wsl.isAvailable' })
|
||||
)
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ selector: 'env-win', method: 'host.wsl.listDistros' })
|
||||
)
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ selector: 'env-win', method: 'host.pwsh.isAvailable' })
|
||||
)
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ selector: 'env-win', method: 'host.gitBash.isAvailable' })
|
||||
)
|
||||
expect(runtimeEnvironmentCall).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ selector: 'env-win', method: 'status.get' })
|
||||
)
|
||||
})
|
||||
|
||||
it('prunes expired runtime owner capability caches', async () => {
|
||||
stubTerminalCapabilityApi({
|
||||
wslAvailable: false,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { callRuntimeRpc, type RuntimeClientTarget } from '@/runtime/runtime-rpc-client'
|
||||
import type { RuntimeStatus } from '../../../shared/runtime-types'
|
||||
|
||||
export type WindowsTerminalCapabilities = {
|
||||
wslAvailable: boolean
|
||||
@@ -37,16 +39,15 @@ type WindowsTerminalCapabilityHookState = {
|
||||
capabilities: WindowsTerminalCapabilities
|
||||
}
|
||||
|
||||
type WindowsTerminalCapabilityLoadTarget = RuntimeClientTarget
|
||||
|
||||
export function getWindowsTerminalCapabilityOwnerKey(
|
||||
activeRuntimeEnvironmentId?: string | null
|
||||
): string {
|
||||
const isWebClient = (globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ === true
|
||||
if (!isWebClient) {
|
||||
return 'local'
|
||||
}
|
||||
// Why: paired web clients can switch hosts; Git Bash/WSL availability is
|
||||
// Why: remote desktop and paired web clients can switch hosts; Git Bash/WSL availability is
|
||||
// host-owned, so a previous runtime's answer must not bleed into the next.
|
||||
return `runtime:${activeRuntimeEnvironmentId?.trim() || 'none'}`
|
||||
const environmentId = activeRuntimeEnvironmentId?.trim()
|
||||
return environmentId ? `runtime:${environmentId}` : 'local'
|
||||
}
|
||||
|
||||
function publish(
|
||||
@@ -99,10 +100,12 @@ export function loadWindowsTerminalCapabilities(
|
||||
force?: boolean
|
||||
now?: number
|
||||
ownerKey?: string
|
||||
target?: WindowsTerminalCapabilityLoadTarget
|
||||
} = {}
|
||||
): Promise<WindowsTerminalCapabilities> {
|
||||
const now = options.now ?? Date.now()
|
||||
const ownerKey = options.ownerKey ?? 'local'
|
||||
const target = options.target ?? { kind: 'local' }
|
||||
pruneExpiredCapabilityOwners(now)
|
||||
const cached = cachedCapabilitiesByOwnerKey.get(ownerKey)
|
||||
if (cached && !options.force && now - cached.loadedAt < CAPABILITY_CACHE_TTL_MS) {
|
||||
@@ -117,16 +120,7 @@ export function loadWindowsTerminalCapabilities(
|
||||
// Separate probes can leave one surface showing stale Windows shell choices.
|
||||
const requestId = ++nextCapabilityRequestId
|
||||
latestCapabilityRequestIdByOwnerKey.set(ownerKey, requestId)
|
||||
const nextPendingCapabilities = Promise.all([
|
||||
window.api.wsl.isAvailable().catch(() => false),
|
||||
window.api.wsl.listDistros().catch(() => []),
|
||||
window.api.pwsh.isAvailable().catch(() => false),
|
||||
window.api.gitBash.isAvailable().catch(() => false),
|
||||
window.api.runtime
|
||||
.getStatus()
|
||||
.then((status) => status.hostPlatform ?? null)
|
||||
.catch(() => null)
|
||||
])
|
||||
const nextPendingCapabilities = Promise.all(readWindowsTerminalCapabilityPromises(target))
|
||||
.then(([wslAvailable, wslDistros, pwshAvailable, gitBashAvailable, hostPlatform]) => {
|
||||
const capabilities = {
|
||||
wslAvailable,
|
||||
@@ -157,9 +151,10 @@ export function loadWindowsTerminalCapabilities(
|
||||
}
|
||||
|
||||
export function refreshWindowsTerminalCapabilities(
|
||||
ownerKey = 'local'
|
||||
ownerKey = 'local',
|
||||
target: WindowsTerminalCapabilityLoadTarget = { kind: 'local' }
|
||||
): Promise<WindowsTerminalCapabilities> {
|
||||
return loadWindowsTerminalCapabilities({ force: true, ownerKey })
|
||||
return loadWindowsTerminalCapabilities({ force: true, ownerKey, target })
|
||||
}
|
||||
|
||||
export function selectWindowsTerminalCapabilitiesForOwner(
|
||||
@@ -178,8 +173,11 @@ export function selectWindowsTerminalCapabilitiesForOwner(
|
||||
export function useWindowsTerminalCapabilities(
|
||||
enabled: boolean,
|
||||
forceRefreshOnMount = false,
|
||||
ownerKey = 'local'
|
||||
ownerKey = 'local',
|
||||
target: WindowsTerminalCapabilityLoadTarget = { kind: 'local' }
|
||||
): WindowsTerminalCapabilities {
|
||||
const targetKind = target.kind
|
||||
const targetEnvironmentId = target.kind === 'environment' ? target.environmentId : null
|
||||
const [state, setState] = useState(() => ({
|
||||
ownerKey,
|
||||
capabilities: getCachedWindowsTerminalCapabilities(ownerKey)
|
||||
@@ -190,6 +188,10 @@ export function useWindowsTerminalCapabilities(
|
||||
setState({ ownerKey, capabilities: UNAVAILABLE_CAPABILITIES })
|
||||
return
|
||||
}
|
||||
const loadTarget: WindowsTerminalCapabilityLoadTarget =
|
||||
targetKind === 'environment' && targetEnvironmentId
|
||||
? { kind: 'environment', environmentId: targetEnvironmentId }
|
||||
: { kind: 'local' }
|
||||
|
||||
let cancelled = false
|
||||
const cached = getCachedWindowsTerminalCapabilities(ownerKey)
|
||||
@@ -204,13 +206,15 @@ export function useWindowsTerminalCapabilities(
|
||||
const subscribers = subscribersByOwnerKey.get(ownerKey) ?? new Set()
|
||||
subscribers.add(setCapabilities)
|
||||
subscribersByOwnerKey.set(ownerKey, subscribers)
|
||||
void loadWindowsTerminalCapabilities({ force: forceRefreshOnMount, ownerKey }).then(
|
||||
(nextCapabilities) => {
|
||||
if (!cancelled) {
|
||||
setState({ ownerKey, capabilities: nextCapabilities })
|
||||
}
|
||||
void loadWindowsTerminalCapabilities({
|
||||
force: forceRefreshOnMount,
|
||||
ownerKey,
|
||||
target: loadTarget
|
||||
}).then((nextCapabilities) => {
|
||||
if (!cancelled) {
|
||||
setState({ ownerKey, capabilities: nextCapabilities })
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
@@ -220,11 +224,52 @@ export function useWindowsTerminalCapabilities(
|
||||
subscribersByOwnerKey.delete(ownerKey)
|
||||
}
|
||||
}
|
||||
}, [enabled, forceRefreshOnMount, ownerKey])
|
||||
}, [enabled, forceRefreshOnMount, ownerKey, targetKind, targetEnvironmentId])
|
||||
|
||||
return selectWindowsTerminalCapabilitiesForOwner(state, enabled, ownerKey)
|
||||
}
|
||||
|
||||
function readWindowsTerminalCapabilityPromises(
|
||||
target: WindowsTerminalCapabilityLoadTarget
|
||||
): [
|
||||
Promise<boolean>,
|
||||
Promise<string[]>,
|
||||
Promise<boolean>,
|
||||
Promise<boolean>,
|
||||
Promise<NodeJS.Platform | null>
|
||||
] {
|
||||
if (target.kind === 'local') {
|
||||
return [
|
||||
window.api.wsl.isAvailable().catch(() => false),
|
||||
window.api.wsl.listDistros().catch(() => []),
|
||||
window.api.pwsh.isAvailable().catch(() => false),
|
||||
window.api.gitBash.isAvailable().catch(() => false),
|
||||
window.api.runtime
|
||||
.getStatus()
|
||||
.then((status) => status.hostPlatform ?? null)
|
||||
.catch(() => null)
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
callRuntimeRpc<boolean>(target, 'host.wsl.isAvailable', undefined, { timeoutMs: 15_000 }).catch(
|
||||
() => false
|
||||
),
|
||||
callRuntimeRpc<string[]>(target, 'host.wsl.listDistros', undefined, {
|
||||
timeoutMs: 15_000
|
||||
}).catch(() => []),
|
||||
callRuntimeRpc<boolean>(target, 'host.pwsh.isAvailable', undefined, {
|
||||
timeoutMs: 15_000
|
||||
}).catch(() => false),
|
||||
callRuntimeRpc<boolean>(target, 'host.gitBash.isAvailable', undefined, {
|
||||
timeoutMs: 15_000
|
||||
}).catch(() => false),
|
||||
callRuntimeRpc<RuntimeStatus>(target, 'status.get', undefined, { timeoutMs: 15_000 })
|
||||
.then((status) => status.hostPlatform ?? null)
|
||||
.catch(() => null)
|
||||
]
|
||||
}
|
||||
|
||||
export function resetWindowsTerminalCapabilitiesForTests(): void {
|
||||
cachedCapabilitiesByOwnerKey.clear()
|
||||
pendingCapabilitiesByOwnerKey.clear()
|
||||
|
||||
Reference in New Issue
Block a user