/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * `RepairQueuePanel` — surface SDK-update revalidation results. * * Runs `ExtensionHostService.revalidateForSdk(currentSdk)` on mount, * lists each extension with compatibility status + test outcome, and * lets the user trigger an AI-assisted repair for items in the * `needsRepair` bucket. Repair routing seeds the chat with a fix * prompt; the chat panel then drives the regular authoring loop. * * Spec: docs/architecture/ai-customization/06-self-improvement.md §5. */ import { useCallback, useState } from 'react'; import { CheckCircle2, RefreshCcw, ShieldAlert, Wrench, X } from 'lucide-react'; import type { RevalidationItem, RevalidationSummary } from '@ifc-lite/extensions'; import { Button } from '@/components/ui/button'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useExtensionHost } from '@/sdk/ExtensionHostProvider'; import { useViewerStore } from '@/store'; import { toast } from '@/components/ui/toast'; import { HelpHint } from './HelpHint'; interface RepairQueuePanelProps { /** SDK version to revalidate against. Defaults to APP_VERSION. */ sdkVersion?: string; onClose?: () => void; } export function RepairQueuePanel({ sdkVersion, onClose }: RepairQueuePanelProps) { const host = useExtensionHost(); const queueChatPrompt = useViewerStore((s) => s.queueChatPrompt); const setChatPanelVisible = useViewerStore((s) => s.setChatPanelVisible); const setScriptPanelVisible = useViewerStore((s) => s.setScriptPanelVisible); const [summary, setSummary] = useState(); const [busy, setBusy] = useState(false); // SDK version comes from the Vite-injected __APP_VERSION__ define. // We deliberately do NOT fall back to '0.0.0' on miss — a fake low // version would flag every range as outdated and produce a wave of // false-positive repair prompts. const version = sdkVersion ?? (typeof __APP_VERSION__ === 'string' && __APP_VERSION__.length > 0 ? __APP_VERSION__ : undefined); const run = useCallback(async () => { if (!version) return; setBusy(true); try { const next = await host.revalidateForSdk(version); setSummary(next); } catch (err) { toast.error(`Revalidation failed: ${err instanceof Error ? err.message : String(err)}`); } finally { setBusy(false); } }, [host, version]); // Eager-run is gated behind an explicit user click. Mounting alone // shouldn't spin up sandboxes for every installed extension — that // can be expensive when many extensions are installed and outdated. const repairItem = (item: RevalidationItem) => { if (!version) return; queueChatPrompt(buildRepairPrompt(item, version)); setChatPanelVisible(true); setScriptPanelVisible(true); toast.success(`Routing repair for ${item.extensionId}…`); }; return (

Repair queue

{summary && ( SDK {summary.sdk} · {summary.needsRepair.length} need fixing )}

When the viewer SDK bumps, extensions whose declared engines.ifcLiteSdk range no longer matches are flagged here.

Run check spins up a sandbox for each outdated extension and runs its manifest tests against the new SDK. Failing tests get a Repair button that seeds chat with a fix prompt — the AI authoring loop produces the patched bundle.

Doesn't run automatically (each check spawns sandboxes).

{onClose && ( )}
{!version ? (
SDK version unknown — cannot revalidate. Set __APP_VERSION__ via Vite define.
) : !summary ? (
No compatibility check has run for this session.
) : summary.items.length === 0 ? (
No installed extensions
) : (
    {summary.items.map((item) => ( repairItem(item)} /> ))}
)}
); } function RepairRow({ item, onRepair, }: { item: RevalidationItem; onRepair: () => void; }) { const tone = item.outcome === 'pass' ? 'text-emerald-600 dark:text-emerald-400' : item.outcome === 'skipped' ? 'text-muted-foreground' : 'text-rose-600 dark:text-rose-400'; return (
  • {item.outcome === 'pass' ? ( ) : ( )} {item.extensionId} {item.outcome}
    Range {item.compatibility.declared} · {item.compatibility.reason}
    {item.tests && item.tests.failed > 0 && (
    {item.tests.failed} test{item.tests.failed === 1 ? '' : 's'} failed: {' '} {item.tests.results.find((r) => !r.passed)?.error}
    )}
    {itemNeedsRepair(item) && ( )}
  • ); } /** * Whether a row should show a Repair button. Mirrors the * `needsRepair` filter in `revalidateAgainstSdk` exactly — a failed * test OR a skipped extension whose declared range is outdated — so * the header count and the actionable rows never disagree. */ function itemNeedsRepair(item: RevalidationItem): boolean { return ( item.outcome === 'fail' || (item.outcome === 'skipped' && item.compatibility.status === 'outdated') ); } function buildRepairPrompt(item: RevalidationItem, sdk: string): string { const failures = item.tests?.results.filter((r) => !r.passed) ?? []; return [ `Repair extension ${item.extensionId} for SDK ${sdk}.`, '', `The declared engine range was \`${item.compatibility.declared}\` (status: ${item.compatibility.status}).`, '', failures.length > 0 ? `${failures.length} test${failures.length === 1 ? '' : 's'} failed under the new SDK:` : 'No failing tests were captured; revalidation flagged compatibility only.', ...failures.map((f) => `- ${f.name}: ${f.error ?? 'unknown'}`), '', 'Update the bundle so tests pass against the new SDK while keeping the same user-visible behaviour. Bump engines.ifcLiteSdk as appropriate.', ].join('\n'); }