Files
fusion/packages/dashboard/app/plugins/pluginSlotRegistry.tsx
gsxdsm e3f98253cc feat: Quality plugin — Task QA tab, preview servers, tests, and suggested cases (#2127)
## Summary

Adds a bundled **Quality** plugin (`fusion-plugin-quality`) that makes
task QA easier and more visual:

- **Task QA tab** (action-first): preview/test server for the task
worktree, allowlisted test runs, report viewer, screenshots CTA,
suggested test cases, CI handoff
- **Quality hub** (left sidebar): project-wide run history and preset
launches
- Host **task-detail slot context** (`taskId`, worktree, `projectId`) so
plugin tabs can scope correctly
- `superviseSpawn` re-exported on the plugin packaging shim for
published plugins
- Plan: `docs/plans/2026-07-14-001-feat-quality-plugin-plan.md`

## Design constraints

- Does **not** replace the merge gate — advisory orchestration only
- Composes Dev Server process patterns and artifact registry (no second
browser stack)
- Never free-form shell; never port 4040
- Full-suite requires explicit confirm

## Test plan

- [x] `pnpm --filter @fusion-plugin-examples/quality test` (15 tests)
- [x] PluginSlot unit tests still pass
- [ ] Enable Quality plugin in dashboard Settings → Built-in Plugins
- [ ] Open Task Detail → **QA** tab with a worktree; start preview, run
verify:fast, generate suggestions
- [ ] Open left sidebar **Quality** hub and list runs
- [ ] Confirm merge gate / PR checks unchanged

## Residual / follow-up (same plan, later units)

- Deeper hub CI (host route)
- Full browser-verification toggle UX + agent QA sessions (U7/U9/U10)
- Richer screenshots gallery wiring to live artifacts API
- Test plans CRUD polish

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added the Quality plugin with a project Quality hub and task-focused
QA tab.
* Added test runs, reports, preview server controls, suggested test
cases, and run history.
* Added configurable test presets, cancellation, status tracking, and
safe command execution.
* Added experimental-feature controls for enabling Quality
functionality.
* Bundled Quality with the CLI and made it available through the plugin
manager.

* **Documentation**
* Added Quality plugin guidance, terminology, configuration details, and
implementation planning documentation.

* **Bug Fixes**
* Improved process supervision so command failures and shutdown timers
are handled safely.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-15 20:28:11 -07:00

161 lines
5.0 KiB
TypeScript

import type { ComponentType, ReactNode } from "react";
import { lazy, Suspense, createElement } from "react";
import { useTranslation } from "react-i18next";
import type { PluginUiSlotEntry } from "../api";
import { DroidCliProviderCard } from "../components/DroidCliProviderCard";
export interface PluginSlotHostActions {
refreshAuthProviders?: () => void;
openSettingsSection?: (section: string) => void;
openModelOnboarding?: () => void;
}
/*
FNXC:Quality 2026-07-14-21:50:
Task-detail plugin tabs need host-injected task context (taskId, worktree, projectId).
Without this bag Task QA cannot scope runs, preview servers, or suggestions.
*/
export interface PluginSlotTaskContext {
taskId?: string;
worktree?: string;
projectId?: string;
title?: string;
modifiedFiles?: string[];
}
export interface PluginSlotComponentProps {
entry: PluginUiSlotEntry;
actions?: PluginSlotHostActions;
context?: PluginSlotTaskContext;
taskId?: string;
worktree?: string;
projectId?: string;
}
interface PluginSlotRegistration {
pluginId: string;
slotId: string;
componentPath: string;
component: ComponentType<PluginSlotComponentProps>;
}
function DroidSettingsProviderCard({ actions }: PluginSlotComponentProps): ReactNode {
return (
<DroidCliProviderCard
compact
authenticated={false}
onToggled={() => {
actions?.refreshAuthProviders?.();
}}
/>
);
}
function DroidOnboardingProviderCard({ actions }: PluginSlotComponentProps): ReactNode {
return (
<DroidCliProviderCard
authenticated={false}
onToggled={() => {
actions?.refreshAuthProviders?.();
}}
/>
);
}
function DroidOnboardingSetupHelp(): ReactNode {
const { t } = useTranslation("app");
return (
<p className="onboarding-helper-text" data-testid="droid-onboarding-setup-help">
{t("plugins.droidOnboardingTip", "Tip: Enable Droid CLI to reuse your Factory AI subscription without adding an API key.")}
</p>
);
}
function DroidPostOnboardingRecommendation({ actions }: PluginSlotComponentProps): ReactNode {
const { t } = useTranslation("app");
return (
<div className="post-onboarding-recommendations__item" data-testid="droid-post-onboarding-recommendation">
<span className="post-onboarding-recommendations__item-text">
<strong>{t("plugins.droidRecommendTitle", "Enable Droid CLI")}</strong>
<span>{t("plugins.droidRecommendDesc", "Use your local Droid CLI session as an AI provider in Fusion.")}</span>
</span>
<button type="button" className="btn btn-sm" onClick={() => actions?.openSettingsSection?.("authentication")}>
{t("plugins.openAuthentication", "Open Authentication")}
</button>
<button type="button" className="btn btn-sm" onClick={() => actions?.openModelOnboarding?.()}>
{t("plugins.openOnboarding", "Open Onboarding")}
</button>
</div>
);
}
const LazyQualityQaTab = lazy(async () => {
const mod = await import("@fusion-plugin-examples/quality/qa-tab");
return { default: mod.QualityQaTabSlot ?? mod.default };
});
function QualityTaskDetailTab(props: PluginSlotComponentProps): ReactNode {
return (
<Suspense fallback={null}>
{createElement(LazyQualityQaTab, {
entry: props.entry,
actions: props.actions,
context: props.context,
taskId: props.taskId ?? props.context?.taskId,
worktree: props.worktree ?? props.context?.worktree,
projectId: props.projectId ?? props.context?.projectId,
})}
</Suspense>
);
}
const REGISTRY: PluginSlotRegistration[] = [
{
pluginId: "fusion-plugin-quality",
slotId: "task-detail-tab",
componentPath: "./qa-tab",
component: QualityTaskDetailTab,
},
{
pluginId: "fusion-plugin-droid-runtime",
slotId: "settings-provider-card",
componentPath: "./components/settings-provider-card.js",
component: DroidSettingsProviderCard,
},
{
pluginId: "fusion-plugin-droid-runtime",
slotId: "settings-integration-card",
componentPath: "./components/settings-integration-card.js",
component: DroidSettingsProviderCard,
},
{
pluginId: "fusion-plugin-droid-runtime",
slotId: "onboarding-provider-card",
componentPath: "./components/onboarding-provider-card.js",
component: DroidOnboardingProviderCard,
},
{
pluginId: "fusion-plugin-droid-runtime",
slotId: "onboarding-setup-help",
componentPath: "./components/onboarding-setup-help.js",
component: DroidOnboardingSetupHelp,
},
{
pluginId: "fusion-plugin-droid-runtime",
slotId: "post-onboarding-recommendation",
componentPath: "./components/post-onboarding-recommendation.js",
component: DroidPostOnboardingRecommendation,
},
];
export function resolvePluginSlotComponent(entry: PluginUiSlotEntry): ComponentType<PluginSlotComponentProps> | null {
const hit = REGISTRY.find(
(candidate) =>
candidate.pluginId === entry.pluginId
&& candidate.slotId === entry.slot.slotId
&& candidate.componentPath === entry.slot.componentPath,
);
return hit?.component ?? null;
}