feat(FN-3576): restore plugin setting group field and grouped agent browser

This merge restores plugin setting group functionality in the dashboard (FN-3576), adds documentation for plugin authoring, and improves test isolation by broadening runtime ignore lists for live fusion app paths in the isolation checker script. The feature touches the PluginManager component with n

Fusion-Task-Id: FN-3576
This commit is contained in:
Fusion
2026-05-06 14:47:13 -07:00
committed by gsxdsm
parent a1ec43d580
commit 2858a0b0b5
6 changed files with 202 additions and 8 deletions

View File

@@ -15,7 +15,7 @@ import { useState, useEffect, useCallback, useRef } from "react";
import { Package, Settings, Trash2, Plus, X, RefreshCw, RotateCcw, ExternalLink } from "lucide-react";
import { fetchPlugins, installPlugin, enablePlugin, disablePlugin, uninstallPlugin, fetchPluginSettings, updatePluginSettings, reloadPlugin } from "../api";
import { DirectoryPicker } from "./DirectoryPicker";
import type { PluginInstallation, PluginState } from "@fusion/core";
import type { PluginInstallation, PluginState, PluginSettingSchema } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
import { useConfirm } from "../hooks/useConfirm";
import { subscribeSse } from "../sse-bus";
@@ -46,9 +46,42 @@ interface BundledPlugin {
experimental?: boolean;
}
export const DEFAULT_AGENT_BROWSER_PLUGIN_ID = "fusion-plugin-agent-browser-runtime";
export const AGENT_BROWSER_SETTINGS_SCHEMA: Record<string, PluginSettingSchema> = {
enabled: { type: "boolean", label: "Enable Agent Browser", group: "General" },
installChannel: {
type: "enum",
label: "Install Channel",
enumValues: ["stable", "beta", "nightly"],
defaultValue: "stable",
group: "General",
},
commandTimeoutMs: {
type: "number",
label: "Command Timeout (ms)",
defaultValue: 120000,
group: "General",
},
headlessMode: { type: "boolean", label: "Headless Mode", defaultValue: true, group: "Browser" },
allowedDomains: { type: "array", label: "Allowed Domains", itemType: "string", group: "Browser" },
promptExecutorSystem: { type: "string", label: "Executor System Prompt", multiline: true, group: "Prompt Contributions" },
promptExecutorTask: { type: "string", label: "Executor Task Prompt", multiline: true, group: "Prompt Contributions" },
promptTriage: { type: "string", label: "Triage Prompt", multiline: true, group: "Prompt Contributions" },
promptReviewer: { type: "string", label: "Reviewer Prompt", multiline: true, group: "Prompt Contributions" },
promptHeartbeat: { type: "string", label: "Heartbeat Prompt", multiline: true, group: "Prompt Contributions" },
skillExposure: {
type: "enum",
label: "Skill Exposure",
enumValues: ["none", "selected", "all"],
defaultValue: "selected",
group: "Skills",
},
};
const BUNDLED_PLUGINS: BundledPlugin[] = [
{
id: "fusion-plugin-agent-browser-runtime",
id: DEFAULT_AGENT_BROWSER_PLUGIN_ID,
name: "Agent Browser Runtime",
path: "./plugins/fusion-plugin-agent-browser-runtime",
experimental: true,
@@ -91,6 +124,25 @@ export const STATE_COLORS: Record<string, string> = {
installed: "var(--color-info)",
};
function groupSettingsSchema(settingsSchema: Record<string, PluginSettingSchema>) {
const grouped = new Map<string, Array<[string, PluginSettingSchema]>>();
const ungrouped: Array<[string, PluginSettingSchema]> = [];
for (const [key, schema] of Object.entries(settingsSchema)) {
if (schema.group) {
const groupItems = grouped.get(schema.group) ?? [];
groupItems.push([key, schema]);
grouped.set(schema.group, groupItems);
} else {
ungrouped.push([key, schema]);
}
}
return { grouped, ungrouped };
}
export function PluginManager({ addToast, projectId }: PluginManagerProps) {
const [plugins, setPlugins] = useState<PluginInstallation[]>([]);
const [loading, setLoading] = useState(true);
@@ -358,9 +410,29 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
<p className="text-muted">Loading...</p>
) : selectedPlugin.settingsSchema && Object.keys(selectedPlugin.settingsSchema).length > 0 ? (
<div className="plugin-settings-form">
{Object.entries(selectedPlugin.settingsSchema).map(([key, schema]) => {
const helpId = `setting-${key}-help`;
return (
{(() => {
const { grouped, ungrouped } = groupSettingsSchema(selectedPlugin.settingsSchema);
const sections: Array<{ title: string | null; entries: Array<[string, PluginSettingSchema]> }> = [];
if (ungrouped.length > 0) {
sections.push({ title: null, entries: ungrouped });
}
for (const [groupName, entries] of grouped.entries()) {
sections.push({ title: groupName, entries });
}
return sections.map((section) => (
<div
key={section.title ?? "ungrouped"}
className={section.title ? "plugin-settings-group" : undefined}
>
{section.title && (
<h6 className="plugin-settings-group-heading">{section.title}</h6>
)}
{section.entries.map(([key, schema]) => {
const helpId = `setting-${key}-help`;
return (
<div key={key} className="form-group">
<label htmlFor={`setting-${key}`}>
{schema.label || key}
@@ -479,8 +551,11 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
<span id={helpId} className="form-help">{schema.description}</span>
)}
</div>
);
})}
);
})}
</div>
));
})()}
<button className="btn btn-primary" onClick={handleSaveSettings}>
Save Settings
</button>