feat(FN-3071): add droid runtime plugin with event bridge, process manageme

This merge introduces a droid runtime plugin system with event bridge and process management (FN-3228), stabilizes plugin workspace verification gates and suppresses placeholder plugin shells in onboarding/settings flows (FN-3071), tokenizes danger/error styling in task detail components (FN-3350),

Fusion-Task-Id: FN-3071
This commit is contained in:
Fusion
2026-05-04 04:24:55 -07:00
committed by gsxdsm
parent 02d28359c2
commit b8d36506db
14 changed files with 343 additions and 17 deletions

View File

@@ -2112,7 +2112,7 @@ export function ModelOnboardingModal({
</div>
) : (
<>
<PluginSlot slotId="onboarding-provider-card" projectId={projectId} />
<PluginSlot slotId="onboarding-provider-card" projectId={projectId} renderPlaceholder={false} />
<section className="onboarding-provider-section" data-testid="onboarding-quick-start-providers">
<h3 className="onboarding-section-title">Quick start providers</h3>

View File

@@ -10,20 +10,16 @@ interface PluginSlotProps {
projectId?: string;
/** Optional plugin IDs to restrict rendering to a subset of matching entries */
pluginIds?: string[];
/** Render fallback shell placeholders while dynamic slot component mounting is unavailable */
renderPlaceholder?: boolean;
}
/**
* Renders plugin slot registrations for a host surface.
*
* Dynamic plugin component loading is not yet available, so this renders a
* lightweight non-technical placeholder while preserving plugin slot boundaries.
* Each rendered slot is wrapped in an ErrorBoundary to isolate failures from
* the parent dashboard UI.
*/
export function PluginSlot({ slotId, projectId, pluginIds }: PluginSlotProps): ReactNode {
export function PluginSlot({ slotId, projectId, pluginIds, renderPlaceholder = true }: PluginSlotProps): ReactNode {
const { getSlotsForId, loading, error } = usePluginUiSlots(projectId);
// Non-critical failure — no visible UI when loading, errored, or no matching slots
if (loading || error || !slotId) {
return null;
}
@@ -32,7 +28,7 @@ export function PluginSlot({ slotId, projectId, pluginIds }: PluginSlotProps): R
pluginIds && pluginIds.length > 0 ? pluginIds.includes(entry.pluginId) : true,
);
if (matchingEntries.length === 0) {
if (matchingEntries.length === 0 || !renderPlaceholder) {
return null;
}

View File

@@ -196,7 +196,7 @@ export function PostOnboardingRecommendations({
);
})}
</ul>
<PluginSlot slotId="post-onboarding-recommendation" />
<PluginSlot slotId="post-onboarding-recommendation" renderPlaceholder={false} />
</div>
</div>
<button

View File

@@ -5063,8 +5063,8 @@ export function SettingsModal({
</div>
) : (
<div className="auth-panel-body">
<PluginSlot slotId="settings-provider-card" projectId={projectId} />
<PluginSlot slotId="settings-integration-card" projectId={projectId} />
<PluginSlot slotId="settings-provider-card" projectId={projectId} renderPlaceholder={false} />
<PluginSlot slotId="settings-integration-card" projectId={projectId} renderPlaceholder={false} />
{!showAuthenticatedGroup && (
<div className="auth-section-hint">
Sign in to at least one provider to get started with AI models.

View File

@@ -76,7 +76,6 @@ describe("PluginSlot", () => {
const shells = container.querySelectorAll("[data-plugin-slot]");
expect(shells).toHaveLength(2);
// Verify both shells have correct attributes
expect(shells[0]).toHaveAttribute("data-plugin-id", "plugin-x");
expect(shells[0]).toHaveAttribute("data-slot-id", "board-column-footer");
expect(shells[1]).toHaveAttribute("data-plugin-id", "plugin-y");
@@ -120,6 +119,22 @@ describe("PluginSlot", () => {
expect(vi.mocked(usePluginUiSlots)).toHaveBeenCalledWith("proj-1");
});
it("suppresses placeholder rendering when renderPlaceholder is false", () => {
const entry = createSlotEntry("settings-provider-card", "plugin-droid");
vi.mocked(usePluginUiSlots).mockReturnValue({
slots: [entry],
getSlotsForId: vi.fn(() => [entry]),
loading: false,
error: null,
});
const { container } = render(
<PluginSlot slotId="settings-provider-card" renderPlaceholder={false} />,
);
expect(container.firstChild).toBeNull();
});
it("returns null for empty string slotId", () => {
const getSlotsForId = vi.fn(() => []);
vi.mocked(usePluginUiSlots).mockReturnValue({