Consolidate tab bar host platform capability

This commit is contained in:
Neil
2026-05-31 03:00:45 -07:00
parent 6d57de21e3
commit 4d7d306f1c
4 changed files with 84 additions and 66 deletions
+14 -32
View File
@@ -216,41 +216,23 @@ function TabBarInner({
),
[agentCmdOverrides, defaultAgent, detectedIds]
)
const [runtimeHostPlatform, setRuntimeHostPlatform] = useState<NodeJS.Platform | null>(null)
useEffect(() => {
if (
!(globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ ||
!activeRuntimeEnvironmentId
) {
setRuntimeHostPlatform(null)
return
}
let cancelled = false
void window.api.runtime
.getStatus()
.then((status) => {
if (!cancelled) {
setRuntimeHostPlatform(status.hostPlatform ?? null)
}
})
.catch(() => {
if (!cancelled) {
setRuntimeHostPlatform(null)
}
})
return () => {
cancelled = true
}
}, [activeRuntimeEnvironmentId])
const isWebClient = (globalThis as { __ORCA_WEB_CLIENT__?: boolean }).__ORCA_WEB_CLIENT__ === true
const windowsTerminalCapabilityOwnerKey = getWindowsTerminalCapabilityOwnerKey(
activeRuntimeEnvironmentId
)
const shouldProbeWindowsShellCapabilities =
(isWindows || (isWebClient && activeRuntimeEnvironmentId !== null)) &&
!worktreeHasRemoteConnection
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
shouldProbeWindowsShellCapabilities,
false,
windowsTerminalCapabilityOwnerKey
)
// Why: SSH-backed PTYs ignore local Windows shell overrides; showing these
// entries there promises PowerShell/CMD/Git Bash but opens the remote shell.
const shouldShowWindowsShellMenu =
(isWindows || runtimeHostPlatform === 'win32') && !worktreeHasRemoteConnection
const windowsTerminalCapabilities = useWindowsTerminalCapabilities(
shouldShowWindowsShellMenu,
false,
getWindowsTerminalCapabilityOwnerKey(activeRuntimeEnvironmentId)
)
(isWindows || windowsTerminalCapabilities.hostPlatform === 'win32') &&
!worktreeHasRemoteConnection
const resolvedGroupId = groupId ?? worktreeId
const statusByRelativePath = useMemo(() => buildStatusMap(gitStatusEntries), [gitStatusEntries])
@@ -16,7 +16,6 @@ const appStoreSnapshot: {
repos: [],
worktreesByRepo: {}
}
let runtimeHostPlatformState: NodeJS.Platform | null | undefined
const useAppStoreMock = vi.fn(
(
@@ -58,9 +57,6 @@ vi.mock('react', async () => {
useRef: <T>(current: T) => ({ current }),
useState: <T>(initial: T | (() => T)) => {
const value = typeof initial === 'function' ? (initial as () => T)() : initial
if (value === null && runtimeHostPlatformState !== undefined) {
return [runtimeHostPlatformState as T, vi.fn()] as const
}
return [value, vi.fn()] as const
}
}
@@ -251,7 +247,6 @@ describe('TabBar PowerShell launch wiring', () => {
appStoreSnapshot.activeRuntimeEnvironmentId = null
appStoreSnapshot.repos = []
appStoreSnapshot.worktreesByRepo = {}
runtimeHostPlatformState = undefined
vi.stubGlobal('navigator', { userAgent: 'Windows' })
})
@@ -267,7 +262,8 @@ describe('TabBar PowerShell launch wiring', () => {
listDistros: vi.fn().mockResolvedValue([])
},
pwsh: { isAvailable: vi.fn().mockResolvedValue(true) },
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) }
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
const capabilities = await import('@/lib/windows-terminal-capabilities')
@@ -318,7 +314,8 @@ describe('TabBar PowerShell launch wiring', () => {
listDistros: vi.fn().mockResolvedValue(['Ubuntu'])
},
pwsh: { isAvailable: vi.fn().mockResolvedValue(false) },
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) }
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
const capabilities = await import('@/lib/windows-terminal-capabilities')
@@ -364,11 +361,11 @@ describe('TabBar PowerShell launch wiring', () => {
listDistros: vi.fn().mockResolvedValue(['Ubuntu'])
},
pwsh: { isAvailable: vi.fn().mockResolvedValue(false) },
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) }
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
appStoreSnapshot.activeRuntimeEnvironmentId = 'web-env-1'
runtimeHostPlatformState = 'win32'
const capabilities = await import('@/lib/windows-terminal-capabilities')
await capabilities.loadWindowsTerminalCapabilities({
force: true,
@@ -419,7 +416,8 @@ describe('TabBar PowerShell launch wiring', () => {
listDistros: vi.fn().mockResolvedValue([])
},
pwsh: { isAvailable: vi.fn().mockResolvedValue(false) },
gitBash: { isAvailable: vi.fn().mockResolvedValue(true) }
gitBash: { isAvailable: vi.fn().mockResolvedValue(true) },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
const capabilities = await import('@/lib/windows-terminal-capabilities')
@@ -474,7 +472,8 @@ describe('TabBar PowerShell launch wiring', () => {
listDistros: vi.fn().mockResolvedValue(['Ubuntu'])
},
pwsh: { isAvailable: vi.fn().mockResolvedValue(true) },
gitBash: { isAvailable: vi.fn().mockResolvedValue(true) }
gitBash: { isAvailable: vi.fn().mockResolvedValue(true) },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
const capabilities = await import('@/lib/windows-terminal-capabilities')
@@ -12,26 +12,32 @@ function stubTerminalCapabilityApi(args: {
pwshAvailable: boolean
wslDistros?: string[]
gitBashAvailable?: boolean
hostPlatform?: NodeJS.Platform | null
}): {
wslIsAvailable: ReturnType<typeof vi.fn>
wslListDistros: ReturnType<typeof vi.fn>
pwshIsAvailable: ReturnType<typeof vi.fn>
isGitBashAvailable: ReturnType<typeof vi.fn>
runtimeGetStatus: ReturnType<typeof vi.fn>
} {
const wslIsAvailable = vi.fn().mockResolvedValue(args.wslAvailable)
const wslListDistros = vi.fn().mockResolvedValue(args.wslDistros ?? [])
const pwshIsAvailable = vi.fn().mockResolvedValue(args.pwshAvailable)
const isGitBashAvailable = vi.fn().mockResolvedValue(args.gitBashAvailable ?? false)
const runtimeGetStatus = vi
.fn()
.mockResolvedValue({ hostPlatform: 'hostPlatform' in args ? args.hostPlatform : 'win32' })
vi.stubGlobal('window', {
api: {
wsl: { isAvailable: wslIsAvailable, listDistros: wslListDistros },
pwsh: { isAvailable: pwshIsAvailable },
gitBash: { isAvailable: isGitBashAvailable }
gitBash: { isAvailable: isGitBashAvailable },
runtime: { getStatus: runtimeGetStatus }
}
})
return { wslIsAvailable, wslListDistros, pwshIsAvailable, isGitBashAvailable }
return { wslIsAvailable, wslListDistros, pwshIsAvailable, isGitBashAvailable, runtimeGetStatus }
}
describe('windows terminal capabilities', () => {
@@ -41,19 +47,25 @@ describe('windows terminal capabilities', () => {
})
it('shares WSL, PowerShell, and Git Bash availability between terminal UI consumers', async () => {
const { wslIsAvailable, wslListDistros, pwshIsAvailable, isGitBashAvailable } =
stubTerminalCapabilityApi({
wslAvailable: true,
pwshAvailable: true,
wslDistros: ['Ubuntu'],
gitBashAvailable: true
})
const {
wslIsAvailable,
wslListDistros,
pwshIsAvailable,
isGitBashAvailable,
runtimeGetStatus
} = stubTerminalCapabilityApi({
wslAvailable: true,
pwshAvailable: true,
wslDistros: ['Ubuntu'],
gitBashAvailable: true
})
expect(getCachedWindowsTerminalCapabilities()).toEqual({
wslAvailable: false,
wslDistros: [],
pwshAvailable: false,
gitBashAvailable: false,
hostPlatform: null,
isLoading: false
})
@@ -62,6 +74,7 @@ describe('windows terminal capabilities', () => {
wslDistros: ['Ubuntu'],
pwshAvailable: true,
gitBashAvailable: true,
hostPlatform: 'win32',
isLoading: false
}
await expect(loadWindowsTerminalCapabilities()).resolves.toEqual(expected)
@@ -72,6 +85,7 @@ describe('windows terminal capabilities', () => {
expect(wslListDistros).toHaveBeenCalledTimes(1)
expect(pwshIsAvailable).toHaveBeenCalledTimes(1)
expect(isGitBashAvailable).toHaveBeenCalledTimes(1)
expect(runtimeGetStatus).toHaveBeenCalledTimes(1)
})
it('keeps WSL available when the PowerShell version probe fails', async () => {
@@ -81,7 +95,8 @@ describe('windows terminal capabilities', () => {
api: {
wsl: { isAvailable: wslIsAvailable, listDistros: vi.fn().mockResolvedValue([]) },
pwsh: { isAvailable: pwshIsAvailable },
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) }
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
@@ -90,6 +105,7 @@ describe('windows terminal capabilities', () => {
wslDistros: [],
pwshAvailable: false,
gitBashAvailable: false,
hostPlatform: 'win32',
isLoading: false
})
})
@@ -101,7 +117,8 @@ describe('windows terminal capabilities', () => {
api: {
wsl: { isAvailable: wslIsAvailable, listDistros: vi.fn().mockResolvedValue([]) },
pwsh: { isAvailable: pwshIsAvailable },
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) }
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
@@ -125,7 +142,8 @@ describe('windows terminal capabilities', () => {
api: {
wsl: { isAvailable: wslIsAvailable, listDistros: vi.fn().mockResolvedValue([]) },
pwsh: { isAvailable: pwshIsAvailable },
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) }
gitBash: { isAvailable: vi.fn().mockResolvedValue(false) },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
@@ -144,6 +162,10 @@ describe('windows terminal capabilities', () => {
it('does not reuse capability cache between runtime owners', async () => {
const isGitBashAvailable = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false)
const runtimeGetStatus = vi
.fn()
.mockResolvedValueOnce({ hostPlatform: 'win32' })
.mockResolvedValueOnce({ hostPlatform: 'linux' })
vi.stubGlobal('window', {
api: {
wsl: {
@@ -151,24 +173,28 @@ describe('windows terminal capabilities', () => {
listDistros: vi.fn().mockResolvedValue([])
},
pwsh: { isAvailable: vi.fn().mockResolvedValue(false) },
gitBash: { isAvailable: isGitBashAvailable }
gitBash: { isAvailable: isGitBashAvailable },
runtime: { getStatus: runtimeGetStatus }
}
})
await expect(
loadWindowsTerminalCapabilities({ ownerKey: 'runtime:host-a' })
).resolves.toMatchObject({ gitBashAvailable: true })
).resolves.toMatchObject({ gitBashAvailable: true, hostPlatform: 'win32' })
await expect(
loadWindowsTerminalCapabilities({ ownerKey: 'runtime:host-b' })
).resolves.toMatchObject({ gitBashAvailable: false })
).resolves.toMatchObject({ gitBashAvailable: false, hostPlatform: 'linux' })
expect(getCachedWindowsTerminalCapabilities('runtime:host-a')).toMatchObject({
gitBashAvailable: true
gitBashAvailable: true,
hostPlatform: 'win32'
})
expect(getCachedWindowsTerminalCapabilities('runtime:host-b')).toMatchObject({
gitBashAvailable: false
gitBashAvailable: false,
hostPlatform: 'linux'
})
expect(isGitBashAvailable).toHaveBeenCalledTimes(2)
expect(runtimeGetStatus).toHaveBeenCalledTimes(2)
})
it('does not select the previous owner capabilities while a new owner loads', async () => {
@@ -180,7 +206,8 @@ describe('windows terminal capabilities', () => {
listDistros: vi.fn().mockResolvedValue([])
},
pwsh: { isAvailable: vi.fn().mockResolvedValue(false) },
gitBash: { isAvailable: isGitBashAvailable }
gitBash: { isAvailable: isGitBashAvailable },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
@@ -197,6 +224,7 @@ describe('windows terminal capabilities', () => {
wslDistros: [],
pwshAvailable: false,
gitBashAvailable: false,
hostPlatform: null,
isLoading: false
})
})
@@ -209,7 +237,8 @@ describe('windows terminal capabilities', () => {
api: {
wsl: { isAvailable: wslIsAvailable, listDistros: vi.fn().mockResolvedValue([]) },
pwsh: { isAvailable: pwshIsAvailable },
gitBash: { isAvailable: isGitBashAvailable }
gitBash: { isAvailable: isGitBashAvailable },
runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'win32' }) }
}
})
@@ -218,6 +247,7 @@ describe('windows terminal capabilities', () => {
wslDistros: [],
pwshAvailable: false,
gitBashAvailable: false,
hostPlatform: 'win32',
isLoading: false
})
})
@@ -5,6 +5,7 @@ export type WindowsTerminalCapabilities = {
wslDistros: string[]
pwshAvailable: boolean
gitBashAvailable: boolean
hostPlatform: NodeJS.Platform | null
isLoading: boolean
}
@@ -13,6 +14,7 @@ const UNAVAILABLE_CAPABILITIES: WindowsTerminalCapabilities = {
wslDistros: [],
pwshAvailable: false,
gitBashAvailable: false,
hostPlatform: null,
isLoading: false
}
@@ -81,22 +83,27 @@ export function loadWindowsTerminalCapabilities(
return pendingCapabilities
}
// Why: Settings and the tab bar need one shared answer. Separate probes can
// leave Settings rendering without WSL while the "+" menu already shows it.
// Why: Settings, status bar, and paired web tab bars need one shared answer.
// 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.gitBash.isAvailable().catch(() => false),
window.api.runtime
.getStatus()
.then((status) => status.hostPlatform ?? null)
.catch(() => null)
])
.then(([wslAvailable, wslDistros, pwshAvailable, gitBashAvailable]) => {
.then(([wslAvailable, wslDistros, pwshAvailable, gitBashAvailable, hostPlatform]) => {
const capabilities = {
wslAvailable,
wslDistros,
pwshAvailable,
gitBashAvailable,
hostPlatform,
isLoading: false
}
if (requestId === latestCapabilityRequestIdByOwnerKey.get(ownerKey)) {