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
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { createProjectViewKey } from "@/projects/workspace-structure";
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import {
@@ -25,6 +26,10 @@ import {
const SERVER_ID = "test-server";
function equivalenceViewKey(projectKey: string): string {
return createProjectViewKey({ kind: "equivalence", projectKey });
}
function createWorkspace(
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
): WorkspaceDescriptor {
@@ -114,10 +119,10 @@ function emptySidebarOrder(): SidebarOrderSnapshot {
};
}
function selectWorkspaceStructureProjectKeys(
function selectWorkspaceStructureProjectViewKeys(
state: Parameters<typeof selectWorkspaceStructureProjects>[0],
): string[] {
return selectWorkspaceStructureProjects(state, [SERVER_ID]).map((project) => project.projectKey);
return selectWorkspaceStructureProjects(state, [SERVER_ID]).map((project) => project.viewKey);
}
afterEach(() => {
@@ -348,9 +353,11 @@ describe("workspace structure composition", () => {
};
initializeWorkspaces([workspace]);
const emittedProjectKeys = [selectWorkspaceStructureProjectKeys(useSessionStore.getState())];
const emittedProjectKeys = [
selectWorkspaceStructureProjectViewKeys(useSessionStore.getState()),
];
const stop = useSessionStore.subscribe((state) => {
emittedProjectKeys.push(selectWorkspaceStructureProjectKeys(state));
emittedProjectKeys.push(selectWorkspaceStructureProjectViewKeys(state));
});
try {
@@ -360,7 +367,10 @@ describe("workspace structure composition", () => {
stop();
}
expect(emittedProjectKeys).toEqual([["project-a"], ["project-a"]]);
expect(emittedProjectKeys).toEqual([
[equivalenceViewKey("project-a")],
[equivalenceViewKey("project-a")],
]);
});
it("changes for membership updates but not status-only updates", () => {
@@ -451,10 +461,13 @@ describe("workspace structure composition", () => {
const before = snapshotStructure(SERVER_ID, emptySidebarOrder());
const after = snapshotStructure(SERVER_ID, {
...emptySidebarOrder(),
projectOrder: ["project-b", "project-a"],
projectOrder: [equivalenceViewKey("project-b"), equivalenceViewKey("project-a")],
});
expect(after.projects.map((project) => project.projectKey)).toEqual(["project-b", "project-a"]);
expect(after.projects.map((project) => project.viewKey)).toEqual([
equivalenceViewKey("project-b"),
equivalenceViewKey("project-a"),
]);
expect(after).not.toEqual(before);
});
});
@@ -214,8 +214,7 @@ export function composeWorkspaceStructure(input: {
const orderedProjects = applyStoredOrdering({
items: input.projects.map((project) => {
const workspaceOrder =
input.workspaceOrderByScope[project.projectKey] ?? EMPTY_WORKSPACE_KEYS;
const workspaceOrder = input.workspaceOrderByScope[project.viewKey] ?? EMPTY_WORKSPACE_KEYS;
return {
...project,
workspaceKeys: applyStoredOrdering({
@@ -226,7 +225,7 @@ export function composeWorkspaceStructure(input: {
};
}),
storedOrder: input.projectOrder,
getKey: (project) => project.projectKey,
getKey: (project) => project.viewKey,
});
return { projects: orderedProjects };