From 805a0e11beef6caceaa4815c51d3bb2c908cb911 Mon Sep 17 00:00:00 2001 From: flamboh Date: Mon, 2 Nov 2026 08:43:14 +0000 Subject: [PATCH 1/2] fix(web): project groups use the first checkout icon they have --- .../ProjectFavicon.browser.test.tsx | 47 +++++++++++++++-- apps/web/src/components/ProjectFavicon.tsx | 51 +++++++++++++++++-- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/ProjectFavicon.browser.test.tsx b/apps/web/src/components/ProjectFavicon.browser.test.tsx index 746302ef5b24..74166b10df61 100644 --- a/apps/web/src/components/ProjectFavicon.browser.test.tsx +++ b/apps/web/src/components/ProjectFavicon.browser.test.tsx @@ -6,15 +6,20 @@ import { expect, it, vi } from "vite-plus/test"; import { render } from "../../test/render"; import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon"; -const state = vi.hoisted(() => ({ favicon: undefined as Atom.Atom | undefined })); +const state = vi.hoisted(() => ({ + favicon: undefined as Atom.Atom | undefined, + faviconByCwd: new Map>(), +})); vi.mock(import("../state/assets"), () => ({ - projectFaviconUrlAtom: () => { - if (!state.favicon) throw new Error("Favicon resource not configured"); - return state.favicon; + projectFaviconUrlAtom: ({ cwd }) => { + const favicon = state.faviconByCwd.get(cwd) ?? state.favicon; + if (!favicon) throw new Error("Favicon resource not configured"); + return favicon; }, })); function setup() { + state.faviconByCwd.clear(); const activeResources = new Set(); state.favicon = Atom.make((get) => { activeResources.add("favicon"); @@ -55,3 +60,37 @@ it("only activates the favicon resource after removing a custom icon", async () registry.dispose(); } }); + +it("shows a group's icon from its first checkout that has one", async () => { + setup(); + const faviconUrl = "data:image/png;base64,iVBORw0KGgo="; + state.faviconByCwd.set("/server", Atom.make(null)); + state.faviconByCwd.set("/t3code", Atom.make(faviconUrl)); + const primary: ProjectFaviconProject = { + environmentId: EnvironmentId.make("primary"), + workspaceRoot: "/server", + title: "server", + faviconPath: null, + projectIcon: null, + }; + const secondary: ProjectFaviconProject = { + environmentId: EnvironmentId.make("secondary"), + workspaceRoot: "/t3code", + title: "t3code", + faviconPath: null, + projectIcon: null, + }; + const registry = AtomRegistry.make(); + try { + await using screen = await render( + + + , + ); + await vi.waitFor(() => + expect(screen.container.querySelector("img")?.getAttribute("src")).toBe(faviconUrl), + ); + } finally { + registry.dispose(); + } +}); diff --git a/apps/web/src/components/ProjectFavicon.tsx b/apps/web/src/components/ProjectFavicon.tsx index f0e6e750bf38..b5372a64e8af 100644 --- a/apps/web/src/components/ProjectFavicon.tsx +++ b/apps/web/src/components/ProjectFavicon.tsx @@ -31,12 +31,23 @@ function DynamicProjectIconFallback() { export type ProjectFaviconProject = Pick< EnvironmentProject, "environmentId" | "workspaceRoot" | "title" | "faviconPath" | "projectIcon" ->; -export function ProjectFavicon(input: { +> & { + readonly memberProjects?: ReadonlyArray; +}; +type ProjectFaviconProps = { project: ProjectFaviconProject; className?: string | undefined; fallbackIcon?: ComponentType<{ className?: string }>; -}) { +}; +export function ProjectFavicon(input: ProjectFaviconProps) { + const members = input.project.memberProjects; + if (members && members.length > 1) { + return ; + } + return ; +} + +function CheckoutFavicon(input: ProjectFaviconProps) { const { project } = input; if (project.projectIcon?.kind === "monogram") { return ( @@ -77,7 +88,39 @@ export function ProjectFavicon(input: { return ; } -function AutomaticProjectFavicon(input: Parameters[0]) { +type ProjectGroupFaviconProps = ProjectFaviconProps & { + members: ReadonlyArray; + index: number; +}; + +function ProjectGroupFavicon(input: ProjectGroupFaviconProps) { + const { members, index, ...props } = input; + const member = members[index]; + if (!member) return ; + if (member.projectIcon || member.faviconPath) { + return ; + } + return ; +} + +function ProjectGroupAutomaticFavicon( + input: ProjectGroupFaviconProps & { member: ProjectFaviconProject }, +) { + const { members, index, member, ...props } = input; + const src = useAtomValue( + projectFaviconUrlAtom({ + environmentId: member.environmentId, + cwd: member.workspaceRoot, + faviconPath: member.faviconPath, + }), + ); + if (src && !isProjectFaviconFallbackUrl(src)) { + return ; + } + return ; +} + +function AutomaticProjectFavicon(input: ProjectFaviconProps) { const { project } = input; const src = useAtomValue( projectFaviconUrlAtom({ From ca3bc8a7984138362bc05489a68e2014db584427 Mon Sep 17 00:00:00 2001 From: flamboh Date: Mon, 2 Nov 2026 09:11:21 +0000 Subject: [PATCH 2/2] fix(web): project settings icon preview follows the group's icon --- apps/web/src/components/settings/ProjectSettingsPanel.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/settings/ProjectSettingsPanel.tsx b/apps/web/src/components/settings/ProjectSettingsPanel.tsx index 9255c2f9b52d..accf600f7912 100644 --- a/apps/web/src/components/settings/ProjectSettingsPanel.tsx +++ b/apps/web/src/components/settings/ProjectSettingsPanel.tsx @@ -492,7 +492,10 @@ function ProjectDetail({ } control={
- +