## 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 -->
114 lines
3.4 KiB
TypeScript
114 lines
3.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { ErrorBoundary } from "./ErrorBoundary";
|
|
import { usePluginUiSlots } from "../hooks/usePluginUiSlots";
|
|
import {
|
|
resolvePluginSlotComponent,
|
|
type PluginSlotHostActions,
|
|
type PluginSlotTaskContext,
|
|
} from "../plugins/pluginSlotRegistry";
|
|
import "./PluginSlot.css";
|
|
|
|
interface PluginSlotProps {
|
|
/** The slot identifier to render (e.g., "task-detail-tab", "header-action") */
|
|
slotId: string;
|
|
/** Optional project ID for multi-project slot scoping */
|
|
projectId?: string;
|
|
/** Optional plugin IDs to restrict rendering to a subset of matching entries */
|
|
pluginIds?: string[];
|
|
/** Render unresolved entry shell states for unregistered slot components */
|
|
renderPlaceholder?: boolean;
|
|
/** Optional host-controlled callbacks that slot components can call */
|
|
actions?: PluginSlotHostActions;
|
|
/** Optional task context for task-detail-tab and similar surfaces */
|
|
context?: PluginSlotTaskContext;
|
|
taskId?: string;
|
|
worktree?: string;
|
|
}
|
|
|
|
function PluginSlotMissingComponent({ slotId, pluginId }: { slotId: string; pluginId: string }): ReactNode {
|
|
const { t } = useTranslation("app");
|
|
return (
|
|
<section
|
|
className="plugin-slot-shell"
|
|
data-plugin-slot
|
|
data-slot-id={slotId}
|
|
data-plugin-id={pluginId}
|
|
data-plugin-slot-state="missing-component"
|
|
role="status"
|
|
aria-live="polite"
|
|
>
|
|
<p className="plugin-slot-shell__title">{t("plugins.componentUnavailable", "Plugin component unavailable")}</p>
|
|
<p className="plugin-slot-shell__message">
|
|
{t("plugins.couldNotResolve", "The dashboard could not resolve this plugin surface from the static host registry.")}
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Renders plugin slot registrations for a host surface.
|
|
*/
|
|
export function PluginSlot({
|
|
slotId,
|
|
projectId,
|
|
pluginIds,
|
|
renderPlaceholder = true,
|
|
actions,
|
|
context,
|
|
taskId,
|
|
worktree,
|
|
}: PluginSlotProps): ReactNode {
|
|
const { getSlotsForId, loading, error } = usePluginUiSlots(projectId);
|
|
|
|
if (loading || error || !slotId) {
|
|
return null;
|
|
}
|
|
|
|
const matchingEntries = getSlotsForId(slotId).filter((entry) =>
|
|
pluginIds && pluginIds.length > 0 ? pluginIds.includes(entry.pluginId) : true,
|
|
);
|
|
|
|
if (matchingEntries.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const resolvedContext: PluginSlotTaskContext = {
|
|
...context,
|
|
projectId: context?.projectId ?? projectId,
|
|
taskId: context?.taskId ?? taskId,
|
|
worktree: context?.worktree ?? worktree,
|
|
};
|
|
|
|
return (
|
|
<ErrorBoundary level="page">
|
|
<>
|
|
{matchingEntries.map((entry, index) => {
|
|
const key = `${entry.pluginId}-${entry.slot.slotId}-${index}`;
|
|
const SlotComponent = resolvePluginSlotComponent(entry);
|
|
|
|
if (SlotComponent) {
|
|
return (
|
|
<SlotComponent
|
|
key={key}
|
|
entry={entry}
|
|
actions={actions}
|
|
context={resolvedContext}
|
|
taskId={resolvedContext.taskId}
|
|
worktree={resolvedContext.worktree}
|
|
projectId={resolvedContext.projectId}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (!renderPlaceholder) {
|
|
return null;
|
|
}
|
|
|
|
return <PluginSlotMissingComponent key={key} slotId={entry.slot.slotId} pluginId={entry.pluginId} />;
|
|
})}
|
|
</>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|