diff --git a/apps/desktop/src/pages/Rubrics.tsx b/apps/desktop/src/pages/Rubrics.tsx index 3e2bdc72..02eaade5 100644 --- a/apps/desktop/src/pages/Rubrics.tsx +++ b/apps/desktop/src/pages/Rubrics.tsx @@ -23,7 +23,13 @@ import { saveReviewConfig, type StandardsPack, } from '@/lib/review-service'; -import { getStandardsPackUsage, isTauriAvailable } from '@/lib/tauri-ipc'; +import { + getRubricSettings, + isTauriAvailable, + type RubricSettingsReceipt, + saveRubricPack, + setActiveRubricPack, +} from '@/lib/tauri-ipc'; function fallbackConfig(): ReviewConfig { return { @@ -77,47 +83,76 @@ export default function Rubrics({ embedded = false }: { embedded?: boolean }) { const [usage, setUsage] = useState>({}); const [expandedPreview, setExpandedPreview] = useState(null); const [copiedPreview, setCopiedPreview] = useState(null); + const [syncIssue, setSyncIssue] = useState(null); const packs = getStandardsPacks(config); const activePack = getActiveStandardsPack(config); const customRules = config.customRules ?? []; - // Usage is keyed by pack NAME (the value persisted on each review), not id. useEffect(() => { if (!isTauriAvailable()) return; let cancelled = false; - getStandardsPackUsage() - .then((rows) => { + getRubricSettings(loadReviewConfig()) + .then((receipt) => { if (cancelled) return; - const map: Record = {}; - for (const row of rows) { - map[row.standards_pack] = { - reviewCount: row.review_count, - totalFindings: row.total_findings, - }; - } - setUsage(map); + applyCanonicalReceipt(receipt); }) - .catch(() => { - // Non-fatal — packs simply show "no usage yet". + .catch((error) => { + if (cancelled) return; + setSyncIssue(error instanceof Error ? error.message : String(error)); }); return () => { cancelled = true; }; }, []); - function persist(next: ReviewConfig) { + function applyCanonicalReceipt(receipt: RubricSettingsReceipt) { + const standardsPacks = receipt.packs + .filter((pack) => !pack.built_in) + .map(({ id, name, focus, checks }) => ({ id, name, focus, checks })); + const persisted: ReviewConfig = { + customRules: receipt.custom_rules, + standardsPacks, + ...(receipt.active_pack_id ? { activeStandardsPack: receipt.active_pack_id } : {}), + }; + saveReviewConfig(persisted); + setConfig({ + ...persisted, + activeStandardsPack: receipt.active_pack_id ?? DEFAULT_STANDARDS_PACKS[0].id, + }); + setUsage( + Object.fromEntries( + receipt.packs.map((pack) => [ + pack.id, + { reviewCount: pack.review_count, totalFindings: pack.total_findings }, + ]) + ) + ); + setSyncIssue(null); + setSaved(true); + window.setTimeout(() => setSaved(false), 1600); + } + + function persistLocal(next: ReviewConfig) { setConfig(next); saveReviewConfig(next); setSaved(true); window.setTimeout(() => setSaved(false), 1600); } - function selectPack(packId: string) { - persist({ ...config, activeStandardsPack: packId }); + async function selectPack(packId: string) { + if (!isTauriAvailable()) { + persistLocal({ ...config, activeStandardsPack: packId }); + return; + } + try { + applyCanonicalReceipt(await setActiveRubricPack(packId)); + } catch (error) { + setSyncIssue(error instanceof Error ? error.message : String(error)); + } } - function clonePack(source: StandardsPack) { + async function clonePack(source: StandardsPack) { const cloneName = `${source.name} (copy)`; const cloneId = uniquePackId(makePackId(cloneName), packs); const clone: StandardsPack = { @@ -126,12 +161,21 @@ export default function Rubrics({ embedded = false }: { embedded?: boolean }) { focus: source.focus, checks: [...source.checks], }; - persist({ - ...config, - activeStandardsPack: clone.id, - standardsPacks: [...(config.standardsPacks ?? []), clone], - }); - setExpandedPreview(clone.id); + if (!isTauriAvailable()) { + persistLocal({ + ...config, + activeStandardsPack: clone.id, + standardsPacks: [...(config.standardsPacks ?? []), clone], + }); + setExpandedPreview(clone.id); + return; + } + try { + applyCanonicalReceipt(await saveRubricPack(clone)); + setExpandedPreview(clone.id); + } catch (error) { + setSyncIssue(error instanceof Error ? error.message : String(error)); + } } async function copyPreview(pack: StandardsPack) { @@ -145,7 +189,7 @@ export default function Rubrics({ embedded = false }: { embedded?: boolean }) { } } - function addCustomPack() { + async function addCustomPack() { const checks = draftChecks .split('\n') .map((line) => line.trim()) @@ -162,14 +206,26 @@ export default function Rubrics({ embedded = false }: { embedded?: boolean }) { checks, }; - persist({ - ...config, - activeStandardsPack: pack.id, - standardsPacks: [...(config.standardsPacks ?? []), pack], - }); - setDraftName(''); - setDraftFocus(''); - setDraftChecks(''); + if (!isTauriAvailable()) { + persistLocal({ + ...config, + activeStandardsPack: pack.id, + standardsPacks: [...(config.standardsPacks ?? []), pack], + }); + setDraftName(''); + setDraftFocus(''); + setDraftChecks(''); + return; + } + + try { + applyCanonicalReceipt(await saveRubricPack(pack)); + setDraftName(''); + setDraftFocus(''); + setDraftChecks(''); + } catch (error) { + setSyncIssue(error instanceof Error ? error.message : String(error)); + } } return ( @@ -214,6 +270,11 @@ export default function Rubrics({ embedded = false }: { embedded?: boolean }) { Saved )} + {syncIssue && ( +
+ Canonical rubric sync failed. Existing local settings were kept: {syncIssue} +
+ )}
@@ -239,7 +300,7 @@ export default function Rubrics({ embedded = false }: { embedded?: boolean }) {
diff --git a/apps/desktop/src/pages/TRex.tsx b/apps/desktop/src/pages/TRex.tsx index 11991dc9..8aa2e6ff 100644 --- a/apps/desktop/src/pages/TRex.tsx +++ b/apps/desktop/src/pages/TRex.tsx @@ -408,7 +408,7 @@ export default function TRex() { { @@ -439,7 +439,10 @@ export default function TRex() { onCleanup={handleWarmCleanup} /> - + diff --git a/apps/desktop/tests/e2e/review-warm-evidence.spec.ts b/apps/desktop/tests/e2e/review-warm-evidence.spec.ts index 3bb2e647..0cc4a3c0 100644 --- a/apps/desktop/tests/e2e/review-warm-evidence.spec.ts +++ b/apps/desktop/tests/e2e/review-warm-evidence.spec.ts @@ -412,7 +412,8 @@ test('Review presents deterministic coverage and rejected candidate counts', asy await expect(decision.getByRole('link', { name: 'Runtime evidence' })).toBeVisible(); }); -test('Review shows readiness for an external review agent', async ({ page }) => { +test('Review shows readiness for an external review agent', async ({ page, context }) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); await installReviewMock(page, false); await navigateTo(page, '/review'); await waitForNoSpinners(page); diff --git a/apps/desktop/tests/e2e/settings.spec.ts b/apps/desktop/tests/e2e/settings.spec.ts index a5d350f5..a9f5648a 100644 --- a/apps/desktop/tests/e2e/settings.spec.ts +++ b/apps/desktop/tests/e2e/settings.spec.ts @@ -15,67 +15,10 @@ test.describe('Settings page', () => { consoleErrors.assertNoErrors(); }); - // ─── General tab ────────────────────────────────────────────────────── - - test('General tab is selected by default and shows AI Provider section', async ({ page }) => { - // "General" should be the active category - await expect(page.locator('text=General').first()).toBeVisible(); - - // AI Provider section heading - await expect(page.getByRole('heading', { name: 'AI Provider' })).toBeVisible(); - }); - - // ─── Provider dropdown ──────────────────────────────────────────────── - - test('Can select AI provider from dropdown', async ({ page }) => { - // The provider dropdown is a