Files
fusion/packages/dashboard/app/components/PluginSlot.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

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>
);
}