diff --git a/crates/domain/src/settings.rs b/crates/domain/src/settings.rs
index 2dbbaecb..901a9450 100644
--- a/crates/domain/src/settings.rs
+++ b/crates/domain/src/settings.rs
@@ -38,6 +38,8 @@ pub struct IdeaUserSettings {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProjectIconSet {
+ /// Unicode emoji catalogue.
+ Emoji,
/// Devicon language and platform catalogue.
Devicon,
/// Simple Icons brand catalogue.
diff --git a/crates/domain/tests/settings.rs b/crates/domain/tests/settings.rs
index 22938408..01a24132 100644
--- a/crates/domain/tests/settings.rs
+++ b/crates/domain/tests/settings.rs
@@ -45,6 +45,7 @@ fn language_roundtrips_as_stable_codes_and_rejects_unknown_values() {
#[test]
fn project_icon_roundtrips_for_every_known_set() {
for (set, wire) in [
+ (ProjectIconSet::Emoji, "emoji"),
(ProjectIconSet::Devicon, "devicon"),
(ProjectIconSet::SimpleIcons, "simple-icons"),
(ProjectIconSet::Lucide, "lucide"),
@@ -64,6 +65,19 @@ fn project_icon_roundtrips_for_every_known_set() {
}
}
+#[test]
+fn legacy_project_icon_sets_remain_accepted() {
+ for wire in ["devicon", "simple-icons", "lucide", "idea"] {
+ let icon: ProjectIconRef = serde_json::from_value(serde_json::json!({
+ "set": wire,
+ "name": "legacy-icon"
+ }))
+ .unwrap();
+
+ assert_eq!(icon.name(), "legacy-icon");
+ }
+}
+
#[test]
fn project_icon_rejects_unknown_sets_and_unstable_or_oversized_names() {
assert!(
diff --git a/frontend/src/adapters/project.test.ts b/frontend/src/adapters/project.test.ts
index 6d4f12b3..27500e60 100644
--- a/frontend/src/adapters/project.test.ts
+++ b/frontend/src/adapters/project.test.ts
@@ -67,12 +67,12 @@ describe("TauriProjectGateway invoke payloads", () => {
it("passes project icon updates through the additive settings request", async () => {
const gateway = new TauriProjectGateway();
await gateway.updateProjectSettings("proj-1", {
- icon: { set: "devicon", name: "typescript" },
+ icon: { set: "emoji", name: "rocket" },
});
expect(invoke).toHaveBeenCalledWith("update_project_settings", {
request: {
projectId: "proj-1",
- icon: { set: "devicon", name: "typescript" },
+ icon: { set: "emoji", name: "rocket" },
},
});
});
diff --git a/frontend/src/assets/project-icons/NOTICE.md b/frontend/src/assets/project-icons/NOTICE.md
deleted file mode 100644
index 9d6402d3..00000000
--- a/frontend/src/assets/project-icons/NOTICE.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# Project icon notices
-
-## Angular
-
-The vendored `angular.svg` is the Angular icon supplied by the official
-[Angular Press Kit](https://angular.dev/press-kit).
-
-Copyright Google LLC. Licensed under
-[Creative Commons Attribution 4.0 International](https://creativecommons.org/licenses/by/4.0/).
-
-No Unity, Unreal Engine, Windows, Apple, Google, or Microsoft logo artwork is
-bundled. Their searchable catalogue entries resolve to product-owned generic
-symbols.
diff --git a/frontend/src/assets/project-icons/angular.svg b/frontend/src/assets/project-icons/angular.svg
deleted file mode 100644
index e3f8c217..00000000
--- a/frontend/src/assets/project-icons/angular.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx
index 968a6bfb..0d66e639 100644
--- a/frontend/src/features/agents/CustomAgentChatView.test.tsx
+++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx
@@ -213,9 +213,9 @@ describe("CustomAgentChatView", () => {
expect(watermark.getAttribute("aria-hidden")).toBe("true");
expect((watermark.firstElementChild as HTMLElement | null)?.style.width).toBe("92%");
expect((watermark.firstElementChild as HTMLElement | null)?.style.height).toBe("92%");
- expect(watermark.textContent).toBe("Py");
- publishProjectIconChanged({ projectId: "project-1", icon: { set: "lucide", name: "star" } });
- await waitFor(() => expect(watermark.textContent).toBe("โ
"));
+ expect(watermark.textContent).toBe("๐");
+ publishProjectIconChanged({ projectId: "project-1", icon: { set: "emoji", name: "star" } });
+ await waitFor(() => expect(watermark.textContent).toBe("โญ"));
});
it("settles an isolated conversation id without recovered turns as an actionable unavailable state (#307/#314)", async () => {
diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx
index ce2f6472..4ba398f7 100644
--- a/frontend/src/features/agents/CustomAgentChatView.tsx
+++ b/frontend/src/features/agents/CustomAgentChatView.tsx
@@ -44,7 +44,7 @@ import {
markProjectCliInteractionStopped,
} from "@/features/projects/projectCliInteractionActivity";
import { Button, EntryTimestamp, Spinner, cn, normalizeTimestamp } from "@/shared";
-import { onProjectIconChanged, ProjectIcon, resolveProjectIcon } from "@/features/projects/projectIcons";
+import { onProjectIconChanged, ProjectIcon } from "@/features/projects/projectIcons";
import type {
ChatAttachmentInput,
ChatDraftScope,
@@ -2478,9 +2478,7 @@ export function CustomAgentChatView({
}
}
- const projectWatermarkRef = projectIcon && resolveProjectIcon(projectIcon, "watermark")
- ? projectIcon
- : null;
+ const projectFallbackLabel = cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? projectId;
return (
- {projectWatermarkRef && (
-
- )}
+
(value);
+ const [selected, setSelected] = useState
(() => toEmojiIconRef(value));
const results = useMemo(() => {
const words = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
return PROJECT_ICON_CATALOG.filter((entry) => words.every((word) => entry.keywords.some((key) => key.includes(word))));
diff --git a/frontend/src/features/projects/ProjectSettingsPanel.test.tsx b/frontend/src/features/projects/ProjectSettingsPanel.test.tsx
index fd940b0c..f997a235 100644
--- a/frontend/src/features/projects/ProjectSettingsPanel.test.tsx
+++ b/frontend/src/features/projects/ProjectSettingsPanel.test.tsx
@@ -278,14 +278,14 @@ describe("ProjectSettingsPanel", () => {
fireEvent.click(await screen.findByRole("button", { name: "Choisir une icรดne" }));
const search = screen.getByLabelText("Search icons in English");
fireEvent.change(search, { target: { value: "docker" } });
- const docker = await screen.findByRole("option", { name: "Docker" });
- fireEvent.click(docker);
- expect(docker.getAttribute("aria-selected")).toBe("true");
+ const packageEmoji = await screen.findByRole("option", { name: "Package" });
+ fireEvent.click(packageEmoji);
+ expect(packageEmoji.getAttribute("aria-selected")).toBe("true");
fireEvent.click(screen.getByRole("button", { name: "Appliquer" }));
await waitFor(() =>
expect(update).toHaveBeenCalledWith(project.id, {
- icon: { set: "simple-icons", name: "docker" },
+ icon: { set: "emoji", name: "package" },
}),
);
expect(await screen.findByRole("button", { name: "Modifier lโicรดne" })).toBeTruthy();
diff --git a/frontend/src/features/projects/ProjectSettingsPanel.tsx b/frontend/src/features/projects/ProjectSettingsPanel.tsx
index 9edce4e0..8cffabeb 100644
--- a/frontend/src/features/projects/ProjectSettingsPanel.tsx
+++ b/frontend/src/features/projects/ProjectSettingsPanel.tsx
@@ -22,6 +22,7 @@ export function ProjectSettingsPanel({ projectId }: { projectId: string }) {
const { t } = useI18n();
const [themeColor, setThemeColor] = useState(null);
const [projectIcon, setProjectIcon] = useState(null);
+ const [projectName, setProjectName] = useState("");
const [iconPickerOpen, setIconPickerOpen] = useState(false);
const [busy, setBusy] = useState(true);
const [error, setError] = useState(null);
@@ -74,6 +75,14 @@ export function ProjectSettingsPanel({ projectId }: { projectId: string }) {
};
}, [load]);
+ useEffect(() => {
+ let cancelled = false;
+ void project.listProjects().then((projects) => {
+ if (!cancelled) setProjectName(projects.find(({ id }) => id === projectId)?.name ?? "");
+ }).catch(() => undefined);
+ return () => { cancelled = true; };
+ }, [project, projectId]);
+
useEffect(
() =>
onProjectThemeChanged(
@@ -188,7 +197,7 @@ export function ProjectSettingsPanel({ projectId }: { projectId: string }) {
- {projectIcon ?
:
โ}
+
{t("project.icon")}
diff --git a/frontend/src/features/projects/projectIcons.test.tsx b/frontend/src/features/projects/projectIcons.test.tsx
index e37d273e..7b3b09b9 100644
--- a/frontend/src/features/projects/projectIcons.test.tsx
+++ b/frontend/src/features/projects/projectIcons.test.tsx
@@ -7,86 +7,81 @@ import {
ProjectIcon,
findProjectIcon,
resolveProjectIcon,
+ toEmojiIconRef,
} from "./projectIcons";
-describe("project icon catalogue", () => {
- it("carries legal policy for every entry and an attribution for the vendored Angular artwork", () => {
- expect(PROJECT_ICON_CATALOG.length).toBeGreaterThan(70);
+describe("project emoji catalogue", () => {
+ it("contains only stable Unicode emoji references and no bundled artwork metadata", () => {
+ expect(PROJECT_ICON_CATALOG.length).toBeGreaterThan(90);
for (const entry of PROJECT_ICON_CATALOG) {
- expect(entry.legal.owner).toBeTruthy();
- expect(entry.legal.license).toBeTruthy();
- expect(typeof entry.legal.productUiAllowed).toBe("boolean");
- expect(typeof entry.legal.watermarkAllowed).toBe("boolean");
+ expect(entry.set).toBe("emoji");
+ expect(entry.name).toMatch(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
+ expect(entry.glyph).toBeTruthy();
+ expect(entry).not.toHaveProperty("assetSrc");
+ expect(entry).not.toHaveProperty("legal");
}
-
- const angular = findProjectIcon({ set: "simple-icons", name: "angular" });
- expect(angular).toMatchObject({
- group: "technologies",
- legal: {
- license: "CC BY 4.0",
- productUiAllowed: true,
- watermarkAllowed: true,
- sourceUrl: "https://angular.dev/press-kit",
- },
- });
- expect(angular?.assetSrc).toBeTruthy();
});
- it.each([
- ["unity", "gamepad-2"],
- ["unrealengine", "gamepad-2"],
- ["windows", "terminal"],
- ["apple", "terminal"],
- ["google", "globe-2"],
- ["microsoft", "terminal"],
- ])("resolves the restricted %s brand to product-owned fallback artwork", (name, fallbackName) => {
- const set = name === "windows" || name === "apple" ? "devicon" : "simple-icons";
- const restricted = findProjectIcon({ set, name });
- expect(restricted?.legal.productUiAllowed).toBe(false);
- expect(restricted?.legal.watermarkAllowed).toBe(false);
- expect(resolveProjectIcon({ set, name }, "product-ui")?.name).toBe(fallbackName);
- expect(resolveProjectIcon({ set, name }, "watermark")?.name).toBe(fallbackName);
+ it("resolves known legacy references to emoji and unknown references to an initial", () => {
+ expect(resolveProjectIcon({ set: "devicon", name: "python" })?.glyph).toBe("๐");
+ expect(toEmojiIconRef({ set: "lucide", name: "star" })).toEqual({ set: "emoji", name: "star" });
+ expect(findProjectIcon({ set: "devicon", name: "python" })).toBeNull();
+
+ const { getByRole } = render(
);
+ expect(getByRole("img", { name: "U" }).textContent).toBe("U");
+ expect(getByRole("img").dataset.resolvedProjectIcon).toBe("initial");
});
- it("provides stable, strongly boxed A-Z monograms", () => {
- const letters = PROJECT_ICON_CATALOG.filter((entry) => entry.group === "letters");
- expect(letters.map((entry) => entry.glyph).join("")).toBe("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
+ it("keeps legacy alphabet initials readable", () => {
const { getByRole } = render(
);
- expect(getByRole("img", { name: "Z" }).style.borderTopWidth).toBe("1px");
- expect(getByRole("img", { name: "Z" }).style.borderRadius).toBe("18%");
+ expect(getByRole("img", { name: "Z" }).textContent).toBe("Z");
+ });
+
+ it("uses the project initial when no icon is configured", () => {
+ const { getByRole } = render(
);
+ expect(getByRole("img", { name: "A" }).textContent).toBe("A");
+ expect(getByRole("img").dataset.projectIcon).toBe("none");
});
});
describe("ProjectIconPicker", () => {
- function renderPicker() {
- render(
);
+ function renderPicker(value = null as Parameters
[0]["value"]) {
+ render();
}
- it("shows the four catalogue groups", () => {
+ it("shows phone-like emoji categories only", () => {
renderPicker();
- expect(screen.getByText("Plateformes")).toBeTruthy();
- expect(screen.getByText("Technologies / Frameworks")).toBeTruthy();
- expect(screen.getByText("Symboles / Gรฉnรฉriques")).toBeTruthy();
- expect(screen.getByText("Lettres")).toBeTruthy();
+ for (const label of ["Smileys", "Animaux et nature", "Nourriture et boissons", "Activitรฉs", "Voyages et lieux", "Objets", "Symboles"]) {
+ expect(screen.getByText(label)).toBeTruthy();
+ }
+ expect(screen.queryByText("Technologies / Frameworks")).toBeNull();
+ expect(screen.queryByText("Lettres")).toBeNull();
});
it.each([
- ["unity", "Unity"],
- ["unreal", "Unreal Engine"],
- ["angular", "Angular"],
- ["windows", "Windows"],
+ ["python", "Snake"],
+ ["docker", "Package"],
+ ["space launch", "Rocket"],
+ ["database", "Floppy disk"],
])("finds %s using the English search metadata", (query, label) => {
renderPicker();
fireEvent.change(screen.getByLabelText("Search icons in English"), { target: { value: query } });
expect(screen.getByRole("option", { name: label })).toBeTruthy();
});
- it("finds and selects an alphabet monogram", () => {
+ it("selects and persists an emoji reference", () => {
const onApply = vi.fn();
render();
- fireEvent.change(screen.getByLabelText("Search icons in English"), { target: { value: "letter q" } });
- fireEvent.click(screen.getByRole("option", { name: "Q" }));
+ fireEvent.change(screen.getByLabelText("Search icons in English"), { target: { value: "rocket" } });
+ fireEvent.click(screen.getByRole("option", { name: "Rocket" }));
fireEvent.click(screen.getByRole("button", { name: "Appliquer" }));
- expect(onApply).toHaveBeenCalledWith({ set: "idea", name: "letter-q" });
+ expect(onApply).toHaveBeenCalledWith({ set: "emoji", name: "rocket" });
+ });
+
+ it("normalizes a known legacy selection before applying it", () => {
+ const onApply = vi.fn();
+ render();
+ fireEvent.click(screen.getByRole("button", { name: "Appliquer" }));
+ expect(onApply).toHaveBeenCalledWith({ set: "emoji", name: "snake" });
});
});
diff --git a/frontend/src/features/projects/projectIcons.tsx b/frontend/src/features/projects/projectIcons.tsx
index 199dd75d..d847320c 100644
--- a/frontend/src/features/projects/projectIcons.tsx
+++ b/frontend/src/features/projects/projectIcons.tsx
@@ -1,211 +1,231 @@
import type { CSSProperties } from "react";
-import angularLogo from "@/assets/project-icons/angular.svg";
-import type { ProjectIconRef, ProjectIconSet } from "@/ports";
+import type { ProjectIconRef } from "@/ports";
-export type ProjectIconGroup = "platforms" | "technologies" | "symbols" | "letters";
+export type ProjectIconGroup =
+ | "smileys"
+ | "nature"
+ | "food"
+ | "activities"
+ | "travel"
+ | "objects"
+ | "symbols";
export type ProjectIconSurface = "product-ui" | "watermark";
-export interface ProjectIconLegalMetadata {
- owner: string;
- license: string;
- sourceUrl?: string;
- attribution?: string;
- productUiAllowed: boolean;
- watermarkAllowed: boolean;
- fallback?: ProjectIconRef;
-}
-
export interface ProjectIconCatalogEntry extends ProjectIconRef {
+ set: "emoji";
label: string;
group: ProjectIconGroup;
glyph: string;
- assetSrc?: string;
keywords: readonly string[];
- legal: ProjectIconLegalMetadata;
}
-const OWNED: ProjectIconLegalMetadata = {
- owner: "IdeA",
- license: "Product-owned generic artwork",
- productUiAllowed: true,
- watermarkAllowed: true,
-};
-
-const generic = (
- set: ProjectIconSet,
+const emoji = (
name: string,
label: string,
glyph: string,
group: ProjectIconGroup,
keywords: string[] = [],
): ProjectIconCatalogEntry => ({
- set, name, label, glyph, group,
+ set: "emoji",
+ name,
+ label,
+ glyph,
+ group,
keywords: [label, name, ...keywords].map((word) => word.toLowerCase()),
- legal: OWNED,
});
-const restrictedBrand = (
- set: ProjectIconSet,
- name: string,
- label: string,
- group: ProjectIconGroup,
- fallback: ProjectIconRef,
- keywords: string[] = [],
-): ProjectIconCatalogEntry => ({
- set, name, label, group, glyph: "",
- keywords: [label, name, ...keywords].map((word) => word.toLowerCase()),
- legal: {
- owner: label,
- license: "No bundled trademark artwork; generic fallback only",
- productUiAllowed: false,
- watermarkAllowed: false,
- fallback,
- },
-});
-
-const GAME = { set: "lucide", name: "gamepad-2" } as const;
-const GLOBE = { set: "lucide", name: "globe-2" } as const;
-const TERMINAL = { set: "lucide", name: "terminal" } as const;
-
-const TECHNOLOGY_MONOGRAMS: readonly ProjectIconCatalogEntry[] = [
- generic("devicon", "c", "C", "C", "technologies", ["language"]),
- generic("devicon", "cplusplus", "C++", "C++", "technologies", ["cpp language"]),
- generic("devicon", "csharp", "C#", "C#", "technologies", ["dotnet language"]),
- generic("devicon", "java", "Java", "J", "technologies", ["language jvm"]),
- generic("devicon", "javascript", "JavaScript", "JS", "technologies", ["language js"]),
- generic("devicon", "typescript", "TypeScript", "TS", "technologies", ["language ts"]),
- generic("devicon", "python", "Python", "Py", "technologies", ["language"]),
- generic("devicon", "rust", "Rust", "Rs", "technologies", ["language cargo"]),
- generic("devicon", "go", "Go", "Go", "technologies", ["language golang"]),
- generic("devicon", "kotlin", "Kotlin", "Kt", "technologies", ["language jvm"]),
- generic("devicon", "swift", "Swift", "Sw", "technologies", ["language ios"]),
- generic("devicon", "php", "PHP", "PHP", "technologies", ["language"]),
- generic("devicon", "ruby", "Ruby", "Rb", "technologies", ["language rails"]),
- generic("simple-icons", "github", "GitHub", "GH", "technologies", ["git repository monogram"]),
- generic("simple-icons", "gitlab", "GitLab", "GL", "technologies", ["git repository monogram"]),
- generic("simple-icons", "docker", "Docker", "D", "technologies", ["container monogram"]),
- generic("simple-icons", "kubernetes", "Kubernetes", "K8s", "technologies", ["container cloud monogram"]),
- generic("simple-icons", "react", "React", "โ", "technologies", ["frontend framework atom symbol"]),
- generic("simple-icons", "vuedotjs", "Vue", "V", "technologies", ["frontend framework monogram"]),
- {
- set: "simple-icons", name: "angular", label: "Angular", group: "technologies", glyph: "A",
- assetSrc: angularLogo,
- keywords: ["angular", "frontend", "web", "framework"],
- legal: {
- owner: "Google LLC / Angular",
- license: "CC BY 4.0",
- sourceUrl: "https://angular.dev/press-kit",
- attribution: "Angular logo ยฉ Google LLC, provided under CC BY 4.0.",
- productUiAllowed: true,
- watermarkAllowed: true,
- },
- },
- generic("simple-icons", "nodedotjs", "Node.js", "N", "technologies", ["javascript runtime monogram"]),
- generic("simple-icons", "postgresql", "PostgreSQL", "PG", "technologies", ["database sql monogram"]),
- generic("simple-icons", "redis", "Redis", "R", "technologies", ["database cache monogram"]),
- generic("simple-icons", "figma", "Figma", "Fi", "technologies", ["design tool monogram"]),
- restrictedBrand("simple-icons", "unity", "Unity", "technologies", GAME, ["game engine gaming framework"]),
- restrictedBrand("simple-icons", "unrealengine", "Unreal Engine", "technologies", GAME, ["unreal ue game engine gaming framework"]),
-];
-
-const PLATFORM_ENTRIES: readonly ProjectIconCatalogEntry[] = [
- generic("devicon", "android", "Android", "A", "platforms", ["mobile generic monogram"]),
- restrictedBrand("devicon", "windows", "Windows", "platforms", TERMINAL, ["microsoft desktop operating system os"]),
- restrictedBrand("devicon", "apple", "macOS / Apple", "platforms", TERMINAL, ["mac ios iphone operating system os"]),
- generic("devicon", "linux", "Linux", "L", "platforms", ["operating system os monogram"]),
- generic("devicon", "ubuntu", "Ubuntu", "U", "platforms", ["linux distribution distro monogram"]),
- generic("devicon", "debian", "Debian", "D", "platforms", ["linux distribution distro monogram"]),
- generic("devicon", "fedora", "Fedora", "F", "platforms", ["linux distribution distro monogram"]),
- generic("devicon", "archlinux", "Arch Linux", "A", "platforms", ["linux distribution distro monogram"]),
- generic("devicon", "redhat", "Red Hat", "RH", "platforms", ["linux distribution distro monogram"]),
- generic("simple-icons", "amazonwebservices", "AWS", "AWS", "platforms", ["cloud amazon generic monogram"]),
- restrictedBrand("simple-icons", "googlecloud", "Google Cloud", "platforms", GLOBE, ["google gcp cloud"]),
- restrictedBrand("simple-icons", "google", "Google", "platforms", GLOBE, ["cloud web search"]),
- restrictedBrand("simple-icons", "microsoft", "Microsoft", "platforms", TERMINAL, ["windows azure platform"]),
-];
-
-const SYMBOL_ENTRIES: readonly ProjectIconCatalogEntry[] = [
- generic("lucide", "star", "Star", "โ
", "symbols", ["favorite symbol"]),
- generic("lucide", "sparkles", "Sparkles", "โฆ", "symbols", ["magic symbol"]),
- generic("lucide", "arrow-up-right", "Arrow up right", "โ", "symbols", ["direction symbol"]),
- generic("lucide", "arrow-right", "Arrow right", "โ", "symbols", ["direction symbol"]),
- generic("lucide", "rocket", "Rocket", "โฒ", "symbols", ["launch space symbol"]),
- generic("lucide", "code-2", "Code", ">", "symbols", ["development brackets symbol"]),
- generic("lucide", "terminal", "Terminal", ">_", "symbols", ["cli shell tool"]),
- generic("lucide", "database", "Database", "DB", "symbols", ["storage server"]),
- generic("lucide", "globe-2", "Globe", "โ", "symbols", ["web world symbol"]),
- generic("lucide", "gamepad-2", "Game", "โ", "symbols", ["gaming controller symbol"]),
- generic("idea", "idea-mark", "IdeA", "IA", "symbols", ["agent ai"]),
-];
-
-const LETTER_ENTRIES: readonly ProjectIconCatalogEntry[] = Array.from({ length: 26 }, (_, index) => {
- const letter = String.fromCharCode(65 + index);
- return generic("idea", `letter-${letter.toLowerCase()}`, letter, letter, "letters", ["alphabet monogram initial"]);
-});
-
-/** Product-owned, versioned catalogue. Search metadata deliberately stays English. */
+/** Curated Unicode characters only. Their artwork is supplied by the operating system. */
export const PROJECT_ICON_CATALOG: readonly ProjectIconCatalogEntry[] = [
- ...PLATFORM_ENTRIES,
- ...TECHNOLOGY_MONOGRAMS,
- ...SYMBOL_ENTRIES,
- ...LETTER_ENTRIES,
+ emoji("grinning-face", "Grinning face", "๐", "smileys", ["happy smile"]),
+ emoji("smiling-face", "Smiling face", "๐", "smileys", ["happy blush"]),
+ emoji("cool-face", "Cool face", "๐", "smileys", ["sunglasses"]),
+ emoji("thinking-face", "Thinking face", "๐ค", "smileys", ["idea question"]),
+ emoji("nerd-face", "Nerd face", "๐ค", "smileys", ["developer glasses"]),
+ emoji("star-struck", "Star-struck", "๐คฉ", "smileys", ["excited"]),
+ emoji("robot", "Robot", "๐ค", "smileys", ["ai agent bot"]),
+ emoji("ghost", "Ghost", "๐ป", "smileys", ["spooky"]),
+ emoji("alien", "Alien", "๐ฝ", "smileys", ["space"]),
+ emoji("skull", "Skull", "๐", "smileys", ["danger"]),
+ emoji("eyes", "Eyes", "๐", "smileys", ["watch view"]),
+ emoji("brain", "Brain", "๐ง ", "smileys", ["mind intelligence"]),
+
+ emoji("dog", "Dog", "๐ถ", "nature", ["animal pet"]),
+ emoji("cat", "Cat", "๐ฑ", "nature", ["animal pet"]),
+ emoji("fox", "Fox", "๐ฆ", "nature", ["animal"]),
+ emoji("lion", "Lion", "๐ฆ", "nature", ["animal"]),
+ emoji("panda", "Panda", "๐ผ", "nature", ["animal"]),
+ emoji("monkey", "Monkey", "๐ต", "nature", ["animal"]),
+ emoji("owl", "Owl", "๐ฆ", "nature", ["animal bird night"]),
+ emoji("butterfly", "Butterfly", "๐ฆ", "nature", ["animal insect"]),
+ emoji("snake", "Snake", "๐", "nature", ["animal python language"]),
+ emoji("turtle", "Turtle", "๐ข", "nature", ["animal"]),
+ emoji("octopus", "Octopus", "๐", "nature", ["animal sea"]),
+ emoji("unicorn", "Unicorn", "๐ฆ", "nature", ["animal magic"]),
+ emoji("seedling", "Seedling", "๐ฑ", "nature", ["plant grow green"]),
+ emoji("evergreen-tree", "Evergreen tree", "๐ฒ", "nature", ["plant forest"]),
+ emoji("cactus", "Cactus", "๐ต", "nature", ["plant desert"]),
+ emoji("sun", "Sun", "โ๏ธ", "nature", ["weather bright"]),
+ emoji("moon", "Moon", "๐", "nature", ["night"]),
+ emoji("cloud", "Cloud", "โ๏ธ", "nature", ["weather aws google internet"]),
+ emoji("lightning", "Lightning", "โก", "nature", ["energy fast"]),
+ emoji("fire", "Fire", "๐ฅ", "nature", ["hot flame"]),
+
+ emoji("apple", "Apple", "๐", "food", ["fruit"]),
+ emoji("lemon", "Lemon", "๐", "food", ["fruit"]),
+ emoji("watermelon", "Watermelon", "๐", "food", ["fruit"]),
+ emoji("avocado", "Avocado", "๐ฅ", "food", ["vegetable"]),
+ emoji("hot-pepper", "Hot pepper", "๐ถ๏ธ", "food", ["spicy"]),
+ emoji("bread", "Bread", "๐", "food", ["bakery"]),
+ emoji("cheese", "Cheese", "๐ง", "food", ["dairy"]),
+ emoji("pizza", "Pizza", "๐", "food", ["meal"]),
+ emoji("hamburger", "Hamburger", "๐", "food", ["meal"]),
+ emoji("sushi", "Sushi", "๐ฃ", "food", ["meal japan"]),
+ emoji("coffee", "Coffee", "โ", "food", ["drink cup java"]),
+ emoji("beer", "Beer", "๐บ", "food", ["drink"]),
+ emoji("cake", "Cake", "๐", "food", ["birthday dessert"]),
+
+ emoji("soccer-ball", "Soccer ball", "โฝ", "activities", ["sport football"]),
+ emoji("basketball", "Basketball", "๐", "activities", ["sport"]),
+ emoji("trophy", "Trophy", "๐", "activities", ["winner award"]),
+ emoji("medal", "Medal", "๐
", "activities", ["winner award"]),
+ emoji("game-controller", "Game controller", "๐ฎ", "activities", ["gaming videogame unity unreal"]),
+ emoji("joystick", "Joystick", "๐น๏ธ", "activities", ["gaming arcade"]),
+ emoji("dice", "Dice", "๐ฒ", "activities", ["game random"]),
+ emoji("puzzle", "Puzzle", "๐งฉ", "activities", ["game plugin piece"]),
+ emoji("artist-palette", "Artist palette", "๐จ", "activities", ["art design color figma"]),
+ emoji("music", "Music", "๐ต", "activities", ["audio note"]),
+ emoji("headphones", "Headphones", "๐ง", "activities", ["audio music"]),
+ emoji("camera", "Camera", "๐ท", "activities", ["photo image"]),
+
+ emoji("car", "Car", "๐", "travel", ["vehicle road"]),
+ emoji("bus", "Bus", "๐", "travel", ["vehicle transport"]),
+ emoji("train", "Train", "๐", "travel", ["vehicle transport"]),
+ emoji("airplane", "Airplane", "โ๏ธ", "travel", ["flight vehicle"]),
+ emoji("rocket", "Rocket", "๐", "travel", ["launch space fast"]),
+ emoji("satellite", "Satellite", "๐ฐ๏ธ", "travel", ["space network"]),
+ emoji("globe", "Globe", "๐", "travel", ["world web internet google cloud"]),
+ emoji("mountain", "Mountain", "โฐ๏ธ", "travel", ["landscape"]),
+ emoji("camping", "Camping", "๐๏ธ", "travel", ["tent outdoors"]),
+ emoji("city", "City", "๐๏ธ", "travel", ["building urban"]),
+ emoji("house", "House", "๐ ", "travel", ["home"]),
+ emoji("anchor", "Anchor", "โ", "travel", ["ship docker"]),
+
+ emoji("laptop", "Laptop", "๐ป", "objects", ["computer code developer terminal"]),
+ emoji("desktop", "Desktop computer", "๐ฅ๏ธ", "objects", ["computer monitor windows mac linux"]),
+ emoji("phone", "Mobile phone", "๐ฑ", "objects", ["android ios smartphone"]),
+ emoji("keyboard", "Keyboard", "โจ๏ธ", "objects", ["computer input"]),
+ emoji("floppy-disk", "Floppy disk", "๐พ", "objects", ["save storage database"]),
+ emoji("optical-disk", "Optical disk", "๐ฟ", "objects", ["storage cd"]),
+ emoji("battery", "Battery", "๐", "objects", ["power energy"]),
+ emoji("light-bulb", "Light bulb", "๐ก", "objects", ["idea smart"]),
+ emoji("magnifying-glass", "Magnifying glass", "๐", "objects", ["search inspect"]),
+ emoji("key", "Key", "๐", "objects", ["security access"]),
+ emoji("lock", "Lock", "๐", "objects", ["security private"]),
+ emoji("hammer-wrench", "Hammer and wrench", "๐ ๏ธ", "objects", ["tools build settings"]),
+ emoji("gear", "Gear", "โ๏ธ", "objects", ["settings system rust"]),
+ emoji("package", "Package", "๐ฆ", "objects", ["box shipping docker container"]),
+ emoji("folder", "Folder", "๐", "objects", ["files project directory git"]),
+ emoji("clipboard", "Clipboard", "๐", "objects", ["document tasks"]),
+ emoji("book", "Book", "๐", "objects", ["documentation read"]),
+ emoji("chart", "Chart", "๐", "objects", ["analytics data"]),
+ emoji("database", "Database", "๐๏ธ", "objects", ["storage server sql redis"]),
+
+ emoji("red-heart", "Red heart", "โค๏ธ", "symbols", ["love favorite ruby"]),
+ emoji("orange-heart", "Orange heart", "๐งก", "symbols", ["love favorite"]),
+ emoji("green-heart", "Green heart", "๐", "symbols", ["love favorite vue"]),
+ emoji("blue-heart", "Blue heart", "๐", "symbols", ["love favorite"]),
+ emoji("purple-heart", "Purple heart", "๐", "symbols", ["love favorite"]),
+ emoji("star", "Star", "โญ", "symbols", ["favorite"]),
+ emoji("sparkles", "Sparkles", "โจ", "symbols", ["magic new"]),
+ emoji("check-mark", "Check mark", "โ
", "symbols", ["success done"]),
+ emoji("warning", "Warning", "โ ๏ธ", "symbols", ["alert danger angular"]),
+ emoji("prohibited", "Prohibited", "๐ซ", "symbols", ["stop forbidden"]),
+ emoji("question-mark", "Question mark", "โ", "symbols", ["help unknown"]),
+ emoji("exclamation-mark", "Exclamation mark", "โ", "symbols", ["alert important"]),
+ emoji("recycle", "Recycle", "โป๏ธ", "symbols", ["reuse green"]),
+ emoji("infinity", "Infinity", "โพ๏ธ", "symbols", ["continuous loop"]),
+ emoji("atom", "Atom", "โ๏ธ", "symbols", ["science react"]),
+ emoji("up-arrow", "Up arrow", "โฌ๏ธ", "symbols", ["direction"]),
+ emoji("right-arrow", "Right arrow", "โก๏ธ", "symbols", ["direction next"]),
];
+const LEGACY_EMOJI_BY_REF: Readonly> = {
+ "devicon:c": "laptop", "devicon:cplusplus": "laptop", "devicon:csharp": "laptop",
+ "devicon:java": "coffee", "devicon:javascript": "lightning", "devicon:typescript": "laptop",
+ "devicon:python": "snake", "devicon:rust": "gear", "devicon:go": "rocket",
+ "devicon:kotlin": "laptop", "devicon:swift": "rocket", "devicon:php": "globe",
+ "devicon:ruby": "red-heart", "devicon:android": "phone", "devicon:windows": "desktop",
+ "devicon:apple": "apple", "devicon:linux": "desktop", "devicon:ubuntu": "desktop",
+ "devicon:debian": "desktop", "devicon:fedora": "desktop", "devicon:archlinux": "desktop",
+ "devicon:redhat": "desktop", "simple-icons:github": "folder", "simple-icons:gitlab": "folder",
+ "simple-icons:docker": "package", "simple-icons:kubernetes": "anchor", "simple-icons:react": "atom",
+ "simple-icons:vuedotjs": "green-heart", "simple-icons:angular": "warning",
+ "simple-icons:nodedotjs": "seedling", "simple-icons:postgresql": "floppy-disk",
+ "simple-icons:redis": "database", "simple-icons:figma": "artist-palette",
+ "simple-icons:unity": "game-controller", "simple-icons:unrealengine": "game-controller",
+ "simple-icons:amazonwebservices": "cloud", "simple-icons:googlecloud": "cloud",
+ "simple-icons:google": "globe", "simple-icons:microsoft": "desktop",
+ "lucide:star": "star", "lucide:sparkles": "sparkles", "lucide:arrow-up-right": "up-arrow",
+ "lucide:arrow-right": "right-arrow", "lucide:rocket": "rocket", "lucide:code-2": "laptop",
+ "lucide:terminal": "keyboard", "lucide:database": "floppy-disk", "lucide:globe-2": "globe",
+ "lucide:gamepad-2": "game-controller", "idea:idea-mark": "robot",
+};
+
export function findProjectIcon(ref: ProjectIconRef | null | undefined): ProjectIconCatalogEntry | null {
- return ref ? PROJECT_ICON_CATALOG.find((entry) => entry.set === ref.set && entry.name === ref.name) ?? null : null;
+ if (!ref || ref.set !== "emoji") return null;
+ return PROJECT_ICON_CATALOG.find((entry) => entry.name === ref.name) ?? null;
}
export function resolveProjectIcon(
ref: ProjectIconRef | null | undefined,
- surface: ProjectIconSurface = "product-ui",
+ _surface: ProjectIconSurface = "product-ui",
): ProjectIconCatalogEntry | null {
- let entry = findProjectIcon(ref);
- const visited = new Set();
- while (entry) {
- const key = `${entry.set}:${entry.name}`;
- if (visited.has(key)) return null;
- visited.add(key);
- const allowed = surface === "watermark" ? entry.legal.watermarkAllowed : entry.legal.productUiAllowed;
- if (allowed) return entry;
- entry = findProjectIcon(entry.legal.fallback);
- }
- return null;
+ const direct = findProjectIcon(ref);
+ if (direct || !ref) return direct;
+ const emojiName = LEGACY_EMOJI_BY_REF[`${ref.set}:${ref.name}`];
+ return emojiName ? findProjectIcon({ set: "emoji", name: emojiName }) : null;
}
-export function ProjectIcon({ icon: ref, size = 32, className, style, decorative = false, surface = "product-ui" }: {
- icon: ProjectIconRef;
+export function toEmojiIconRef(ref: ProjectIconRef | null | undefined): ProjectIconRef | null {
+ const resolved = resolveProjectIcon(ref);
+ return resolved ? { set: "emoji", name: resolved.name } : null;
+}
+
+function fallbackInitial(ref: ProjectIconRef | null, fallbackLabel?: string): string {
+ const legacyLetter = ref?.set === "idea" ? /^letter-([a-z])$/i.exec(ref.name)?.[1] : undefined;
+ const first = legacyLetter ?? fallbackLabel?.match(/[a-z0-9]/i)?.[0] ?? ref?.name.match(/[a-z0-9]/i)?.[0] ?? "?";
+ return first.toUpperCase();
+}
+
+export function ProjectIcon({ icon: ref, fallbackLabel, size = 32, className, style, decorative = false, surface = "product-ui" }: {
+ icon: ProjectIconRef | null;
+ fallbackLabel?: string;
size?: number | string;
className?: string;
style?: CSSProperties;
decorative?: boolean;
surface?: ProjectIconSurface;
}) {
- const requested = findProjectIcon(ref);
const entry = resolveProjectIcon(ref, surface);
- if (!requested || !entry) return null;
+ const label = entry?.label ?? fallbackInitial(ref, fallbackLabel);
const numericSize = typeof size === "number" ? size : null;
- const isLetter = entry.group === "letters";
return (
- {entry.assetSrc
- ?
- : entry.glyph}
+ {entry?.glyph ?? fallbackInitial(ref, fallbackLabel)}
);
}
diff --git a/frontend/src/i18n/catalog.ts b/frontend/src/i18n/catalog.ts
index 14a89c8b..2fa14464 100644
--- a/frontend/src/i18n/catalog.ts
+++ b/frontend/src/i18n/catalog.ts
@@ -42,10 +42,13 @@ export const fr = {
"project.icon.noResults": "Aucune icรดne ne correspond ร cette recherche.",
"project.icon.error": "Impossible dโappliquer lโicรดne.",
"project.icon.picker": "Choisir lโicรดne du projet",
- "project.icon.group.platforms": "Plateformes",
- "project.icon.group.technologies": "Technologies / Frameworks",
- "project.icon.group.symbols": "Symboles / Gรฉnรฉriques",
- "project.icon.group.letters": "Lettres",
+ "project.icon.group.smileys": "Smileys",
+ "project.icon.group.nature": "Animaux et nature",
+ "project.icon.group.food": "Nourriture et boissons",
+ "project.icon.group.activities": "Activitรฉs",
+ "project.icon.group.travel": "Voyages et lieux",
+ "project.icon.group.objects": "Objets",
+ "project.icon.group.symbols": "Symboles",
"custom.unavailable.title": "CLI custom indisponible",
"custom.unavailable.body": "Cette cellule reste configurรฉe en CLI custom. Vรฉrifiez le profil de lโagent ou utilisez temporairement la TUI native.",
"custom.openNative": "Ouvrir temporairement la TUI native",
@@ -145,10 +148,13 @@ export const en: Record = {
"project.icon.noResults": "No icon matches this search.",
"project.icon.error": "Unable to apply the icon.",
"project.icon.picker": "Choose the project icon",
- "project.icon.group.platforms": "Platforms",
- "project.icon.group.technologies": "Technologies / Frameworks",
- "project.icon.group.symbols": "Symbols / Generic",
- "project.icon.group.letters": "Letters",
+ "project.icon.group.smileys": "Smileys",
+ "project.icon.group.nature": "Animals & nature",
+ "project.icon.group.food": "Food & drink",
+ "project.icon.group.activities": "Activities",
+ "project.icon.group.travel": "Travel & places",
+ "project.icon.group.objects": "Objects",
+ "project.icon.group.symbols": "Symbols",
"custom.unavailable.title": "Custom CLI unavailable",
"custom.unavailable.body": "This cell remains configured for the custom CLI. Check the agent profile or temporarily use the native TUI.",
"custom.openNative": "Temporarily open the native TUI",
diff --git a/frontend/src/ports/index.ts b/frontend/src/ports/index.ts
index 4b8d1631..d736a6ad 100644
--- a/frontend/src/ports/index.ts
+++ b/frontend/src/ports/index.ts
@@ -495,7 +495,7 @@ export interface UserSettingsGateway {
updateUserSettings(settings: Partial): Promise;
}
-export type ProjectIconSet = "devicon" | "simple-icons" | "lucide" | "idea";
+export type ProjectIconSet = "emoji" | "devicon" | "simple-icons" | "lucide" | "idea";
export interface ProjectIconRef {
set: ProjectIconSet;