diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index c47eaf14a..4086cd384 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -64,6 +64,7 @@ import { parseHostWorkspaceRouteFromPathname, } from "@/utils/host-routes"; import { + shouldShowSidebarHostLabels, useSidebarWorkspaceEntry, type SidebarProjectEntry, type SidebarStatusWorkspacePlacement, @@ -1879,6 +1880,7 @@ function ProjectBlock({ creatingWorkspaceIds, activeWorkspaceSelection, hostLabelByServerId, + showHostLabels, }: { project: SidebarProjectEntry; collapsed: boolean; @@ -1899,6 +1901,7 @@ function ProjectBlock({ creatingWorkspaceIds: ReadonlySet; activeWorkspaceSelection: ActiveWorkspaceSelection | null; hostLabelByServerId: ReadonlyMap; + showHostLabels: boolean; }) { const rowModel = useMemo( () => @@ -1928,9 +1931,7 @@ function ProjectBlock({ 1 - ? (hostLabelByServerId.get(item.serverId) ?? item.serverId) - : null + showHostLabels ? (hostLabelByServerId.get(item.serverId) ?? item.serverId) : null } shortcutNumber={shortcutIndexByWorkspaceKey.get(item.workspaceKey) ?? null} showShortcutBadge={showShortcutBadges} @@ -1947,7 +1948,7 @@ function ProjectBlock({ }, [ project.projectKind, - project.hosts.length, + showHostLabels, activeWorkspaceSelection, creatingWorkspaceIds, hostLabelByServerId, @@ -2115,6 +2116,7 @@ function areProjectBlockPropsEqual(previous: ProjectBlockProps, next: ProjectBlo previous.showShortcutBadges === next.showShortcutBadges && previous.shortcutIndexByWorkspaceKey === next.shortcutIndexByWorkspaceKey && previous.hostLabelByServerId === next.hostLabelByServerId && + previous.showHostLabels === next.showHostLabels && previous.parentGestureRef === next.parentGestureRef && previous.onToggleCollapsed === next.onToggleCollapsed && previous.onWorkspacePress === next.onWorkspacePress && @@ -2181,6 +2183,7 @@ export function SidebarWorkspaceList({ } return labels; }, [hosts]); + const showHostLabels = useMemo(() => shouldShowSidebarHostLabels(projects), [projects]); const content = groupMode === "status" ? ( @@ -2189,6 +2192,8 @@ export function SidebarWorkspaceList({ projectNamesByKey={projectNamesByKey} shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey} onWorkspacePress={onWorkspacePress} + hostLabelByServerId={hostLabelByServerId} + showHostLabels={showHostLabels} /> ) : ( ); @@ -2213,11 +2219,15 @@ function SidebarStatusModeWrapper({ projectNamesByKey, shortcutIndexByWorkspaceKey: _projectShortcutIndex, onWorkspacePress, + hostLabelByServerId, + showHostLabels, }: { statusWorkspacePlacements: SidebarStatusWorkspacePlacement[]; projectNamesByKey: Map; shortcutIndexByWorkspaceKey: Map; onWorkspacePress?: () => void; + hostLabelByServerId: ReadonlyMap; + showHostLabels: boolean; }) { const showShortcutBadges = useShowShortcutBadges(); @@ -2228,6 +2238,8 @@ function SidebarStatusModeWrapper({ shortcutIndexByWorkspaceKey={_projectShortcutIndex} showShortcutBadges={showShortcutBadges} onWorkspacePress={onWorkspacePress} + hostLabelByServerId={hostLabelByServerId} + showHostLabels={showHostLabels} /> ); } @@ -2243,12 +2255,14 @@ function ProjectModeList({ parentGestureRef, pathname, hostLabelByServerId, + showHostLabels, }: Omit< SidebarWorkspaceListProps, "statusWorkspacePlacements" | "projectNamesByKey" | "groupMode" | "isRefreshing" | "onRefresh" > & { pathname: string; hostLabelByServerId: ReadonlyMap; + showHostLabels: boolean; }) { const { t } = useTranslation(); const [creatingWorkspaceIds, setCreatingWorkspaceIds] = useState>(() => new Set()); @@ -2436,6 +2450,7 @@ function ProjectModeList({ creatingWorkspaceIds={creatingWorkspaceIds} activeWorkspaceSelection={activeWorkspaceSelection} hostLabelByServerId={hostLabelByServerId} + showHostLabels={showHostLabels} /> ); }, @@ -2445,6 +2460,7 @@ function ProjectModeList({ handleWorktreeCreated, handleWorkspaceReorder, hostLabelByServerId, + showHostLabels, onWorkspacePress, onToggleProjectCollapsed, parentGestureRef, diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx index bd39e2b9d..3b506647c 100644 --- a/packages/app/src/components/sidebar/sidebar-status-list.tsx +++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx @@ -95,6 +95,8 @@ interface StatusWorkspaceListProps { shortcutIndexByWorkspaceKey: Map; showShortcutBadges: boolean; onWorkspacePress?: () => void; + hostLabelByServerId: ReadonlyMap; + showHostLabels: boolean; } export function SidebarStatusWorkspaceList({ @@ -103,6 +105,8 @@ export function SidebarStatusWorkspaceList({ shortcutIndexByWorkspaceKey: _projectShortcutIndex, showShortcutBadges, onWorkspacePress, + hostLabelByServerId, + showHostLabels, }: StatusWorkspaceListProps) { const groups = useMemo( () => buildStatusGroups(workspaces, projectNamesByKey), @@ -138,6 +142,8 @@ export function SidebarStatusWorkspaceList({ shortcutIndex={statusShortcutIndex} showShortcutBadges={showShortcutBadges} onWorkspacePress={onWorkspacePress} + hostLabelByServerId={hostLabelByServerId} + showHostLabels={showHostLabels} /> ) : ( @@ -154,6 +160,8 @@ export function SidebarStatusWorkspaceList({ shortcutIndex={statusShortcutIndex} showShortcutBadges={showShortcutBadges} onWorkspacePress={onWorkspacePress} + hostLabelByServerId={hostLabelByServerId} + showHostLabels={showHostLabels} /> )} @@ -168,6 +176,8 @@ function StatusGroupList({ shortcutIndex, showShortcutBadges, onWorkspacePress, + hostLabelByServerId, + showHostLabels, }: { groups: StatusGroup[]; collapsedStatusGroupKeys: ReadonlySet; @@ -175,6 +185,8 @@ function StatusGroupList({ shortcutIndex: Map; showShortcutBadges: boolean; onWorkspacePress?: () => void; + hostLabelByServerId: ReadonlyMap; + showHostLabels: boolean; }) { return ( <> @@ -190,7 +202,12 @@ function StatusGroupList({ void; @@ -316,7 +348,7 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({ return ( { }); }); +describe("shouldShowSidebarHostLabels", () => { + it("is false with no visible projects", () => { + expect(shouldShowSidebarHostLabels([])).toBe(false); + }); + + it("is false when every project lives on a single host", () => { + const projects = buildSidebarProjectsFromStructure({ + projects: [ + project({ projectKey: "project-a", workspaceKeys: ["ws-1"] }), + project({ projectKey: "project-b", workspaceKeys: ["ws-2"] }), + ], + }); + + expect(shouldShowSidebarHostLabels(projects)).toBe(false); + }); + + it("is true when projects span separate hosts", () => { + const projects = buildSidebarProjectsFromStructure({ + projects: [ + project({ + projectKey: "project-a", + hosts: [ + { serverId: "host-a", iconWorkingDir: "/repo/project-a", canCreateWorktree: true }, + ], + workspaceKeys: ["host-a:ws-1"], + }), + project({ + projectKey: "project-b", + hosts: [ + { serverId: "host-b", iconWorkingDir: "/repo/project-b", canCreateWorktree: true }, + ], + workspaceKeys: ["host-b:ws-2"], + }), + ], + }); + + expect(shouldShowSidebarHostLabels(projects)).toBe(true); + }); + + it("is true for a single project shared across hosts", () => { + const projects = buildSidebarProjectsFromStructure({ + projects: [ + project({ + projectKey: "getpaseo/paseo", + hosts: [ + { serverId: "host-a", iconWorkingDir: "/repo/paseo", canCreateWorktree: true }, + { serverId: "host-b", iconWorkingDir: "/repo/paseo", canCreateWorktree: true }, + ], + workspaceKeys: ["host-a:main", "host-b:feature"], + }), + ], + }); + + expect(shouldShowSidebarHostLabels(projects)).toBe(true); + }); +}); + describe("computeSidebarOrderUpdates", () => { it("returns no updates when there are no visible projects", () => { const updates = computeSidebarOrderUpdates({ diff --git a/packages/app/src/hooks/sidebar-workspaces-view-model.ts b/packages/app/src/hooks/sidebar-workspaces-view-model.ts index 393c20c0d..dccf4e9e3 100644 --- a/packages/app/src/hooks/sidebar-workspaces-view-model.ts +++ b/packages/app/src/hooks/sidebar-workspaces-view-model.ts @@ -351,6 +351,20 @@ export function buildSidebarProjectsFromHostProjects(input: { })); } +// Host labels disambiguate which machine a workspace lives on; they only earn their +// space once the visible sidebar spans more than one host. Counting distinct hosts +// across the visible projects (not all connected hosts) keeps labels off when a host +// filter pins the view to a single host. +export function shouldShowSidebarHostLabels(projects: SidebarProjectEntry[]): boolean { + const serverIds = new Set(); + for (const project of projects) { + for (const host of project.hosts) { + serverIds.add(host.serverId); + } + } + return serverIds.size >= 2; +} + export function applyStoredOrdering(input: { items: T[]; storedOrder: string[]; diff --git a/packages/app/src/hooks/use-sidebar-workspaces-list.ts b/packages/app/src/hooks/use-sidebar-workspaces-list.ts index af3def5b3..d02f8a945 100644 --- a/packages/app/src/hooks/use-sidebar-workspaces-list.ts +++ b/packages/app/src/hooks/use-sidebar-workspaces-list.ts @@ -31,6 +31,7 @@ export { computeSidebarOrderUpdates, createSidebarWorkspaceEntry, deriveSidebarLoadingState, + shouldShowSidebarHostLabels, type SidebarLoadingState, type SidebarOrderUpdates, type SidebarStatusWorkspacePlacement,