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:
@@ -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 () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -139,6 +139,7 @@ export type {
|
||||
PluginToolResult,
|
||||
PluginRouteDefinition,
|
||||
PluginRouteMethod,
|
||||
PluginUiSurface,
|
||||
PluginUiSlotDefinition,
|
||||
PluginDashboardViewDefinition,
|
||||
PluginRuntimeManifestMetadata,
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user