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
+13 -12
View File
@@ -3,7 +3,6 @@ import { buildCreateAgentPreferences, buildSeededHost, TEST_HOST_LABEL } from ".
import { getServerId } from "./server-id";
import { expectAppRoute } from "./route-assertions";
import {
buildProjectsSettingsRoute,
buildSettingsHostSectionRoute,
buildSettingsRoute,
buildSettingsSectionRoute,
@@ -30,20 +29,21 @@ const SECTION_LABELS = {
about: "About",
} as const;
export type SettingsSection = keyof typeof SECTION_LABELS | "projects";
export type SettingsSection = keyof typeof SECTION_LABELS;
type HostSection = "connections" | "agents" | "workspaces" | "providers" | "usage" | "host";
type HostSection =
| "projects"
| "connections"
| "agents"
| "workspaces"
| "providers"
| "usage"
| "host";
export async function openSettingsSection(page: Page, section: SettingsSection): Promise<void> {
const sidebar = page.getByTestId("settings-sidebar");
await expect(sidebar).toBeVisible();
if (section === "projects") {
await page.getByTestId("settings-projects").click();
await expectAppRoute(page, buildProjectsSettingsRoute());
return;
}
await sidebar.getByRole("button", { name: SECTION_LABELS[section], exact: true }).click();
await expectAppRoute(page, buildSettingsSectionRoute(section));
}
@@ -143,8 +143,8 @@ export async function seedSavedSettingsHosts(
}
export async function selectSettingsHost(page: Page, serverId: string): Promise<void> {
await page.getByTestId("settings-host-picker").click();
await page.getByTestId(`settings-host-picker-item-${serverId}`).click();
await page.locator('[data-testid="settings-host-picker"]:visible').click();
await page.locator(`[data-testid="settings-host-picker-item-${serverId}"]:visible`).click();
}
export async function expectSettingsHostPickerLabel(page: Page, label: string): Promise<void> {
@@ -171,7 +171,7 @@ export async function expectSettingsSidebarHidden(page: Page): Promise<void> {
export async function expectSettingsSidebarSections(
page: Page,
sections: Array<Exclude<SettingsSection, "projects">>,
sections: SettingsSection[],
): Promise<void> {
const sidebar = page.getByTestId("settings-sidebar");
for (const section of sections) {
@@ -376,6 +376,7 @@ export async function expectRetiredSidebarSectionsAbsent(page: Page): Promise<vo
// Host group rows are now flat top-level sections (no drill-in).
await expect(sidebar.getByTestId("settings-host-section-connections")).toBeVisible();
await expect(sidebar.getByTestId("settings-host-section-projects")).toBeVisible();
await expect(sidebar.getByTestId("settings-host-section-agents")).toBeVisible();
await expect(sidebar.getByTestId("settings-host-section-workspaces")).toBeVisible();
await expect(sidebar.getByTestId("settings-host-section-providers")).toBeVisible();