feat(FN-3066): extend plugin UI slot metadata and fix settings token fallba

This release (v0.15.0) introduces plugin dashboard views and a new dependency graph plugin, extends the plugin slot system with richer metadata surfaces and UX improvements, adds SQLite WAL tuning with integrity checks, and stabilizes agent run log streaming with enriched session labels. It also fix

Fusion-Task-Id: FN-3066
This commit is contained in:
Fusion
2026-05-01 23:44:44 -07:00
committed by gsxdsm
parent 1729f52d63
commit 39ba9ab0bd
13 changed files with 227 additions and 59 deletions

View File

@@ -1177,6 +1177,7 @@ describe("PluginLoader", () => {
expect(slots).toHaveLength(1);
expect(slots[0].pluginId).toBe("slots-a");
expect(slots[0].slot.slotId).toBe("task-detail-tab");
expect(slots[0].slot.surface).toBe("task-detail-tab");
expect(slots[0].slot.label).toBe("Task Details");
expect(slots[0].slot.componentPath).toBe("./components/TaskDetailTab.js");
});
@@ -1235,6 +1236,52 @@ describe("PluginLoader", () => {
"header-action",
);
expect(slots.filter((s) => s.pluginId === "slots-b")).toHaveLength(2);
expect(slots.map((slot) => slot.pluginId)).toEqual(["slots-a", "slots-b", "slots-b"]);
});
it("sorts slots by order and then pluginId/slotId", async () => {
await pluginStore.init();
const loader = new PluginLoader({
pluginStore,
taskStore: mockTaskStore,
});
(loader as any).plugins.set("plugin-b", {
manifest: makeManifest({ id: "plugin-b" }),
state: "started",
hooks: {},
uiSlots: [
{
slotId: "onboarding-provider-card",
label: "B",
componentPath: "./B.js",
order: 10,
},
],
} as FusionPlugin);
(loader as any).plugins.set("plugin-a", {
manifest: makeManifest({ id: "plugin-a" }),
state: "started",
hooks: {},
uiSlots: [
{
slotId: "onboarding-provider-card",
label: "A-first",
componentPath: "./A.js",
order: 1,
},
{
slotId: "settings-section",
label: "A-second",
componentPath: "./A2.js",
},
],
} as FusionPlugin);
const slots = loader.getPluginUiSlots();
expect(slots.map((slot) => slot.slot.label)).toEqual(["A-first", "B", "A-second"]);
});
it("each slot includes correct pluginId", async () => {

View File

@@ -708,12 +708,30 @@ describe("PluginUiSlotDefinition", () => {
label: "Task Details",
icon: "FileText",
componentPath: "./components/TaskDetailTab.js",
surface: "task-detail-tab",
order: 5,
placement: "after-default",
};
expect(slot.slotId).toBe("task-detail-tab");
expect(slot.label).toBe("Task Details");
expect(slot.icon).toBe("FileText");
expect(slot.componentPath).toBe("./components/TaskDetailTab.js");
expect(slot.surface).toBe("task-detail-tab");
expect(slot.order).toBe(5);
expect(slot.placement).toBe("after-default");
});
it("accepts new host-owned onboarding/settings surfaces", () => {
const slot = {
slotId: "onboarding-setup-help",
label: "Setup help",
componentPath: "./components/SetupHelp.js",
surface: "onboarding-setup-help",
};
expect(slot.slotId).toBe("onboarding-setup-help");
expect(slot.surface).toBe("onboarding-setup-help");
});
it("accepts a valid PluginUiSlotDefinition without optional icon field", () => {

View File

@@ -139,6 +139,7 @@ export type {
PluginToolResult,
PluginRouteDefinition,
PluginRouteMethod,
PluginUiSurface,
PluginUiSlotDefinition,
PluginDashboardViewDefinition,
PluginRuntimeManifestMetadata,

View File

@@ -769,11 +769,24 @@ export class PluginLoader extends EventEmitter<{
for (const [pluginId, plugin] of this.plugins) {
if (plugin.uiSlots) {
for (const slot of plugin.uiSlots) {
slots.push({ pluginId, slot });
slots.push({
pluginId,
slot: {
...slot,
surface: slot.surface ?? (typeof slot.slotId === "string" ? slot.slotId as PluginUiSlotDefinition["surface"] : undefined),
},
});
}
}
}
return slots;
return slots.sort((a, b) => {
const orderA = a.slot.order ?? Number.MAX_SAFE_INTEGER;
const orderB = b.slot.order ?? Number.MAX_SAFE_INTEGER;
if (orderA !== orderB) return orderA - orderB;
if (a.pluginId !== b.pluginId) return a.pluginId.localeCompare(b.pluginId);
return String(a.slot.slotId).localeCompare(String(b.slot.slotId));
});
}

View File

@@ -159,24 +159,48 @@ export interface PluginRouteDefinition {
// ── Plugin UI Slots ─────────────────────────────────────────────────
/**
* Host-defined dashboard UI surfaces that plugins can contribute to.
* Existing generic surfaces remain supported for backward compatibility.
*/
export type PluginUiSurface =
| "header-action"
| "task-detail-tab"
| "task-card-badge"
| "board-column-footer"
| "settings-section"
| "settings-provider-card"
| "settings-integration-card"
| "onboarding-provider-card"
| "onboarding-recommendation-card"
| "onboarding-setup-help"
| "post-onboarding-recommendation";
/**
* UI slot definition for plugin-provided dashboard components.
* Each slot represents a mount point where a plugin can render UI.
* Each slot represents a host-owned mount point where a plugin can render UI.
*/
export interface PluginUiSlotDefinition {
/** Unique slot identifier (e.g., "task-detail-tab", "header-action", "settings-section") */
slotId: string;
/**
* Unique slot identifier. Should match one of the known host surfaces above,
* but string is retained for compatibility with legacy plugins.
*/
slotId: PluginUiSurface | string;
/** Human-readable label for the UI slot */
label: string;
/** Optional icon name (lucide-react icon name or custom icon identifier) */
icon?: string;
/**
* Path to the JS module that exports the component.
* This should be a web component or a function component descriptor
* that the dashboard can render in the slot.
* Path is relative to the plugin's root directory.
*/
componentPath: string;
/** Optional explicit surface metadata (defaults to slotId). */
surface?: PluginUiSurface;
/** Optional deterministic render order; lower values render first. */
order?: number;
/** Optional host placement hint for the surface. */
placement?: "before-default" | "after-default" | "replace-default";
}
/**