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:
7
.changeset/FN-7198-compound-engineering-icon-match.md
Normal file
7
.changeset/FN-7198-compound-engineering-icon-match.md
Normal 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.
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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]>([
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user