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 (