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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ export function PostOnboardingRecommendations({
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<PluginSlot slotId="post-onboarding-recommendation" />
|
||||
<PluginSlot slotId="post-onboarding-recommendation" renderPlaceholder={false} />
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user