diff --git a/src/renderer/src/components/right-sidebar/PortsPanel.folder-workspace.test.tsx b/src/renderer/src/components/right-sidebar/PortsPanel.folder-workspace.test.tsx new file mode 100644 index 00000000000..a8329a66b91 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/PortsPanel.folder-workspace.test.tsx @@ -0,0 +1,94 @@ +// @vitest-environment happy-dom + +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import type { FolderWorkspace } from '../../../../shared/folder-workspace-types' +import type { WorkspacePortScanResult } from '../../../../shared/workspace-ports' +import { TooltipProvider } from '@/components/ui/tooltip' +import { useAppStore } from '@/store' +import { workspacePortRuntimeTargetKey } from '@/lib/workspace-port-actions' +import PortsPanel from './PortsPanel' + +const initialState = useAppStore.getInitialState() + +const folderWorkspace: FolderWorkspace = { + id: 'f1', + projectGroupId: 'g1', + name: 'notes', + folderPath: '/home/me/notes', + connectionId: null, + executionHostId: 'runtime:env-a', + linkedTask: null, + comment: '', + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 0, + lastActivityAt: 0, + createdAt: 0, + updatedAt: 0 +} + +const scan: WorkspacePortScanResult = { + platform: 'linux', + scannedAt: 1, + ports: [ + { + id: '127.0.0.1:5173:42', + bindHost: '127.0.0.1', + connectHost: '127.0.0.1', + port: 5173, + pid: 42, + processName: 'vite', + protocol: 'http', + kind: 'external' + } + ] +} + +afterEach(() => { + cleanup() + useAppStore.setState(initialState, true) +}) + +describe('Ports panel for a folder workspace on a managed SSH server', () => { + it('shows the SSH forwards and the server listeners, not "No workspace selected"', () => { + const scanKey = `${workspacePortRuntimeTargetKey({ kind: 'environment', environmentId: 'env-a' })}:all` + useAppStore.setState({ + folderWorkspaces: [folderWorkspace], + activeWorktreeId: 'folder:f1', + activeWorkspaceExecutionHostId: 'runtime:env-a', + runtimeEnvironments: [ + { + id: 'env-a', + name: 'Box', + createdAt: 0, + updatedAt: 0, + lastUsedAt: null, + runtimeId: null, + preferredEndpointId: 'ep', + endpoints: [], + orcadDeployment: { + sshTargetId: 'box', + sshTargetGeneration: 1, + localPort: 7001, + remotePort: 7002 + } + } + ], + sshConnectionStates: new Map([ + ['box', { targetId: 'box', status: 'connected', error: null, reconnectAttempt: 0 }] + ]), + workspacePortScansByKey: { [scanKey]: scan } + }) + render( + + + + ) + expect(screen.queryByText('No workspace selected')).toBeNull() + expect(screen.getByRole('button', { name: /Add/ })).toBeTruthy() + // The server's listener reaches the (collapsed) External section. + expect(screen.getByRole('button', { name: /External\s*1/ })).toBeTruthy() + }) +}) diff --git a/src/renderer/src/components/right-sidebar/local-workspace-ports-panel.tsx b/src/renderer/src/components/right-sidebar/local-workspace-ports-panel.tsx index 84d541d8a4e..99686556c8c 100644 --- a/src/renderer/src/components/right-sidebar/local-workspace-ports-panel.tsx +++ b/src/renderer/src/components/right-sidebar/local-workspace-ports-panel.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react' import { RefreshCw, Server } from 'lucide-react' import { toast } from 'sonner' import { useAppStore } from '@/store' -import { useActiveWorktree, useRepoById } from '@/store/selectors' +import { useActiveWorktree } from '@/store/selectors' import { cn } from '@/lib/utils' import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' import { @@ -40,7 +40,8 @@ export function LocalWorkspacePortsPanel({ sshForwardTargetId?: string | null }): React.JSX.Element { const activeWorktree = useActiveWorktree() - const activeRepo = useRepoById(activeWorktree?.repoId ?? null) + // Why the workspace's repoId, not a repo row: a folder workspace has no repo, only a synthetic id. + const activeRepoId = activeWorktree?.repoId ?? null const settings = useAppStore((s) => s.settings) const createBrowserTab = useAppStore((s) => s.createBrowserTab) const setRemoteBrowserPageHandle = useAppStore((s) => s.setRemoteBrowserPageHandle) @@ -57,7 +58,7 @@ export function LocalWorkspacePortsPanel({ const scanKey = runtimeTarget ? `${workspacePortRuntimeTargetKey(runtimeTarget)}:all` : null const refresh = useCallback(() => { - if (!activeRepo || !runtimeTarget || !scanKey) { + if (!activeRepoId || !runtimeTarget || !scanKey) { return Promise.resolve() } setWorkspacePortScanRefreshing(true) @@ -92,7 +93,7 @@ export function LocalWorkspacePortsPanel({ }) return promise }, [ - activeRepo, + activeRepoId, runtimeTarget, scanKey, replaceWorkspacePortScans, @@ -109,11 +110,11 @@ export function LocalWorkspacePortsPanel({ const handleStopPort = useCallback( async (port: WorkspacePort) => { - if (!activeRepo || !port.pid) { + if (!activeRepoId || !port.pid) { return } const result = await killWorkspacePortForTarget(runtimeTarget, { - repoId: activeRepo.id, + repoId: activeRepoId, pid: port.pid, port: port.port }) @@ -146,7 +147,7 @@ export function LocalWorkspacePortsPanel({ ) } }, - [activeRepo, runtimeTarget, replaceWorkspacePortScans, setWorkspacePortScanRefreshing] + [activeRepoId, runtimeTarget, replaceWorkspacePortScans, setWorkspacePortScanRefreshing] ) const handleOpenPortInBrowser = useCallback( @@ -178,8 +179,8 @@ export function LocalWorkspacePortsPanel({ ) const { activePorts, otherWorkspacePorts, externalPorts } = useMemo( - () => getLocalWorkspacePortSections(displayScan, activeRepo?.id, activeWorktree?.id), - [activeRepo?.id, activeWorktree?.id, displayScan] + () => getLocalWorkspacePortSections(displayScan, activeRepoId, activeWorktree?.id), + [activeRepoId, activeWorktree?.id, displayScan] ) const showPortSections = shouldShowLocalWorkspacePortSections(displayScan, { @@ -188,7 +189,7 @@ export function LocalWorkspacePortsPanel({ externalPorts }) - if (!activeRepo) { + if (!activeRepoId) { return (