feat(FN-3093): address plugin manager responsive overflow

The merge fixes responsive overflow issues in the PluginManager component by adding 15 lines of CSS.

Fusion-Task-Id: FN-3093
This commit is contained in:
Fusion
2026-05-01 16:46:45 -07:00
committed by gsxdsm
parent 92d7c7bbb8
commit 76797f71cb
8 changed files with 614 additions and 17 deletions

View File

@@ -170,12 +170,19 @@
color: var(--text-muted);
}
.plugin-homepage {
flex-wrap: wrap;
align-items: flex-start;
}
.plugin-homepage a {
display: inline-flex;
align-items: center;
gap: var(--space-xs);
color: var(--color-info);
font-size: 0.85rem;
flex-wrap: wrap;
overflow-wrap: anywhere;
}
.plugin-detail-section-heading {
@@ -258,6 +265,8 @@
display: flex;
align-items: center;
gap: var(--space-sm);
row-gap: var(--space-xs);
flex-wrap: wrap;
min-width: 0;
}
@@ -312,6 +321,12 @@
text-transform: uppercase;
}
@media (min-width: 769px) {
.plugin-bundled-runtime-status {
margin-left: auto;
}
}
.plugin-bundled-runtime-status--installed {
background: var(--status-done-bg);
color: var(--done);
@@ -323,6 +338,11 @@
}
@media (max-width: 768px) {
.plugin-manager,
.plugin-manager-detail {
padding-inline: var(--space-sm);
}
.plugin-manager-detail-header {
gap: var(--space-sm);
}

View File

@@ -47,6 +47,12 @@ interface BundledRuntimePlugin {
}
const BUNDLED_RUNTIME_PLUGINS: BundledRuntimePlugin[] = [
{
id: "fusion-plugin-agent-browser-runtime",
name: "Agent Browser Runtime",
path: "./plugins/fusion-plugin-agent-browser-runtime",
experimental: true,
},
{
id: "fusion-plugin-hermes-runtime",
name: "Hermes Runtime",
@@ -351,6 +357,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
</label>
{schema.type === "string" && !schema.multiline && (
<input
className="input"
type="text"
id={`setting-${key}`}
value={(pluginSettings[key] as string) ?? ""}
@@ -361,6 +368,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
)}
{schema.type === "string" && schema.multiline && (
<textarea
className="input"
id={`setting-${key}`}
rows={4}
value={(pluginSettings[key] as string) ?? ""}
@@ -371,6 +379,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
)}
{schema.type === "password" && (
<input
className="input"
type="password"
id={`setting-${key}`}
value={(pluginSettings[key] as string) ?? ""}
@@ -381,6 +390,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
)}
{schema.type === "number" && (
<input
className="input"
type="number"
id={`setting-${key}`}
value={(pluginSettings[key] as number) ?? ""}
@@ -400,6 +410,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
)}
{schema.type === "enum" && (
<select
className="select"
id={`setting-${key}`}
value={(pluginSettings[key] as string) ?? ""}
onChange={(e) => setPluginSettings({ ...pluginSettings, [key]: e.target.value })}
@@ -416,6 +427,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
{(pluginSettings[key] as unknown[] | undefined)?.map((item, index) => (
<div key={index} className="plugin-settings-array-item">
<input
className="input"
type={schema.itemType === "number" ? "number" : "text"}
value={(item as string | number) ?? ""}
onChange={(e) => {
@@ -497,11 +509,11 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
}
const installedPluginIds = new Set(plugins.map((plugin) => plugin.id));
const installedPluginsById = new Map(plugins.map((plugin) => [plugin.id, plugin]));
// Bundled runtime plugin IDs — these are shown in the dedicated section below,
// so we exclude them from the main plugin list to avoid duplication.
const bundledPluginIds = new Set(BUNDLED_RUNTIME_PLUGINS.map((p) => p.id));
const userInstalledPlugins = plugins.filter((p) => !bundledPluginIds.has(p.id));
// Keep bundled plugins in the main list once installed so users can always
// access enable/disable, settings, and uninstall controls.
const installedPlugins = plugins;
const renderBundledRuntimeSection = () => (
<section className="plugin-bundled-runtime-section" aria-label="Bundled Runtime Plugins">
@@ -529,11 +541,20 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
</div>
<button
className={`btn ${isInstalled ? "btn-secondary" : "btn-primary"} btn-sm`}
onClick={() => handleInstallBundledRuntimePlugin(bundledPlugin)}
disabled={isInstalled || installingBundledPluginId === bundledPlugin.id}
onClick={() => {
if (isInstalled) {
const installedPlugin = installedPluginsById.get(bundledPlugin.id);
if (installedPlugin) {
void handleSelectPlugin(installedPlugin);
}
return;
}
void handleInstallBundledRuntimePlugin(bundledPlugin);
}}
disabled={installingBundledPluginId === bundledPlugin.id}
>
{isInstalled
? "Installed"
? "Manage"
: installingBundledPluginId === bundledPlugin.id
? "Installing..."
: `Install ${bundledPlugin.name}`}
@@ -592,7 +613,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
<div className="settings-empty-state">Loading plugins...</div>
) : (
<>
{userInstalledPlugins.length === 0 ? (
{installedPlugins.length === 0 ? (
<div className="settings-empty-state">
<Package size={32} className="text-muted" />
<p>No plugins installed.</p>
@@ -600,7 +621,7 @@ export function PluginManager({ addToast, projectId }: PluginManagerProps) {
</div>
) : (
<div className="plugin-list">
{userInstalledPlugins.map((plugin) => (
{installedPlugins.map((plugin) => (
<div key={plugin.id} className="plugin-item">
<div className="plugin-info">
<span className="plugin-name">{plugin.name}</span>

View File

@@ -226,6 +226,7 @@ describe("PluginManager", () => {
});
expect(screen.getByText("No plugins installed.")).toBeTruthy();
expect(screen.getByText("Agent Browser Runtime")).toBeTruthy();
expect(screen.getByText("Hermes Runtime")).toBeTruthy();
expect(screen.getByText("Paperclip Runtime")).toBeTruthy();
expect(screen.getByText("OpenClaw Runtime")).toBeTruthy();
@@ -503,7 +504,7 @@ describe("PluginManager", () => {
});
});
it("shows installed status and disables bundled runtime install button when already installed", async () => {
it("keeps bundled plugin manageable after install", async () => {
vi.mocked(fetchPlugins).mockResolvedValueOnce([
{
...mockPlugins[0],
@@ -521,9 +522,17 @@ describe("PluginManager", () => {
const hermesCard = screen.getByText("Hermes Runtime").closest(".plugin-bundled-runtime-item");
expect(hermesCard).toBeTruthy();
const installButton = within(hermesCard as HTMLElement).getByRole("button", { name: /^Installed$/i });
expect(installButton).toBeDisabled();
const manageButton = within(hermesCard as HTMLElement).getByRole("button", { name: /^Manage$/i });
expect(manageButton).not.toBeDisabled();
expect(within(hermesCard as HTMLElement).getAllByText("Installed").length).toBeGreaterThanOrEqual(1);
await userEvent.click(manageButton);
await waitFor(() => {
expect(fetchPluginSettings).toHaveBeenCalledWith("fusion-plugin-hermes-runtime", undefined);
});
expect(screen.getByTestId("plugin-manager-detail")).toBeTruthy();
});
describe("SSE Live Updates", () => {