Keep selected projects when switching hosts (#2700)

* fix(app): preserve project selection across hosts

Rendered project identity and persisted equivalence were conflated after duplicate-project handling. Keep them separate so host changes resolve selectable equivalents deterministically without resetting legacy sidebar identity.

* fix(app): restore selected clone after host round trip

Keep manual origin and current destination placements separately so switching back restores the chosen checkout while archive gaps preserve the active host selection.

* fix(app): key status icons by project view

* fix(app): preserve origin during archive gaps
This commit is contained in:
Mohamed Boudra
2026-07-31 15:42:08 +02:00
committed by GitHub
parent 87ef631ac4
commit 99eca19a76
56 changed files with 1234 additions and 428 deletions
+8 -8
View File
@@ -77,14 +77,14 @@ interface SidebarSharedProps {
pinnedGroups: PinnedSidebarGroups;
projects: SidebarProjectEntry[];
workspaceEntriesByKey: ReadonlyMap<string, SidebarWorkspaceEntry>;
projectNamesByKey: Map<string, string>;
projectNamesByViewKey: Map<string, string>;
isInitialLoad: boolean;
isRevalidating: boolean;
isManualRefresh: boolean;
groupMode: SidebarGroupMode;
collapsedProjectKeys: ReadonlySet<string>;
shortcutIndexByWorkspaceKey: Map<string, number>;
toggleProjectCollapsed: (projectKey: string) => void;
toggleProjectCollapsed: (projectViewKey: string) => void;
handleRefresh: () => void;
handleOpenProject: () => void;
handleHome: () => void;
@@ -132,7 +132,7 @@ export const LeftSidebar = memo(function LeftSidebar({ active }: { active: boole
const {
projects,
workspaceEntriesByKey,
projectNamesByKey,
projectNamesByViewKey,
isInitialLoad,
isRevalidating,
refreshAll,
@@ -238,7 +238,7 @@ export const LeftSidebar = memo(function LeftSidebar({ active }: { active: boole
pinnedGroups,
projects,
workspaceEntriesByKey,
projectNamesByKey,
projectNamesByViewKey,
isInitialLoad,
isRevalidating,
isManualRefresh,
@@ -592,7 +592,7 @@ function MobileSidebar({
pinnedGroups,
projects,
workspaceEntriesByKey,
projectNamesByKey,
projectNamesByViewKey,
isInitialLoad,
isRevalidating,
isManualRefresh,
@@ -708,7 +708,7 @@ function MobileSidebar({
pinnedGroups={pinnedGroups}
projects={projects}
workspaceEntriesByKey={workspaceEntriesByKey}
projectNamesByKey={projectNamesByKey}
projectNamesByViewKey={projectNamesByViewKey}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
onWorkspacePress={handleWorkspacePress}
@@ -738,7 +738,7 @@ function DesktopSidebar({
pinnedGroups,
projects,
workspaceEntriesByKey,
projectNamesByKey,
projectNamesByViewKey,
isInitialLoad,
isRevalidating,
isManualRefresh,
@@ -875,7 +875,7 @@ function DesktopSidebar({
pinnedGroups={pinnedGroups}
projects={projects}
workspaceEntriesByKey={workspaceEntriesByKey}
projectNamesByKey={projectNamesByKey}
projectNamesByViewKey={projectNamesByViewKey}
isRefreshing={isManualRefresh && isRevalidating}
onRefresh={handleRefresh}
onAddProject={handleOpenProject}
@@ -15,22 +15,22 @@ const WHITE_TEXT = { color: "#ffffff" } as const;
export function ProjectIconView({
iconDataUri,
initial,
projectKey,
projectViewKey,
imageStyle,
fallbackStyle,
textStyle,
}: {
iconDataUri: string | null;
initial: string;
projectKey: string;
projectViewKey: string;
imageStyle: StyleProp<ImageStyle>;
fallbackStyle: StyleProp<ViewStyle>;
textStyle: StyleProp<TextStyle>;
}) {
const imageSource = useMemo(() => ({ uri: iconDataUri ?? "" }), [iconDataUri]);
const fallbackStyles = useMemo(
() => [fallbackStyle, { backgroundColor: deriveProjectIconColor(projectKey) }],
[fallbackStyle, projectKey],
() => [fallbackStyle, { backgroundColor: deriveProjectIconColor(projectViewKey) }],
[fallbackStyle, projectViewKey],
);
const textStyles = useMemo(() => [textStyle, WHITE_TEXT], [textStyle]);
@@ -8,6 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import React from "react";
import type { ReactElement } from "react";
import { createProjectViewKey } from "@/projects/workspace-structure";
vi.hoisted(() => {
(globalThis as unknown as { __DEV__: boolean }).__DEV__ = false;
@@ -196,7 +197,7 @@ function ProjectHeaderProbe({
project: SidebarProjectEntry;
counts: RenderCounts;
}): null {
incrementRecord(counts.headers, project.projectKey);
incrementRecord(counts.headers, project.viewKey);
return null;
}
@@ -232,7 +233,7 @@ function ProjectActiveProbe({
activeSelection?.serverId === serverId &&
project.workspaces.some((entry) => entry.workspaceId === activeSelection.workspaceId);
void isActive;
incrementRecord(counts.projectSelection, project.projectKey);
incrementRecord(counts.projectSelection, project.viewKey);
return null;
}
@@ -260,7 +261,7 @@ function SidebarFrameProbe({ counts }: { counts: RenderCounts }): ReactElement {
return (
<>
{projects.map((project) => (
<div key={project.projectKey}>
<div key={project.viewKey}>
<ProjectHeaderProbe project={project} counts={counts} />
<ProjectActiveProbe serverId={SERVER_ID} project={project} counts={counts} />
{project.workspaces.map((entry) => (
@@ -453,8 +454,8 @@ describe("sidebar workspace render isolation", () => {
expect(counts.frame).toBe(1);
expect(counts.projectSelection).toEqual({
"project-a": 1,
"project-b": 1,
[createProjectViewKey({ kind: "equivalence", projectKey: "project-a" })]: 1,
[createProjectViewKey({ kind: "equivalence", projectKey: "project-b" })]: 1,
});
expect(counts.rowSelection).toEqual({
"a-main": 1,
@@ -57,7 +57,7 @@ import {
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
import { useHostFeatureMap } from "@/runtime/host-features";
import { useIsCompactFormFactor } from "@/constants/layout";
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
import { useProjectIconDataByProjectViewKey } from "@/projects/project-icons";
import {
buildNewWorkspaceRoute,
buildProjectSettingsRoute,
@@ -137,7 +137,7 @@ import { useLocalDaemonServerId } from "@/hooks/use-is-local-daemon";
const workspaceKeyExtractor = (workspace: SidebarWorkspacePlacement) => workspace.workspaceKey;
const projectKeyExtractor = (project: SidebarProjectEntry) => project.projectKey;
const projectViewKeyExtractor = (project: SidebarProjectEntry) => project.viewKey;
const WORKSPACE_STATUS_DOT_WIDTH = 14;
const DEFAULT_STATUS_DOT_SIZE = 7;
@@ -234,9 +234,9 @@ interface SidebarWorkspaceListProps {
pinnedGroups: PinnedSidebarGroups;
projects: SidebarProjectEntry[];
workspaceEntriesByKey: ReadonlyMap<string, SidebarWorkspaceEntry>;
projectNamesByKey: Map<string, string>;
projectNamesByViewKey: Map<string, string>;
collapsedProjectKeys: ReadonlySet<string>;
onToggleProjectCollapsed: (projectKey: string) => void;
onToggleProjectCollapsed: (projectViewKey: string) => void;
shortcutIndexByWorkspaceKey: Map<string, number>;
groupMode: "project" | "status";
isRefreshing?: boolean;
@@ -431,7 +431,7 @@ function ProjectLeadingVisual({
displayName,
iconDataUri,
workspace,
projectKey,
projectViewKey,
chevron = null,
showChevron = false,
isArchiving = false,
@@ -439,7 +439,7 @@ function ProjectLeadingVisual({
displayName: string;
iconDataUri: string | null;
workspace: SidebarWorkspaceEntry | null;
projectKey: string;
projectViewKey: string;
chevron?: "expand" | "collapse" | null;
showChevron?: boolean;
isArchiving?: boolean;
@@ -467,7 +467,7 @@ function ProjectLeadingVisual({
<ProjectIcon
iconDataUri={iconDataUri}
placeholderInitial={placeholderInitial}
projectKey={projectKey}
projectViewKey={projectViewKey}
/>
</View>
);
@@ -477,7 +477,7 @@ function ProjectLeadingVisual({
<ProjectLeadingVisualStatus
iconDataUri={iconDataUri}
placeholderInitial={placeholderInitial}
projectKey={projectKey}
projectViewKey={projectViewKey}
isArchiving={isArchiving}
shouldShowSyncedLoader={shouldShowSyncedLoader}
activeWorkspace={activeWorkspace}
@@ -518,7 +518,7 @@ function ProjectRowTrailingActions({
onPress={onBeginWorkspaceSetup}
visible={actionsVisible}
showShortcutHint={isProjectActive}
testID={`sidebar-project-new-worktree-${project.projectKey}`}
testID={`sidebar-project-new-worktree-${project.viewKey}`}
/>
) : null}
{onRemoveProject ? (
@@ -527,7 +527,7 @@ function ProjectRowTrailingActions({
pointerEvents={actionsVisible ? "auto" : "none"}
>
<ProjectKebabMenu
projectKey={project.projectKey}
projectViewKey={project.viewKey}
settingsTarget={settingsTarget}
projectPath={localProjectPath}
onRemoveProject={onRemoveProject}
@@ -555,13 +555,13 @@ function renderKebabTriggerIcon({ hovered }: { hovered?: boolean }) {
}
function ProjectKebabMenu({
projectKey,
projectViewKey,
settingsTarget,
projectPath,
onRemoveProject,
removeProjectStatus,
}: {
projectKey: string;
projectViewKey: string;
settingsTarget: { serverId: string; projectId: string } | null;
projectPath: string;
onRemoveProject: () => void;
@@ -595,14 +595,14 @@ function ProjectKebabMenu({
style={projectKebabStyle}
accessibilityRole={platformIsWeb ? undefined : "button"}
accessibilityLabel={t("sidebar.project.actions.menu")}
testID={`sidebar-project-kebab-${projectKey}`}
testID={`sidebar-project-kebab-${projectViewKey}`}
>
{renderKebabTriggerIcon}
</DropdownMenuTrigger>
<DropdownMenuContent align="end" width={220}>
{canOpenProjectSettings ? (
<DropdownMenuItem
testID={`sidebar-project-menu-open-settings-${projectKey}`}
testID={`sidebar-project-menu-open-settings-${projectViewKey}`}
leading={settingsLeadingIcon}
onSelect={handleOpenProjectSettings}
>
@@ -611,7 +611,7 @@ function ProjectKebabMenu({
) : null}
{canOpenInNewWindow ? (
<DropdownMenuItem
testID={`sidebar-project-menu-open-new-window-${projectKey}`}
testID={`sidebar-project-menu-open-new-window-${projectViewKey}`}
leading={openInNewWindowLeadingIcon}
onSelect={handleOpenInNewWindow}
>
@@ -620,10 +620,10 @@ function ProjectKebabMenu({
) : null}
<OpenInFileManagerMenuItem
path={projectPath}
testID={`sidebar-project-menu-open-folder-${projectKey}`}
testID={`sidebar-project-menu-open-folder-${projectViewKey}`}
/>
<DropdownMenuItem
testID={`sidebar-project-menu-remove-${projectKey}`}
testID={`sidebar-project-menu-remove-${projectViewKey}`}
leading={trash2LeadingIcon}
status={removeProjectStatus}
pendingLabel={t("sidebar.project.actions.removing")}
@@ -725,17 +725,17 @@ function WorkspaceRowRightGroup({
function ProjectIcon({
iconDataUri,
placeholderInitial,
projectKey,
projectViewKey,
}: {
iconDataUri: string | null;
placeholderInitial: string;
projectKey: string;
projectViewKey: string;
}) {
return (
<ProjectIconView
iconDataUri={iconDataUri}
initial={placeholderInitial}
projectKey={projectKey}
projectViewKey={projectViewKey}
imageStyle={styles.projectIcon}
fallbackStyle={styles.projectIconFallback}
textStyle={styles.projectIconFallbackText}
@@ -746,14 +746,14 @@ function ProjectIcon({
function ProjectLeadingVisualStatus({
iconDataUri,
placeholderInitial,
projectKey,
projectViewKey,
isArchiving,
shouldShowSyncedLoader,
activeWorkspace,
}: {
iconDataUri: string | null;
placeholderInitial: string;
projectKey: string;
projectViewKey: string;
isArchiving: boolean;
shouldShowSyncedLoader: boolean;
activeWorkspace: SidebarWorkspaceEntry;
@@ -796,7 +796,7 @@ function ProjectLeadingVisualStatus({
<ProjectIcon
iconDataUri={iconDataUri}
placeholderInitial={placeholderInitial}
projectKey={projectKey}
projectViewKey={projectViewKey}
/>
{dotColorStyle ? (
<StatusDotOverlay
@@ -936,7 +936,7 @@ function NewWorkspaceGhostRow({
})}
onPress={handlePress}
style={rowStyle}
testID={`sidebar-project-new-workspace-row-${project.projectKey}`}
testID={`sidebar-project-new-workspace-row-${project.viewKey}`}
>
{({ hovered, pressed }) => (
<>
@@ -1040,7 +1040,7 @@ function ProjectHeaderRow({
displayName={displayName}
iconDataUri={iconDataUri}
workspace={workspace}
projectKey={project.projectKey}
projectViewKey={project.viewKey}
chevron={chevron}
showChevron={isHovered && chevron !== null}
isArchiving={isArchiving}
@@ -1088,7 +1088,7 @@ function ProjectHeaderRow({
onTouchMove={interaction.handleTouchMove}
onPressOut={interaction.handlePressOut}
onPress={handlePress}
testID={`sidebar-project-row-${project.projectKey}`}
testID={`sidebar-project-row-${project.viewKey}`}
>
{rowChildren}
</ContextMenuTrigger>
@@ -1111,7 +1111,7 @@ function ProjectHeaderRow({
onTouchMove={interaction.handleTouchMove}
onPressOut={interaction.handlePressOut}
onPress={handlePress}
testID={`sidebar-project-row-${project.projectKey}`}
testID={`sidebar-project-row-${project.viewKey}`}
>
{rowChildren}
</Pressable>
@@ -1622,9 +1622,9 @@ function ProjectBlock({
showShortcutBadges: boolean;
shortcutIndexByWorkspaceKey: Map<string, number>;
parentGestureRef?: MutableRefObject<GestureType | undefined>;
onToggleCollapsed: (projectKey: string) => void;
onToggleCollapsed: (projectViewKey: string) => void;
onWorkspacePress?: () => void;
onWorkspaceReorder: (projectKey: string, workspaces: SidebarWorkspacePlacement[]) => void;
onWorkspaceReorder: (projectViewKey: string, workspaces: SidebarWorkspacePlacement[]) => void;
onWorktreeCreated?: (workspaceId: string) => void;
drag: () => void;
isDragging: boolean;
@@ -1725,9 +1725,9 @@ function ProjectBlock({
const handleWorkspaceDragEnd = useCallback(
(workspaces: SidebarWorkspacePlacement[]) => {
onWorkspaceReorder(project.projectKey, workspaces);
onWorkspaceReorder(project.viewKey, workspaces);
},
[onWorkspaceReorder, project.projectKey],
[onWorkspaceReorder, project.viewKey],
);
const toast = useToast();
@@ -1753,7 +1753,6 @@ function ProjectBlock({
setIsRemovingProject(true);
const readiness = getCurrentProjectRemoveReadiness({
projectKey: project.projectKey,
hosts: project.hosts,
});
if (readiness.kind === "needs_host_update") {
@@ -1785,11 +1784,11 @@ function ProjectBlock({
setIsRemovingProject(false);
});
})();
}, [isRemovingProject, displayName, t, toast, project.projectKey, project.hosts]);
}, [isRemovingProject, displayName, t, toast, project.hosts]);
const handleToggleCollapsed = useCallback(() => {
onToggleCollapsed(project.projectKey);
}, [onToggleCollapsed, project.projectKey]);
onToggleCollapsed(project.viewKey);
}, [onToggleCollapsed, project.viewKey]);
let projectChildren = null;
if (!collapsed) {
@@ -1797,7 +1796,7 @@ function ProjectBlock({
projectChildren = (
<>
<DraggableList
testID={`sidebar-workspace-list-${project.projectKey}`}
testID={`sidebar-workspace-list-${project.viewKey}`}
data={visibleWorkspaces}
keyExtractor={workspaceKeyExtractor}
renderItem={renderWorkspace}
@@ -1813,7 +1812,7 @@ function ProjectBlock({
<SidebarGroupToggleRow
expanded={workspacesExpanded}
onPress={toggleWorkspacesExpanded}
testID={`sidebar-project-show-more-${project.projectKey}`}
testID={`sidebar-project-show-more-${project.viewKey}`}
/>
) : null}
</>
@@ -1925,7 +1924,7 @@ export function SidebarWorkspaceList({
pinnedGroups,
projects,
workspaceEntriesByKey,
projectNamesByKey,
projectNamesByViewKey,
collapsedProjectKeys,
onToggleProjectCollapsed,
shortcutIndexByWorkspaceKey,
@@ -1959,7 +1958,7 @@ export function SidebarWorkspaceList({
() => (groupMode === "status" ? resolveSidebarProjectIconTargets(projects) : []),
[groupMode, projects],
);
const statusProjectIconByProjectKey = useProjectIconDataByProjectKey({
const statusProjectIconByProjectViewKey = useProjectIconDataByProjectViewKey({
projects: statusProjectIconTargets,
});
@@ -1969,8 +1968,8 @@ export function SidebarWorkspaceList({
statusGroups={statusGroups}
pinnedGroups={pinnedGroups}
workspaceEntriesByKey={workspaceEntriesByKey}
projectNamesByKey={projectNamesByKey}
projectIconByProjectKey={statusProjectIconByProjectKey}
projectNamesByViewKey={projectNamesByViewKey}
projectIconByProjectViewKey={statusProjectIconByProjectViewKey}
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
onWorkspacePress={onWorkspacePress}
hostLabelByServerId={hostLabelByServerId}
@@ -2008,8 +2007,8 @@ function SidebarStatusModeWrapper({
statusGroups,
pinnedGroups,
workspaceEntriesByKey,
projectNamesByKey,
projectIconByProjectKey,
projectNamesByViewKey,
projectIconByProjectViewKey,
shortcutIndexByWorkspaceKey: _projectShortcutIndex,
onWorkspacePress,
hostLabelByServerId,
@@ -2021,8 +2020,8 @@ function SidebarStatusModeWrapper({
statusGroups: StatusGroup[];
pinnedGroups: PinnedSidebarGroups;
workspaceEntriesByKey: ReadonlyMap<string, SidebarWorkspaceEntry>;
projectNamesByKey: Map<string, string>;
projectIconByProjectKey: ReadonlyMap<string, string | null>;
projectNamesByViewKey: Map<string, string>;
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
shortcutIndexByWorkspaceKey: Map<string, number>;
onWorkspacePress?: () => void;
hostLabelByServerId: ReadonlyMap<string, string>;
@@ -2040,8 +2039,8 @@ function SidebarStatusModeWrapper({
const entry = workspaceEntriesByKey.get(workspace.workspaceKey);
return entry ? [entry] : [];
})}
projectNamesByKey={projectNamesByKey}
projectIconByProjectKey={projectIconByProjectKey}
projectNamesByViewKey={projectNamesByViewKey}
projectIconByProjectViewKey={projectIconByProjectViewKey}
shortcutIndexByWorkspaceKey={_projectShortcutIndex}
showShortcutBadges={showShortcutBadges}
onWorkspacePress={onWorkspacePress}
@@ -2074,7 +2073,7 @@ function ProjectModeList({
onToggleWorkspacePin,
}: Omit<
SidebarWorkspaceListProps,
"statusGroups" | "projectNamesByKey" | "groupMode" | "isRefreshing" | "onRefresh"
"statusGroups" | "projectNamesByViewKey" | "groupMode" | "isRefreshing" | "onRefresh"
> & {
pathname: string;
hostLabelByServerId: ReadonlyMap<string, string>;
@@ -2128,7 +2127,7 @@ function ProjectModeList({
[parentGestureRef],
);
const projectIconByProjectKey = useProjectIconDataByProjectKey({
const projectIconByProjectViewKey = useProjectIconDataByProjectViewKey({
projects: projectIconTargets,
});
@@ -2180,7 +2179,7 @@ function ProjectModeList({
const handleProjectDragEnd = useCallback(
(reorderedProjects: SidebarProjectEntry[]) => {
const reorderedProjectKeys = reorderedProjects.map((project) => project.projectKey);
const reorderedProjectKeys = reorderedProjects.map((project) => project.viewKey);
const currentProjectOrder = getProjectOrder();
if (
!hasVisibleOrderChanged({
@@ -2202,9 +2201,9 @@ function ProjectModeList({
);
const handleWorkspaceReorder = useCallback(
(projectKey: string, reorderedWorkspaces: SidebarWorkspacePlacement[]) => {
(projectViewKey: string, reorderedWorkspaces: SidebarWorkspacePlacement[]) => {
const reorderedWorkspaceKeys = reorderedWorkspaces.map((workspace) => workspace.workspaceKey);
const currentWorkspaceOrder = getWorkspaceOrder(projectKey);
const currentWorkspaceOrder = getWorkspaceOrder(projectViewKey);
if (
!hasVisibleOrderChanged({
currentOrder: currentWorkspaceOrder,
@@ -2215,7 +2214,7 @@ function ProjectModeList({
}
setWorkspaceOrder(
projectKey,
projectViewKey,
mergeWithRemainder({
currentOrder: currentWorkspaceOrder,
reorderedVisibleKeys: reorderedWorkspaceKeys,
@@ -2262,12 +2261,12 @@ function ProjectModeList({
) => {
return (
<MemoProjectBlock
key={item.projectKey}
key={item.viewKey}
project={item}
workspaceEntriesByKey={workspaceEntriesByKey}
collapsed={collapsedProjectKeys.has(item.projectKey)}
collapsed={collapsedProjectKeys.has(item.viewKey)}
displayName={item.projectName}
iconDataUri={projectIconByProjectKey.get(item.projectKey) ?? null}
iconDataUri={projectIconByProjectViewKey.get(item.viewKey) ?? null}
selectionEnabled={selectionEnabled}
showShortcutBadges={showShortcutBadges}
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
@@ -2303,7 +2302,7 @@ function ProjectModeList({
onWorkspacePress,
onToggleProjectCollapsed,
parentGestureRef,
projectIconByProjectKey,
projectIconByProjectViewKey,
selectionEnabled,
shortcutIndexByWorkspaceKey,
showShortcutBadges,
@@ -2392,7 +2391,7 @@ function ProjectModeList({
<DraggableList
testID="sidebar-project-list"
data={unpinnedProjects}
keyExtractor={projectKeyExtractor}
keyExtractor={projectViewKeyExtractor}
renderItem={renderProject}
onDragEnd={handleProjectDragEnd}
extraData={activeWorkspaceSelectionKey(activeWorkspaceSelection)}
@@ -18,7 +18,7 @@ interface SidebarModel extends SidebarWorkspacesListResult {
statusGroups: StatusGroup[];
pinnedGroups: PinnedSidebarGroups;
collapsedProjectKeys: ReadonlySet<string>;
toggleProjectCollapsed: (projectKey: string) => void;
toggleProjectCollapsed: (projectViewKey: string) => void;
shortcutModel: SidebarShortcutModel;
}
@@ -59,7 +59,7 @@ export function SidebarModelProvider({
projects: list.projects,
pinnedKeys,
workspaceEntriesByKey: projectionWorkspaceEntriesByKey,
projectNamesByKey: list.projectNamesByKey,
projectNamesByViewKey: list.projectNamesByViewKey,
groupMode,
pinnedCollapsed,
collapsedProjectKeys,
@@ -69,7 +69,7 @@ export function SidebarModelProvider({
collapsedProjectKeys,
collapsedStatusGroupKeys,
groupMode,
list.projectNamesByKey,
list.projectNamesByViewKey,
list.projects,
pinnedCollapsed,
pinnedKeys,
@@ -11,7 +11,7 @@ function makeWorkspace(id: string, statusBucket: SidebarWorkspaceEntry["statusBu
workspaceKey: `srv:${id}`,
serverId: "srv",
workspaceId: id,
projectKey: "project",
projectViewKey: "project",
projectName: "Project",
projectKind: "git",
workspaceKind: "worktree",
@@ -36,7 +36,7 @@ function makeWorkspace(id: string, statusBucket: SidebarWorkspaceEntry["statusBu
function makeProject(workspaces: SidebarWorkspacePlacement[]): SidebarProjectEntry {
return {
projectKey: "project",
viewKey: "project",
projectName: "Project",
projectKind: "git",
iconWorkingDir: "/repo",
@@ -68,7 +68,7 @@ function projectionInput(options?: {
[pinned.entry.workspaceKey, pinned.entry],
[unpinned.entry.workspaceKey, unpinned.entry],
]),
projectNamesByKey: new Map([["project", "Project"]]),
projectNamesByViewKey: new Map([["project", "Project"]]),
groupMode: options?.groupMode ?? ("project" as const),
pinnedCollapsed: options?.pinnedCollapsed ?? false,
collapsedProjectKeys: new Set<string>(),
@@ -25,7 +25,7 @@ export function buildSidebarProjection(input: {
projects: SidebarProjectEntry[];
pinnedKeys: PinnedSidebarKeys;
workspaceEntriesByKey: ReadonlyMap<string, SidebarWorkspaceEntry>;
projectNamesByKey: Map<string, string>;
projectNamesByViewKey: Map<string, string>;
groupMode: SidebarGroupMode;
pinnedCollapsed: boolean;
collapsedProjectKeys: ReadonlySet<string>;
@@ -42,7 +42,7 @@ export function buildSidebarProjection(input: {
Array.from(input.workspaceEntriesByKey.values()).filter(
(workspace) => !pinnedWorkspaceKeys.has(workspace.workspaceKey),
),
input.projectNamesByKey,
input.projectNamesByViewKey,
)
: [];
@@ -61,7 +61,7 @@ export function buildSidebarProjection(input: {
sections.push(
...pinnedGroups.unpinnedProjects.map((project) => ({
workspaces: project.workspaces,
collapsed: input.collapsedProjectKeys.has(project.projectKey),
collapsed: input.collapsedProjectKeys.has(project.viewKey),
})),
);
}
@@ -64,8 +64,8 @@ const ThemedCircleX = withUnistyles(CircleX);
interface StatusWorkspaceListProps {
groups: StatusGroup[];
pinnedWorkspaces: SidebarWorkspaceEntry[];
projectNamesByKey: Map<string, string>;
projectIconByProjectKey: ReadonlyMap<string, string | null>;
projectNamesByViewKey: Map<string, string>;
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
shortcutIndexByWorkspaceKey: Map<string, number>;
showShortcutBadges: boolean;
onWorkspacePress?: () => void;
@@ -79,8 +79,8 @@ interface StatusWorkspaceListProps {
export function SidebarStatusWorkspaceList({
groups,
pinnedWorkspaces,
projectNamesByKey,
projectIconByProjectKey,
projectNamesByViewKey,
projectIconByProjectViewKey,
shortcutIndexByWorkspaceKey,
showShortcutBadges,
onWorkspacePress,
@@ -118,8 +118,8 @@ export function SidebarStatusWorkspaceList({
workspace={workspace}
{...buildStatusRowProjectPresentation({
workspace,
projectNamesByKey,
projectIconByProjectKey,
projectNamesByViewKey,
projectIconByProjectViewKey,
hostLabelByServerId,
showHostLabels,
})}
@@ -145,8 +145,8 @@ export function SidebarStatusWorkspaceList({
<StatusGroupList
groups={groups}
collapsedStatusGroupKeys={collapsedStatusGroupKeys}
projectNamesByKey={projectNamesByKey}
projectIconByProjectKey={projectIconByProjectKey}
projectNamesByViewKey={projectNamesByViewKey}
projectIconByProjectViewKey={projectIconByProjectViewKey}
shortcutIndex={statusShortcutIndex}
showShortcutBadges={showShortcutBadges}
onWorkspacePress={onWorkspacePress}
@@ -186,8 +186,8 @@ export function SidebarStatusWorkspaceList({
function StatusGroupList({
groups,
collapsedStatusGroupKeys,
projectNamesByKey,
projectIconByProjectKey,
projectNamesByViewKey,
projectIconByProjectViewKey,
shortcutIndex,
showShortcutBadges,
onWorkspacePress,
@@ -198,8 +198,8 @@ function StatusGroupList({
}: {
groups: StatusGroup[];
collapsedStatusGroupKeys: ReadonlySet<string>;
projectNamesByKey: Map<string, string>;
projectIconByProjectKey: ReadonlyMap<string, string | null>;
projectNamesByViewKey: Map<string, string>;
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
shortcutIndex: Map<string, number>;
showShortcutBadges: boolean;
onWorkspacePress?: () => void;
@@ -215,8 +215,8 @@ function StatusGroupList({
key={group.bucket}
group={group}
collapsed={collapsedStatusGroupKeys.has(group.bucket)}
projectNamesByKey={projectNamesByKey}
projectIconByProjectKey={projectIconByProjectKey}
projectNamesByViewKey={projectNamesByViewKey}
projectIconByProjectViewKey={projectIconByProjectViewKey}
shortcutIndex={shortcutIndex}
showShortcutBadges={showShortcutBadges}
onWorkspacePress={onWorkspacePress}
@@ -233,8 +233,8 @@ function StatusGroupList({
function StatusGroupRows({
group,
collapsed,
projectNamesByKey,
projectIconByProjectKey,
projectNamesByViewKey,
projectIconByProjectViewKey,
shortcutIndex,
showShortcutBadges,
onWorkspacePress,
@@ -245,8 +245,8 @@ function StatusGroupRows({
}: {
group: StatusGroup;
collapsed: boolean;
projectNamesByKey: Map<string, string>;
projectIconByProjectKey: ReadonlyMap<string, string | null>;
projectNamesByViewKey: Map<string, string>;
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
shortcutIndex: Map<string, number>;
showShortcutBadges: boolean;
onWorkspacePress?: () => void;
@@ -276,8 +276,8 @@ function StatusGroupRows({
workspace={workspace}
{...buildStatusRowProjectPresentation({
workspace,
projectNamesByKey,
projectIconByProjectKey,
projectNamesByViewKey,
projectIconByProjectViewKey,
hostLabelByServerId,
showHostLabels,
})}
@@ -324,25 +324,25 @@ interface StatusRowProjectPresentation {
function buildStatusRowProjectPresentation({
workspace,
projectNamesByKey,
projectIconByProjectKey,
projectNamesByViewKey,
projectIconByProjectViewKey,
hostLabelByServerId,
showHostLabels,
}: {
workspace: SidebarWorkspaceEntry;
projectNamesByKey: Map<string, string>;
projectIconByProjectKey: ReadonlyMap<string, string | null>;
projectNamesByViewKey: Map<string, string>;
projectIconByProjectViewKey: ReadonlyMap<string, string | null>;
hostLabelByServerId: ReadonlyMap<string, string>;
showHostLabels: boolean;
}): StatusRowProjectPresentation {
const projectName = projectNamesByKey.get(workspace.projectKey) ?? "";
const projectName = projectNamesByViewKey.get(workspace.projectViewKey) ?? "";
const hostLabel = showHostLabels
? (hostLabelByServerId.get(workspace.serverId) ?? workspace.serverId)
: null;
return {
subtitle: buildStatusRowSubtitle({ projectName, hostLabel }),
projectName,
projectIconDataUri: projectIconByProjectKey.get(workspace.projectKey) ?? null,
projectIconDataUri: projectIconByProjectViewKey.get(workspace.projectViewKey) ?? null,
};
}
@@ -10,12 +10,12 @@ import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-disp
* the meaning, so the icon itself stays out of the accessibility tree.
*/
export function SidebarSubtitleProjectIcon({
projectKey,
projectViewKey,
projectName,
iconDataUri,
testID,
}: {
projectKey: string;
projectViewKey: string;
projectName: string;
iconDataUri: string | null;
testID?: string;
@@ -37,7 +37,7 @@ export function SidebarSubtitleProjectIcon({
<ProjectIconView
iconDataUri={iconDataUri}
initial={placeholderInitial}
projectKey={projectKey}
projectViewKey={projectViewKey}
imageStyle={styles.icon}
fallbackStyle={styles.fallback}
textStyle={styles.fallbackText}
@@ -157,7 +157,7 @@ export const SidebarWorkspaceRowContent = memo(function SidebarWorkspaceRowConte
<View style={styles.workspaceSubtitleRow}>
{subtitleProjectName ? (
<SidebarSubtitleProjectIcon
projectKey={workspace.projectKey}
projectViewKey={workspace.projectViewKey}
projectName={subtitleProjectName}
iconDataUri={subtitleProjectIconDataUri}
testID={`sidebar-row-project-icon-${workspace.workspaceKey}`}