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
@@ -9,11 +9,11 @@ import {
expectProjectWorkspaceCountForHost,
expectSeparateProjects,
openGroupedProjectSettings,
openProjectsForSettingsHost,
openProjectDirectory,
openProjectDirectoryWithHosts,
renameProject,
selectWorkspaceHost,
switchProjectSettingsHost,
} from "./helpers/project-grouping";
import { type IsolatedHostDaemon, startIsolatedHostDaemon } from "./helpers/isolated-host-daemon";
import { connectSeedClient, type SeedDaemonClient } from "./helpers/seed-client";
@@ -380,10 +380,18 @@ test.describe("Sidebar project grouping", () => {
test("renames only the selected host's grouped project", async ({ page, crossHostProject }) => {
await openScenario(page, crossHostProject);
await openGroupedProjectSettings(page, GROUPED_PROJECT_NAME);
await openGroupedProjectSettings(page, {
serverId: getServerId(),
projectName: GROUPED_PROJECT_NAME,
});
await renameProject(page, "Primary-only project name");
await expectProjectSettingsName(page, "Primary-only project name");
await switchProjectSettingsHost(page, SECONDARY_HOST_LABEL);
const secondaryHost = crossHostProject.hosts[0];
if (!secondaryHost) throw new Error("Expected a secondary host");
await openProjectsForSettingsHost(page, {
serverId: secondaryHost.serverId,
projectName: GROUPED_PROJECT_NAME,
});
await expectProjectSettingsName(page, GROUPED_PROJECT_NAME);
});