fix(sidebar): identify remote server workspaces (#10061)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin 2026-07-22 23:03:39 -04:00 committed by GitHub
parent bae29fd1e2
commit a2e440b308
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 184 additions and 34 deletions

View File

@ -14,6 +14,7 @@ let WorktreeCard: typeof WorktreeCardComponent
let sshConnectionStates = new Map<string, { status: string }>()
let sshTargetLabels = new Map<string, string>()
let runtimeStatusByEnvironmentId = new Map<string, { status?: unknown }>()
let runtimeEnvironments: { id: string; name: string }[] = []
let sshStateByEnvironment = new Map()
let worktreesByRepo: Record<string, Worktree[]> = {}
let worktreeCardProperties: WorktreeCardProperty[] = ['status']
@ -32,6 +33,7 @@ vi.mock('@/store', () => ({
openModal,
projectGroups: [],
remoteBranchConflictByWorktreeId: {},
runtimeEnvironments,
runtimeStatusByEnvironmentId,
removedSshTargetLabels: new Map(),
settings: null,
@ -136,6 +138,7 @@ describe('WorktreeCard SSH reconnect prompt', () => {
sshConnectionStates = new Map()
sshTargetLabels = new Map()
runtimeStatusByEnvironmentId = new Map()
runtimeEnvironments = []
sshStateByEnvironment = new Map()
worktreesByRepo = {}
worktreeCardProperties = ['status']
@ -157,6 +160,7 @@ describe('WorktreeCard SSH reconnect prompt', () => {
})
it('marks a runtime-host worktree disconnected when its environment has no status', () => {
runtimeEnvironments = [{ id: 'env-1', name: 'Remote Mac' }]
const runtimeRepo: Repo = {
...makeRepo(),
connectionId: undefined,
@ -166,21 +170,34 @@ describe('WorktreeCard SSH reconnect prompt', () => {
const markup = renderToStaticMarkup(
<WorktreeCard worktree={makeWorktree()} repo={runtimeRepo} isActive={false} />
)
expect(markup).toContain('Server disconnected')
expect(markup).toContain('Remote Mac disconnected')
})
it('shows a runtime-host worktree as connected when its environment has a status', () => {
it('distinguishes connected worktrees on different Orca servers', () => {
runtimeEnvironments = [
{ id: 'env-1', name: 'Remote Mac' },
{ id: 'env-2', name: 'Build Linux' }
]
runtimeStatusByEnvironmentId.set('env-1', { status: { runtimeId: 'r1' } })
const runtimeRepo: Repo = {
...makeRepo(),
connectionId: undefined,
executionHostId: 'runtime:env-1'
}
const markup = renderToStaticMarkup(
<WorktreeCard worktree={makeWorktree()} repo={runtimeRepo} isActive={false} />
runtimeStatusByEnvironmentId.set('env-2', { status: { runtimeId: 'r2' } })
const remoteMacMarkup = renderToStaticMarkup(
<WorktreeCard
worktree={{ ...makeWorktree(), runtimeOwnerEnvironmentId: 'env-1' }}
repo={{ ...makeRepo(), connectionId: undefined, executionHostId: 'runtime:env-2' }}
isActive={false}
/>
)
expect(markup).not.toContain('Server disconnected')
expect(markup).toContain('Project on Orca server')
const buildLinuxMarkup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree()}
repo={{ ...makeRepo(), connectionId: undefined, executionHostId: 'runtime:env-2' }}
isActive={false}
/>
)
expect(remoteMacMarkup).toContain('Project on Remote Mac')
expect(buildLinuxMarkup).toContain('Project on Build Linux')
})
it('reads nested SSH readiness from the owning HUB instead of client-local SSH state', () => {

View File

@ -81,7 +81,12 @@ import {
import { translate } from '@/i18n/i18n'
import { recordRendererCrashBreadcrumb } from '@/lib/crash-diagnostics'
import { folderWorkspaceKey, parseWorkspaceKey } from '../../../../shared/workspace-scope'
import { isRuntimeOwnedSshTargetId, parseExecutionHostId } from '../../../../shared/execution-host'
import {
isRuntimeOwnedSshTargetId,
parseExecutionHostId,
toRuntimeExecutionHostId
} from '../../../../shared/execution-host'
import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides'
import { DEFAULT_AGENT_ACTIVITY_DISPLAY_MODE } from '../../../../shared/constants'
import { getExplicitRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import {
@ -358,13 +363,28 @@ const WorktreeCard = React.memo(function WorktreeCard({
(s) => (s.activeTabTypeByWorktree?.[worktree.id] ?? 'terminal') === 'terminal'
)
const parsedRepoHost = parseExecutionHostId(repo?.executionHostId)
const runtimeOwnerEnvironmentId =
worktree.runtimeOwnerEnvironmentId ??
(parsedRepoHost?.kind === 'runtime' ? parsedRepoHost.environmentId : null)
const runtimeHostId = runtimeOwnerEnvironmentId
? toRuntimeExecutionHostId(runtimeOwnerEnvironmentId)
: null
const runtimeEnvironmentName = useAppStore((s) =>
runtimeOwnerEnvironmentId
? (s.runtimeEnvironments.find((environment) => environment.id === runtimeOwnerEnvironmentId)
?.name ?? null)
: null
)
const runtimeHostLabel = runtimeHostId
? (getHostDisplayLabelOverrides(settings).get(runtimeHostId) ?? runtimeEnvironmentName)
: null
// Why: runtime ("Orca server") hosts get the same disconnected dimming as SSH when their environment has no live status.
const isRuntimeDisconnected = useAppStore((s) => {
const parsed = parseExecutionHostId(repo?.executionHostId)
if (parsed?.kind !== 'runtime') {
if (!runtimeOwnerEnvironmentId) {
return false
}
return !s.runtimeStatusByEnvironmentId.get(parsed.environmentId)?.status
return !s.runtimeStatusByEnvironmentId.get(runtimeOwnerEnvironmentId)?.status
})
// Why: the reconnect dialog blocks, so it never auto-shows for the active card (would steal app-wide focus); opens only on deliberate focus (handleClick).
const [showDisconnectedDialog, setShowDisconnectedDialog] = useState(false)
@ -1405,31 +1425,42 @@ const WorktreeCard = React.memo(function WorktreeCard({
</Tooltip>
)}
{!repo?.connectionId &&
parseExecutionHostId(repo?.executionHostId)?.kind === 'runtime' && (
<Tooltip>
<TooltipTrigger asChild>
<span className="shrink-0 inline-flex items-center">
{isRuntimeDisconnected ? (
<ServerOff className="size-3 text-red-400" />
) : (
<Server className="size-3 text-muted-foreground" />
)}
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{isRuntimeDisconnected
{!repo?.connectionId && parsedRepoHost?.kind === 'runtime' && (
<Tooltip>
<TooltipTrigger asChild>
<span className="shrink-0 inline-flex items-center">
{isRuntimeDisconnected ? (
<ServerOff className="size-3 text-red-400" />
) : (
<Server className="size-3 text-muted-foreground" />
)}
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8}>
{isRuntimeDisconnected
? runtimeHostLabel
? translate(
'auto.components.sidebar.WorktreeCard.runtimeHostDisconnectedNamed',
'{{hostName}} disconnected',
{ hostName: runtimeHostLabel }
)
: translate(
'auto.components.sidebar.WorktreeCard.runtimeHostDisconnected',
'Server disconnected'
)
: runtimeHostLabel
? translate(
'auto.components.sidebar.WorktreeCard.runtimeHostProjectNamed',
'Project on {{hostName}}',
{ hostName: runtimeHostLabel }
)
: translate(
'auto.components.sidebar.WorktreeCard.runtimeHostProject',
'Project on Orca server'
)}
</TooltipContent>
</Tooltip>
)}
</TooltipContent>
</Tooltip>
)}
{showInlineRepoBadge && (
<RepoIdentityChip repo={repo}>

View File

@ -1125,6 +1125,65 @@ describe('buildRows with pinned worktrees', () => {
])
})
it('shows distinct Orca server names when status grouping mixes runtime hosts', () => {
const firstRepo: Repo = {
...repo,
id: 'repo-runtime-a',
executionHostId: 'runtime:env-a'
}
const secondRepo: Repo = {
...repo,
id: 'repo-runtime-b',
executionHostId: 'runtime:env-b'
}
const firstWorktree: Worktree = {
...worktree,
id: 'wt-runtime-a',
repoId: firstRepo.id
}
const secondWorktree: Worktree = {
...worktree,
id: 'wt-runtime-b',
repoId: secondRepo.id
}
const rows = buildRows(
'workspace-status',
[firstWorktree, secondWorktree],
new Map([
[firstRepo.id, firstRepo],
[secondRepo.id, secondRepo]
]),
null,
new Set(),
undefined,
undefined,
undefined,
{},
new Map([
[firstWorktree.id, firstWorktree],
[secondWorktree.id, secondWorktree]
]),
false,
undefined,
[],
new Set(),
new Map(),
new Map(),
[],
undefined,
[],
new Map([
['runtime:env-a', 'Remote Mac'],
['runtime:env-b', 'Build Linux']
])
)
expect(rows.filter((row) => row.type === 'item')).toMatchObject([
{ worktree: { id: firstWorktree.id }, hostContextLabel: 'Remote Mac' },
{ worktree: { id: secondWorktree.id }, hostContextLabel: 'Build Linux' }
])
})
it('omits host context labels when a project group only has one host', () => {
const secondLocalWorktree: Worktree = {
...worktree,

View File

@ -585,6 +585,7 @@ function appendWorktreeRows(
groupDepth: number
sectionKey: string
hostContextLabelByRepoId?: ReadonlyMap<string, string>
hostContextLabelByWorktreeId?: ReadonlyMap<string, string>
cyclicLineageIds: ReadonlySet<string>
}
): void {
@ -594,6 +595,7 @@ function appendWorktreeRows(
groupDepth,
sectionKey,
hostContextLabelByRepoId,
hostContextLabelByWorktreeId,
cyclicLineageIds
} = options
if (!nestLineage) {
@ -608,7 +610,9 @@ function appendWorktreeRows(
isLastLineageChild: false,
lineageChildCount: 0,
lineageCollapsed: false,
hostContextLabel: hostContextLabelByRepoId?.get(worktree.repoId)
hostContextLabel:
hostContextLabelByWorktreeId?.get(worktree.id) ??
hostContextLabelByRepoId?.get(worktree.repoId)
})
)
}
@ -653,7 +657,9 @@ function appendWorktreeRows(
isLastLineageChild: isLastChild,
lineageChildCount: children.length,
lineageCollapsed,
hostContextLabel: hostContextLabelByRepoId?.get(worktree.repoId)
hostContextLabel:
hostContextLabelByWorktreeId?.get(worktree.id) ??
hostContextLabelByRepoId?.get(worktree.repoId)
})
)
if (lineageCollapsed) {
@ -721,6 +727,22 @@ function getMixedHostContextLabels(
return uniqueLabels.size > 1 ? labelsByRepoId : undefined
}
function getMixedWorktreeHostContextLabels(
worktrees: readonly Worktree[],
repoMap: Map<string, Repo>,
hostLabelById: ReadonlyMap<string, string> | undefined,
defaultHostId: ExecutionHostId
): Map<string, string> | undefined {
const labelsByWorktreeId = new Map<string, string>()
const uniqueHostIds = new Set<ExecutionHostId>()
for (const worktree of worktrees) {
const hostId = getWorktreeExecutionHostId(worktree, repoMap.get(worktree.repoId), defaultHostId)
uniqueHostIds.add(hostId)
labelsByWorktreeId.set(worktree.id, hostLabelById?.get(hostId) ?? getExecutionHostLabel(hostId))
}
return uniqueHostIds.size > 1 ? labelsByWorktreeId : undefined
}
function getHostWorktreeCounts(
worktrees: readonly Worktree[],
repoMap: Map<string, Repo>,
@ -1013,6 +1035,12 @@ export function buildRows(
pinnedDisplayPolicy === 'duplicate-in-groups'
? worktrees
: worktrees.filter((worktree) => !worktree.isPinned)
const mixedWorktreeHostContextLabels = getMixedWorktreeHostContextLabels(
naturalWorktrees,
repoMap,
hostLabelById,
defaultHostId
)
const renderedNaturalAnchorRepoIds = getRenderedNaturalAnchorRepoIds({
groupBy,
worktrees: naturalWorktrees,
@ -1052,6 +1080,7 @@ export function buildRows(
collapsedGroups,
groupDepth: 0,
sectionKey: ALL_GROUP_KEY,
hostContextLabelByWorktreeId: mixedWorktreeHostContextLabels,
cyclicLineageIds
})
}
@ -1290,6 +1319,8 @@ export function buildRows(
groupBy === 'repo'
? getMixedHostContextLabels(group, repoMap, projectIndex, hostLabelById)
: undefined
const hostContextLabelByWorktreeId =
groupBy === 'repo' ? undefined : mixedWorktreeHostContextLabels
if (groupBy === 'repo') {
appendWorktreeRows(result, items, repoMap, lineageById, worktreeMap, {
nestLineage,
@ -1297,6 +1328,7 @@ export function buildRows(
groupDepth: projectGroupDepth,
sectionKey: key,
hostContextLabelByRepoId,
hostContextLabelByWorktreeId,
cyclicLineageIds
})
} else {
@ -1306,6 +1338,7 @@ export function buildRows(
groupDepth: projectGroupDepth,
sectionKey: key,
hostContextLabelByRepoId,
hostContextLabelByWorktreeId,
cyclicLineageIds
})
}

View File

@ -4298,7 +4298,9 @@
"ca74db7550": "Project on SSH host",
"021538e1d1": "SSH disconnected",
"runtimeHostDisconnected": "Server disconnected",
"runtimeHostDisconnectedNamed": "{{hostName}} disconnected",
"runtimeHostProject": "Project on Orca server",
"runtimeHostProjectNamed": "Project on {{hostName}}",
"automationCreated": "Created by automation",
"branchIdentity": "Branch",
"branchFolderPathIdentity": "Branch or folder path",

View File

@ -4252,7 +4252,9 @@
"ca74db7550": "Proyecto en host SSH",
"021538e1d1": "SSH desconectado",
"runtimeHostDisconnected": "Servidor desconectado",
"runtimeHostDisconnectedNamed": "{{hostName}} está desconectado",
"runtimeHostProject": "Proyecto en servidor de Orca",
"runtimeHostProjectNamed": "Proyecto en {{hostName}}",
"automationCreated": "Creado por automatización",
"branchIdentity": "Rama",
"branchFolderPathIdentity": "Rama o ruta de carpeta",

View File

@ -4233,7 +4233,9 @@
"ca74db7550": "SSH 経由のリモートプロジェクト",
"021538e1d1": "SSH が切断されました",
"runtimeHostDisconnected": "サーバーが切断されました",
"runtimeHostDisconnectedNamed": "{{hostName}} が切断されました",
"runtimeHostProject": "Orca サーバー上のプロジェクト",
"runtimeHostProjectNamed": "{{hostName}} 上のプロジェクト",
"automationCreated": "自動化により作成",
"branchIdentity": "ブランチ",
"branchFolderPathIdentity": "ブランチまたはフォルダパス",

View File

@ -4233,7 +4233,9 @@
"ca74db7550": "SSH를 통한 원격 프로젝트",
"021538e1d1": "SSH 연결이 끊어졌습니다.",
"runtimeHostDisconnected": "서버 연결 끊김",
"runtimeHostDisconnectedNamed": "{{hostName}} 연결 끊김",
"runtimeHostProject": "Orca 서버의 프로젝트",
"runtimeHostProjectNamed": "{{hostName}}의 프로젝트",
"automationCreated": "자동화로 생성됨",
"branchIdentity": "브랜치",
"branchFolderPathIdentity": "브랜치 또는 폴더 경로",

View File

@ -4233,7 +4233,9 @@
"ca74db7550": "SSH 主机上的项目",
"021538e1d1": "SSH 已断开连接",
"runtimeHostDisconnected": "服务器已断开连接",
"runtimeHostDisconnectedNamed": "{{hostName}} 已断开连接",
"runtimeHostProject": "Orca 服务器上的项目",
"runtimeHostProjectNamed": "{{hostName}} 上的项目",
"automationCreated": "由自动化创建",
"branchIdentity": "分支",
"branchFolderPathIdentity": "分支或文件夹路径",