diff --git a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx index b6bea4e51..1b2b6c9f6 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx @@ -14,6 +14,7 @@ let WorktreeCard: typeof WorktreeCardComponent let sshConnectionStates = new Map() let sshTargetLabels = new Map() let runtimeStatusByEnvironmentId = new Map() +let runtimeEnvironments: { id: string; name: string }[] = [] let sshStateByEnvironment = new Map() let worktreesByRepo: Record = {} 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( ) - 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( - + runtimeStatusByEnvironmentId.set('env-2', { status: { runtimeId: 'r2' } }) + + const remoteMacMarkup = renderToStaticMarkup( + ) - expect(markup).not.toContain('Server disconnected') - expect(markup).toContain('Project on Orca server') + const buildLinuxMarkup = renderToStaticMarkup( + + ) + + 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', () => { diff --git a/src/renderer/src/components/sidebar/WorktreeCard.tsx b/src/renderer/src/components/sidebar/WorktreeCard.tsx index 03f9b311c..dd181ceb9 100644 --- a/src/renderer/src/components/sidebar/WorktreeCard.tsx +++ b/src/renderer/src/components/sidebar/WorktreeCard.tsx @@ -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({ )} - {!repo?.connectionId && - parseExecutionHostId(repo?.executionHostId)?.kind === 'runtime' && ( - - - - {isRuntimeDisconnected ? ( - - ) : ( - - )} - - - - {isRuntimeDisconnected + {!repo?.connectionId && parsedRepoHost?.kind === 'runtime' && ( + + + + {isRuntimeDisconnected ? ( + + ) : ( + + )} + + + + {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' )} - - - )} + + + )} {showInlineRepoBadge && ( diff --git a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts index c3405400d..7d6dae477 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.test.ts @@ -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, diff --git a/src/renderer/src/components/sidebar/worktree-list-groups.ts b/src/renderer/src/components/sidebar/worktree-list-groups.ts index cc637d618..cfc0955a3 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups.ts @@ -585,6 +585,7 @@ function appendWorktreeRows( groupDepth: number sectionKey: string hostContextLabelByRepoId?: ReadonlyMap + hostContextLabelByWorktreeId?: ReadonlyMap cyclicLineageIds: ReadonlySet } ): 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, + hostLabelById: ReadonlyMap | undefined, + defaultHostId: ExecutionHostId +): Map | undefined { + const labelsByWorktreeId = new Map() + const uniqueHostIds = new Set() + 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, @@ -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 }) } diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 66bf18b26..3471bb8ef 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -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", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 6625c0ce0..b708cbcbb 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index f1c2656f1..fd224c1e7 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -4233,7 +4233,9 @@ "ca74db7550": "SSH 経由のリモートプロジェクト", "021538e1d1": "SSH が切断されました", "runtimeHostDisconnected": "サーバーが切断されました", + "runtimeHostDisconnectedNamed": "{{hostName}} が切断されました", "runtimeHostProject": "Orca サーバー上のプロジェクト", + "runtimeHostProjectNamed": "{{hostName}} 上のプロジェクト", "automationCreated": "自動化により作成", "branchIdentity": "ブランチ", "branchFolderPathIdentity": "ブランチまたはフォルダパス", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index cee327565..f4d7e8181 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -4233,7 +4233,9 @@ "ca74db7550": "SSH를 통한 원격 프로젝트", "021538e1d1": "SSH 연결이 끊어졌습니다.", "runtimeHostDisconnected": "서버 연결 끊김", + "runtimeHostDisconnectedNamed": "{{hostName}} 연결 끊김", "runtimeHostProject": "Orca 서버의 프로젝트", + "runtimeHostProjectNamed": "{{hostName}}의 프로젝트", "automationCreated": "자동화로 생성됨", "branchIdentity": "브랜치", "branchFolderPathIdentity": "브랜치 또는 폴더 경로", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index a3777f56a..2645aa755 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -4233,7 +4233,9 @@ "ca74db7550": "SSH 主机上的项目", "021538e1d1": "SSH 已断开连接", "runtimeHostDisconnected": "服务器已断开连接", + "runtimeHostDisconnectedNamed": "{{hostName}} 已断开连接", "runtimeHostProject": "Orca 服务器上的项目", + "runtimeHostProjectNamed": "{{hostName}} 上的项目", "automationCreated": "由自动化创建", "branchIdentity": "分支", "branchFolderPathIdentity": "分支或文件夹路径",