diff --git a/src/renderer/src/components/tab-bar/TabBar.tsx b/src/renderer/src/components/tab-bar/TabBar.tsx index 313186f46f2..a6ecdc5917b 100644 --- a/src/renderer/src/components/tab-bar/TabBar.tsx +++ b/src/renderer/src/components/tab-bar/TabBar.tsx @@ -76,6 +76,7 @@ import { TabStripScrollIndicator } from './TabStripScrollIndicator' import { getTabStripScrollMaskClassName } from './tab-strip-scroll-metrics' import { useTabStripOverflowNavigation } from './tab-strip-overflow-navigation' import { useTabStripDragScrollHandlers } from './tab-strip-drag-scroll' +import { shouldShowWindowsShellMenu } from './windows-shell-menu-visibility' const isWindows = navigator.userAgent.includes('Windows') const isMacOs = navigator.userAgent.includes('Mac') @@ -374,13 +375,14 @@ function TabBarInner({ 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. - const shouldShowWindowsShellMenu = - (isWindows || windowsTerminalCapabilities.hostPlatform === 'win32') && - !worktreeHasRemoteConnection + const showWindowsShellMenu = shouldShowWindowsShellMenu({ + activeRuntimeEnvironmentId, + hostPlatform: windowsTerminalCapabilities.hostPlatform, + isWindowsClient: isWindows, + worktreeHasRemoteConnection + }) const localProjectRuntime = useMemo(() => { - if (!shouldShowWindowsShellMenu || activeRuntimeEnvironmentId?.trim()) { + if (!showWindowsShellMenu || activeRuntimeEnvironmentId?.trim()) { return undefined } return getLocalProjectExecutionRuntimeContext( @@ -410,7 +412,7 @@ function TabBarInner({ projects, repos, settings, - shouldShowWindowsShellMenu, + showWindowsShellMenu, windowsTerminalCapabilities.isLoading, windowsTerminalCapabilities.wslAvailable, windowsTerminalCapabilities.wslDistros, @@ -491,7 +493,7 @@ function TabBarInner({ pendingNewTabMenuFocusRef.current = () => focusTerminalTabSurface(tabId) } const windowsShellEntries = useMemo(() => { - if (!shouldShowWindowsShellMenu || !onNewTerminalWithShell) { + if (!showWindowsShellMenu || !onNewTerminalWithShell) { return undefined } const includeHostShells = projectRuntimeShellMenuMode !== 'wsl' @@ -538,7 +540,7 @@ function TabBarInner({ defaultWindowsShell, onNewTerminalWithShell, projectRuntimeShellMenuMode, - shouldShowWindowsShellMenu, + showWindowsShellMenu, windowsTerminalCapabilities.gitBashAvailable, windowsTerminalCapabilities.wslAvailable ]) diff --git a/src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts b/src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts index 4a7da1c89de..7c92ba6aa86 100644 --- a/src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts +++ b/src/renderer/src/components/tab-bar/TabBar.windows-shell-launch.test.ts @@ -751,4 +751,68 @@ describe('TabBar PowerShell launch wiring', () => { expect(findDropdownMenuItemByText(expandNode(element), 'New Terminal: PowerShell')).toBeNull() expect(findDropdownMenuItemByText(expandNode(element), 'New Terminal')).not.toBeNull() }) + + it('hides local Windows shell rows for a non-Windows serve runtime', async () => { + // Why: a Windows desktop client paired to a Linux `orca serve` runs its PTY on + // the serve host. The local Windows shell choices (PowerShell/CMD/WSL) are + // meaningless there; the plain "New Terminal" already opens the serve's default + // shell. Sibling tests above assert that a win32 remote host still shows the + // rows, so the LOCAL Windows-WSL project-runtime menu (hostPlatform 'win32') + // is unaffected by this suppression. + vi.stubGlobal('navigator', { userAgent: 'Windows' }) + vi.stubGlobal('__ORCA_WEB_CLIENT__', false) + vi.stubGlobal('window', { + api: { + wsl: { + isAvailable: vi.fn().mockResolvedValue(false), + listDistros: vi.fn().mockResolvedValue([]) + }, + pwsh: { isAvailable: vi.fn().mockResolvedValue(false) }, + gitBash: { isAvailable: vi.fn().mockResolvedValue(false) }, + runtime: { getStatus: vi.fn().mockResolvedValue({ hostPlatform: 'linux' }) } + } + }) + appStoreSnapshot.activeRuntimeEnvironmentId = 'serve-env-1' + const capabilities = await import('@/lib/windows-terminal-capabilities') + await capabilities.loadWindowsTerminalCapabilities({ + force: true, + ownerKey: 'runtime:serve-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) => unknown }).type + : null + expect(TabBar).not.toBeNull() + + const element = TabBar!({ + tabs: [], + activeTabId: null, + worktreeId: 'wt-1', + expandedPaneByTabId: {}, + onActivate: () => {}, + onClose: () => {}, + onCloseOthers: () => {}, + onCloseToRight: () => {}, + onNewTerminalTab: () => {}, + onNewTerminalWithShell: vi.fn(), + onNewBrowserTab: () => {}, + onSetCustomTitle: () => {}, + onSetTabColor: () => {}, + onTogglePaneExpand: () => {} + }) + + expect( + findDropdownMenuItemByText(expandNode(element), 'New Terminal: PowerShell') + ).toBeNull() + expect( + findDropdownMenuItemByText(expandNode(element), 'New Terminal: CMD Prompt') + ).toBeNull() + expect(findDropdownMenuItemByText(expandNode(element), 'New Terminal: WSL')).toBeNull() + expect(findDropdownMenuItemByText(expandNode(element), 'New Terminal')).not.toBeNull() + }) }) diff --git a/src/renderer/src/components/tab-bar/windows-shell-menu-visibility.test.ts b/src/renderer/src/components/tab-bar/windows-shell-menu-visibility.test.ts new file mode 100644 index 00000000000..fefc1ca44a4 --- /dev/null +++ b/src/renderer/src/components/tab-bar/windows-shell-menu-visibility.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' +import { shouldShowWindowsShellMenu } from './windows-shell-menu-visibility' + +describe('shouldShowWindowsShellMenu', () => { + it('shows local Windows shells for a local Windows client without a runtime owner', () => { + expect( + shouldShowWindowsShellMenu({ + activeRuntimeEnvironmentId: null, + hostPlatform: null, + isWindowsClient: true, + worktreeHasRemoteConnection: false + }) + ).toBe(true) + }) + + it('keeps the menu hidden while a runtime host platform is unknown', () => { + expect( + shouldShowWindowsShellMenu({ + activeRuntimeEnvironmentId: 'serve-env-1', + hostPlatform: null, + isWindowsClient: true, + worktreeHasRemoteConnection: false + }) + ).toBe(false) + }) + + it('shows runtime Windows shells only after the runtime host is known to be Windows', () => { + expect( + shouldShowWindowsShellMenu({ + activeRuntimeEnvironmentId: 'serve-env-1', + hostPlatform: 'win32', + isWindowsClient: false, + worktreeHasRemoteConnection: false + }) + ).toBe(true) + }) + + it('hides local Windows shells for a non-Windows runtime host', () => { + expect( + shouldShowWindowsShellMenu({ + activeRuntimeEnvironmentId: 'serve-env-1', + hostPlatform: 'linux', + isWindowsClient: true, + worktreeHasRemoteConnection: false + }) + ).toBe(false) + }) + + it('hides local Windows shells for SSH worktrees', () => { + expect( + shouldShowWindowsShellMenu({ + activeRuntimeEnvironmentId: null, + hostPlatform: 'win32', + isWindowsClient: true, + worktreeHasRemoteConnection: true + }) + ).toBe(false) + }) +}) diff --git a/src/renderer/src/components/tab-bar/windows-shell-menu-visibility.ts b/src/renderer/src/components/tab-bar/windows-shell-menu-visibility.ts new file mode 100644 index 00000000000..48af2956238 --- /dev/null +++ b/src/renderer/src/components/tab-bar/windows-shell-menu-visibility.ts @@ -0,0 +1,16 @@ +export function shouldShowWindowsShellMenu(args: { + activeRuntimeEnvironmentId: string | null | undefined + hostPlatform: NodeJS.Platform | null + isWindowsClient: boolean + worktreeHasRemoteConnection: boolean +}): boolean { + // Why: runtime terminals execute on the runtime host. Until that host is known + // to be Windows, local Windows shell choices would advertise the wrong target. + const runtimeHostIsNotKnownWindows = + Boolean(args.activeRuntimeEnvironmentId?.trim()) && args.hostPlatform !== 'win32' + return ( + (args.isWindowsClient || args.hostPlatform === 'win32') && + !args.worktreeHasRemoteConnection && + !runtimeHostIsNotKnownWindows + ) +}