Fix project icon cache reuse in New Workspace (#3600)

* fix(app): reuse project icon cache in new workspace

New Workspace omitted each host placement's effective icon revision, so its icon queries could not share the sidebar's memory or persistent cache entries. Carry that revision through the target projection.

* refactor(app): centralize project icon targets

Project icon consumers constructed equivalent cache targets independently. One target factory now carries each host placement revision through the sidebar, New Workspace, Projects, and Project Settings.

* test(app): inline project icon fixture
This commit is contained in:
Mohamed Boudra
2026-08-20 13:15:58 +02:00
committed by GitHub
parent 4dbd26c88c
commit 975dbd433b
14 changed files with 182 additions and 76 deletions
+19 -5
View File
@@ -3,12 +3,18 @@ import { createProjectViewKey } from "@/projects/workspace-structure";
import type { ProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store";
import { buildProjects, getProjectSummaryForHostProject } from "./projects";
function descriptor(id: string, key: string, root: string): ProjectDescriptor {
function descriptor(
id: string,
key: string,
root: string,
iconRevision?: string,
): ProjectDescriptor {
return {
projectId: id,
projectKey: key,
projectDisplayName: "acme/app",
projectCustomName: null,
projectIconRevision: iconRevision,
projectRootPath: root,
projectKind: "git",
};
@@ -42,14 +48,14 @@ describe("buildProjects", () => {
serverId: "host-a",
serverName: "Host A",
isOnline: true,
projects: [descriptor("prj_a", key, "/a/app")],
projects: [descriptor("prj_a", key, "/a/app", "revision-a")],
workspaces: [workspace("ws-a", "prj_a", "/a/app")],
},
{
serverId: "host-b",
serverName: "Host B",
isOnline: false,
projects: [descriptor("prj_b", key, "/b/app")],
projects: [descriptor("prj_b", key, "/b/app", "revision-b")],
workspaces: [workspace("ws-b", "prj_b", "/b/app")],
},
],
@@ -62,8 +68,16 @@ describe("buildProjects", () => {
onlineHostCount: 1,
totalWorkspaceCount: 2,
hosts: [
expect.objectContaining({ serverId: "host-a", projectId: "prj_a" }),
expect.objectContaining({ serverId: "host-b", projectId: "prj_b" }),
expect.objectContaining({
serverId: "host-a",
projectId: "prj_a",
iconRevision: "revision-a",
}),
expect.objectContaining({
serverId: "host-b",
projectId: "prj_b",
iconRevision: "revision-b",
}),
],
}),
]);
+4
View File
@@ -25,6 +25,7 @@ export interface ProjectHostEntry {
gitRuntime?: WorkspaceDescriptor["gitRuntime"];
githubRuntime?: WorkspaceDescriptor["githubRuntime"];
customIconRevision?: string | null;
iconRevision?: string;
}
export interface ProjectSummary {
@@ -87,6 +88,7 @@ interface HostGroup {
isOnline: boolean;
workspaces: WorkspaceDescriptor[];
customIconRevision?: string | null;
iconRevision?: string;
// Repo root for a project parent that has no workspaces yet. Without it the
// host's repoRoot resolves to "" and the project reads as non-editable.
fallbackRepoRoot: string;
@@ -159,6 +161,7 @@ function toHostEntry(group: HostGroup): ProjectHostEntry {
gitRuntime: canonical?.gitRuntime,
githubRuntime: canonical?.githubRuntime,
customIconRevision: canonical?.projectCustomIconRevision ?? group.customIconRevision,
iconRevision: group.iconRevision,
};
}
@@ -223,6 +226,7 @@ function addHostProjects(
isOnline: host.isOnline,
workspaces: [],
customIconRevision: placement.customIconRevision,
iconRevision: placement.iconRevision,
fallbackRepoRoot: repoRootByProjectId.get(projectId) ?? "",
});
}
@@ -232,7 +232,12 @@ describe("buildSidebarProjectRowModel", () => {
project({
viewKey: '["placement","host-b","project-b"]',
hosts: [
{ serverId: "host-b", iconWorkingDir: "/repo/b", worktreeSupport: "supported" as const },
{
serverId: "host-b",
iconWorkingDir: "/repo/b",
worktreeSupport: "supported" as const,
iconRevision: "effective-revision",
},
],
}),
]);
@@ -242,6 +247,7 @@ describe("buildSidebarProjectRowModel", () => {
serverId: "host-b",
projectId: "project-host-b",
iconWorkingDir: "/repo/b",
iconRevision: "effective-revision",
});
});
@@ -1,4 +1,5 @@
import type { SidebarProjectEntry } from "@/hooks/use-sidebar-workspaces-list";
import { createProjectIconTarget, type ProjectIconTarget } from "@/projects/icon-target";
export interface SidebarProjectHostTarget {
serverId: string;
@@ -53,16 +54,17 @@ export function resolveSidebarProjectIconTarget(
return null;
}
export interface SidebarProjectIconTarget extends SidebarProjectHostTarget {
projectViewKey: string;
}
export type SidebarProjectIconTarget = ProjectIconTarget;
export function resolveSidebarProjectIconTargets(
projects: readonly SidebarProjectEntry[],
): SidebarProjectIconTarget[] {
return projects.flatMap((project) => {
const target = resolveSidebarProjectIconTarget(project);
return target ? [{ projectViewKey: project.viewKey, ...target }] : [];
const iconTarget = target
? createProjectIconTarget({ projectViewKey: project.viewKey, placement: target })
: null;
return iconTarget ? [iconTarget] : [];
});
}