feat: add host-local custom project icons (#2335)

* feat(projects): add local icons and colors

* refactor(projects): simplify appearance handling

* fix(projects): serialize appearance saves

* test(app): update project appearance expectations

* test: fix project appearance CI expectations

* refactor(projects): organize appearance code

* fix(server): adopt main's reconcile model in project appearance updates

* fix(projects): preserve host-local appearance targets

* fix(desktop): restore Linux development startup

Ubuntu can block Electron's unprivileged user-namespace fallback while the npm-installed sandbox helper is not SUID. Disable the Chromium process sandbox only in the Linux development runner so packaged desktop policy remains unchanged.

* feat(projects): add custom project icons

Keep URL acquisition in the client so the host daemon only validates uploaded bytes. Project name and icon changes share one adaptive edit sheet.

* refactor(settings): scope projects by host

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
nikuscs
2026-08-01 01:34:42 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 706ae3d9bb
commit 800bd2bef1
67 changed files with 2158 additions and 912 deletions
@@ -96,6 +96,7 @@ describe("openProjectDirectly", () => {
projectKey: null,
projectDisplayName: "project",
projectCustomName: null,
projectCustomIconRevision: null,
projectKind: "git",
projectRootPath: PROJECT_PATH,
},
@@ -193,6 +194,7 @@ describe("cloneGithubProjectDirectly", () => {
...projectPayload,
projectCustomName: null,
projectKey: null,
projectCustomIconRevision: null,
},
},
]);
@@ -1,49 +0,0 @@
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import type { ProjectIcon } from "@getpaseo/protocol/messages";
export function projectIconQueryKey(serverId: string, cwd: string) {
return ["projectIcon", serverId, cwd] as const;
}
export function projectIconToDataUri(icon: ProjectIcon | null): string | null {
if (!icon) {
return null;
}
return `data:${icon.mimeType};base64,${icon.data}`;
}
interface UseProjectIconQueryOptions {
serverId: string;
cwd: string;
}
export function useProjectIconQuery({ serverId, cwd }: UseProjectIconQueryOptions) {
const { t } = useTranslation();
const client = useHostRuntimeClient(serverId);
const isConnected = useHostRuntimeIsConnected(serverId);
const query = useQuery({
queryKey: projectIconQueryKey(serverId, cwd),
queryFn: async (): Promise<ProjectIcon | null> => {
if (!client) {
throw new Error(t("common.errors.daemonClientUnavailable"));
}
const result = await client.requestProjectIcon(cwd);
return result.icon;
},
enabled: !!client && isConnected && !!cwd,
staleTime: Infinity,
gcTime: 1000 * 60 * 60,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
return {
icon: query.data ?? null,
isLoading: query.isLoading,
isError: query.isError,
};
}