From 9b6aa993967b94fbad72c7d3f99c2de07b71f31c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 28 Jun 2026 08:50:04 +0700 Subject: [PATCH] feat(sidebar): show host name on every row in multi-host setups (#1775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The host name previously appeared only in project grouping mode, and only for projects shared across hosts. It now shows on every workspace row in both project and status grouping modes whenever the sidebar spans more than one host, so you can always tell which machine a workspace lives on. Status rows read "Project · Host". Single-host setups are unchanged. --- .../src/components/sidebar-workspace-list.tsx | 24 ++++++-- .../sidebar/sidebar-status-list.tsx | 52 +++++++++++++---- .../sidebar-workspaces-view-model.test.ts | 58 +++++++++++++++++++ .../hooks/sidebar-workspaces-view-model.ts | 14 +++++ .../src/hooks/use-sidebar-workspaces-list.ts | 1 + 5 files changed, 135 insertions(+), 14 deletions(-) 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,