FN-7198: pin Compound Engineering sidebar icon

Keep Compound Engineering navigation surfaces on the Boxes glyph even when stale plugin metadata is served.

- Route desktop and mobile plugin nav entries through a dashboard-view icon resolver.
- Pin Compound Engineering plugin nav icons to Boxes by plugin id while preserving normal icon fallback behavior for other plugins.
- Align manifest/header icon invariants and add regression coverage for stale Sparkles/Grid3X3 metadata.
- Add a patch changeset for the published Fusion package.

Files changed:
 .changeset/FN-7198-compound-engineering-icon-match.md      |  7 +++++++
 packages/dashboard/app/components/LeftSidebarNav.tsx       |  4 ++--
 packages/dashboard/app/components/MobileNavBar.tsx         |  6 +++---
 .../app/components/__tests__/LeftSidebarNav.test.tsx       |  6 +++++-
 .../app/components/__tests__/MobileNavBar.test.tsx         | 10 +++++++---
 .../app/components/__tests__/pluginNavIcon.test.ts         | 10 +++++++++-
 packages/dashboard/app/components/pluginNavIcon.tsx        | 12 ++++++++++++
 .../src/__tests__/manifest.test.ts                         | 14 ++++++++++++++
 .../src/dashboard/CompoundEngineeringView.tsx              |  3 ++-
 9 files changed, 61 insertions(+), 11 deletions(-)

Fusion-Task-Id: FN-7198
Fusion-Task-Lineage: e910bb82-55a7-4c81-a4fe-947a07b3e308
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-28 12:10:10 -07:00
parent 2be00efc04
commit 581f85013a
9 changed files with 61 additions and 11 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Compound Engineering sidebar navigation now matches its Boxes header icon.
category: fix
dev: Pins Compound Engineering plugin nav entries to Boxes by plugin id so stale dashboard view metadata cannot render Sparkles/Grid3X3 in desktop or mobile navigation.

View File

@@ -31,7 +31,7 @@ import {
import type { ProjectInfo, PluginDashboardViewEntry } from "../api";
import type { TaskView } from "../hooks/useViewState";
import { buildPluginTaskViewId } from "../plugins/pluginViewRegistry";
import { getPluginNavIcon } from "./pluginNavIcon";
import { getPluginDashboardViewNavIcon } from "./pluginNavIcon";
import { GithubIcon } from "./GithubIcon";
export interface LeftSidebarExperimentalFeatures {
@@ -241,7 +241,7 @@ export function LeftSidebarNav({
const mapPluginEntry = useCallback(
(entry: PluginDashboardViewEntry): SidebarNavEntry => {
const PluginIcon = getPluginNavIcon(entry.view.icon);
const PluginIcon = getPluginDashboardViewNavIcon(entry);
const targetView = getPluginEntryView(entry);
return {
id: `plugin-${entry.pluginId}-${entry.view.viewId}`,

View File

@@ -36,7 +36,7 @@ import type { PluginDashboardViewEntry } from "../api";
import { useViewportMode } from "./Header";
import type { TaskView } from "../hooks/useViewState";
import { buildPluginTaskViewId, isPluginViewId } from "../plugins/pluginViewRegistry";
import { getPluginNavIcon } from "./pluginNavIcon";
import { getPluginDashboardViewNavIcon } from "./pluginNavIcon";
export interface PublishedMobileNavHeightInput {
navOffsetHeight: number;
@@ -440,7 +440,7 @@ export function MobileNavBar({
{topLevelPrimaryPluginViews.map((entry) => {
const pluginTaskView = buildPluginTaskViewId(entry.pluginId, entry.view.viewId);
const PluginIcon = getPluginNavIcon(entry.view.icon);
const PluginIcon = getPluginDashboardViewNavIcon(entry);
return (
<button
key={`${entry.pluginId}:${entry.view.viewId}`}
@@ -790,7 +790,7 @@ export function MobileNavBar({
{overflowPluginViews.map((entry) => {
const pluginTaskView = buildPluginTaskViewId(entry.pluginId, entry.view.viewId);
const PluginIcon = getPluginNavIcon(entry.view.icon);
const PluginIcon = getPluginDashboardViewNavIcon(entry);
return (
<button
key={`${entry.pluginId}:${entry.view.viewId}`}

View File

@@ -422,7 +422,7 @@ describe("LeftSidebarNav", () => {
expect(screen.getByTestId("sidebar-nav-plugin-fusion-plugin-overflow-overflow-view")).toBeDefined();
});
it("renders plugin labels without view suffix and shortens Compound Engineering", () => {
it("renders plugin labels without view suffix and pins Compound Engineering to the Boxes sidebar icon", () => {
renderSidebar({
pluginDashboardViews: [
...pluginViews,
@@ -432,6 +432,7 @@ describe("LeftSidebarNav", () => {
viewId: "compound",
label: "Compound Engineering",
componentPath: "./CompoundEngineering",
icon: "Sparkles",
placement: "primary",
order: 0,
},
@@ -449,6 +450,9 @@ describe("LeftSidebarNav", () => {
expect(compoundPlugin).toHaveAttribute("title", "Compound Eng");
expect(compoundPlugin).toHaveTextContent("Compound Eng");
expect(compoundPlugin).not.toHaveTextContent("Compound Engineering");
expect(compoundPlugin.querySelector(".lucide-boxes")).not.toBeNull();
expect(compoundPlugin.querySelector(".lucide-sparkles")).toBeNull();
expect(compoundPlugin.querySelector(".lucide-grid-3x3")).toBeNull();
});
it.each<[TaskView, string]>([

View File

@@ -643,14 +643,14 @@ describe("MobileNavBar", () => {
expect(screen.queryByTestId("mobile-more-item-roadmaps")).toBeNull();
});
it("renders Compound Engineering primary plugin only in the More sheet while Command Center is the first tab", () => {
it("renders Compound Engineering primary plugin only in More with the pinned Boxes icon", () => {
const { container } = render(
<MobileNavBar
{...createDefaultProps()}
pluginDashboardViews={[
{
pluginId: "fusion-plugin-compound-engineering",
view: { viewId: "compound-engineering", label: "Compound Engineering", componentPath: "./CompoundEngineeringView", icon: "Boxes", placement: "primary", order: 36 },
view: { viewId: "compound-engineering", label: "Compound Engineering", componentPath: "./CompoundEngineeringView", icon: "Sparkles", placement: "primary", order: 36 },
},
]}
/>,
@@ -662,7 +662,11 @@ describe("MobileNavBar", () => {
expect(screen.queryByTestId("mobile-nav-tab-plugin-fusion-plugin-compound-engineering-compound-engineering")).toBeNull();
fireEvent.click(screen.getByTestId("mobile-nav-tab-more"));
expect(screen.getByTestId("mobile-more-item-plugin-fusion-plugin-compound-engineering-compound-engineering")).toBeDefined();
const compoundItem = screen.getByTestId("mobile-more-item-plugin-fusion-plugin-compound-engineering-compound-engineering");
expect(compoundItem).toBeDefined();
expect(compoundItem.querySelector(".lucide-boxes")).not.toBeNull();
expect(compoundItem.querySelector(".lucide-sparkles")).toBeNull();
expect(compoundItem.querySelector(".lucide-grid-3x3")).toBeNull();
expect(screen.queryAllByTestId("mobile-more-item-plugin-fusion-plugin-compound-engineering-compound-engineering")).toHaveLength(1);
expect(screen.queryByTestId("mobile-more-item-command-center")).toBeNull();
});

View File

@@ -1,10 +1,18 @@
import { Boxes, Grid3X3 } from "lucide-react";
import { describe, expect, it } from "vitest";
import { getPluginNavIcon } from "../pluginNavIcon";
import { getPluginDashboardViewNavIcon, getPluginNavIcon } from "../pluginNavIcon";
describe("getPluginNavIcon", () => {
it("resolves the Compound Engineering Boxes icon to the registered lucide glyph", () => {
expect(getPluginNavIcon("Boxes")).toBe(Boxes);
expect(getPluginNavIcon("Boxes")).not.toBe(Grid3X3);
});
it("pins Compound Engineering dashboard-view nav entries to Boxes even when stale metadata is served", () => {
expect(getPluginDashboardViewNavIcon({
pluginId: "fusion-plugin-compound-engineering",
view: { viewId: "compound-engineering", label: "Compound Engineering", componentPath: "./dashboard-view", icon: "Sparkles" },
})).toBe(Boxes);
});
it("falls back to Grid3X3 for empty or unknown icon names", () => {

View File

@@ -1,5 +1,6 @@
import { Activity, Bot, Boxes, Brain, CheckSquare, Clock, FileText, Folder, GitBranch, Grid3X3, LayoutGrid, Mail, Map, MessageSquare, Monitor, Search, Sparkles, Target, Workflow, Zap } from "lucide-react";
import type { LucideIcon } from "lucide-react";
import type { PluginDashboardViewEntry } from "../api";
/*
FNXC:Navigation 2026-06-28-00:00:
@@ -32,6 +33,17 @@ function normalizeIconName(iconName?: string): string {
return (iconName ?? "").trim().toLowerCase().replace(/[-_\s]/g, "");
}
const COMPOUND_ENGINEERING_PLUGIN_ID = "fusion-plugin-compound-engineering";
export function getPluginNavIcon(iconName?: string): LucideIcon {
return PLUGIN_NAV_ICON_MAP[normalizeIconName(iconName)] ?? Grid3X3;
}
export function getPluginDashboardViewNavIcon(entry: Pick<PluginDashboardViewEntry, "pluginId" | "view">): LucideIcon {
/*
FNXC:Navigation 2026-06-28-00:00:
Compound Engineering's in-view header can update from the rebuilt dashboard bundle while the sidebar still receives a stale dashboardViews.icon from plugin loader state or bundled install output. Pin the plugin-id-specific nav icon to Boxes so desktop and mobile sidebars match the agreed header glyph even when incoming metadata has not refreshed yet.
*/
if (entry.pluginId === COMPOUND_ENGINEERING_PLUGIN_ID) return Boxes;
return getPluginNavIcon(entry.view.icon);
}

View File

@@ -1,6 +1,8 @@
import { Boxes } from "lucide-react";
import { describe, expect, it } from "vitest";
import manifest from "../../manifest.json";
import packageJson from "../../package.json";
import { COMPOUND_ENGINEERING_VIEW_HEADER_ICON } from "../dashboard/CompoundEngineeringView.js";
import plugin from "../index.js";
import { COMPOUND_ENGINEERING_SKILLS } from "../skills.js";
import { settingsSchema } from "../settings.js";
@@ -33,6 +35,18 @@ describe("compound engineering plugin manifest", () => {
expect(manifest.dashboardViews).toEqual(plugin.dashboardViews);
});
it("keeps the Compound Engineering icon invariant aligned across source surfaces", () => {
const runtimeIcon = plugin.dashboardViews?.[0]?.icon;
const manifestIcon = manifest.dashboardViews?.[0]?.icon;
expect(runtimeIcon).toBe("Boxes");
expect(manifestIcon).toBe(runtimeIcon);
expect(COMPOUND_ENGINEERING_VIEW_HEADER_ICON).toBe(runtimeIcon);
expect(runtimeIcon).not.toBe("Sparkles");
expect(runtimeIcon).not.toBe("Grid3X3");
expect(Boxes).toBeDefined();
});
it("registers the session orchestration routes (U5)", () => {
const paths = (plugin.routes ?? []).map((r) => `${r.method} ${r.path}`);
expect(paths).toEqual(

View File

@@ -14,6 +14,7 @@ import type { CeArtifactEntry, CeArtifactGroup } from "../artifacts/discovery.js
import type { CeSession, CeSessionStatus } from "../session/session-store.js";
const CE_PLUGIN_ID = "fusion-plugin-compound-engineering";
export const COMPOUND_ENGINEERING_VIEW_HEADER_ICON = "Boxes";
/**
* FNXC:CompoundEngineeringUI 2026-06-17-00:52:
@@ -369,7 +370,7 @@ export function CompoundEngineeringView(props: CompoundEngineeringViewProps) {
FNXC:CompoundEngineering 2026-06-28-00:00:
Compound Engineering uses Boxes for its nav and view header icon because Sparkles collides with Insights in the left sidebar. Keep this header aligned with the plugin dashboardViews icon so desktop, mobile, and in-view surfaces share the same no-overlap glyph.
*/
const HeaderIcon = LucideIcons.Boxes;
const HeaderIcon = LucideIcons[COMPOUND_ENGINEERING_VIEW_HEADER_ICON];
if (ceSession.session) {
return (