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
@@ -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}`}