feat(ai): PR3a — FE tri-state ui.aiMode migration + define-once SampleBadge - #293
Conversation
v2 AI P0 PR3a design: migrate FE aiPreview->tri-state ui.aiMode, an Off|Preview segmented selector (Live deferred to PR3b), a define-once SampleBadge sample-data treatment across all Preview surfaces, and the architectural-invariants tri-state update. Backend untouched (consumes the PR2 wire contract). Revised after two ce-doc-review passes (7 personas each): pass-1 found the false readKey/writeKey claim, the inbox/Activity-rail truthfulness hole, replace-not-duplicate the existing sample labels, the SegmentedControl type cast, and the test-fixture migration; pass-2 found the under-scoped e2e migration (inbox/ai-gating-sweep specs + e2e not type-checked) and the InboxPage vs InboxRow badge placement.
13-task TDD plan from the approved PR3a spec: add ui.aiMode, migrate all consumers (context/hooks/selector/SampleBadge across all Preview surfaces), then remove aiPreview last so every commit compiles; full e2e migration + cross-surface badge coverage; invariant-doc update. Code blocks built from verbatim current source.
Plan pass-1: fixed AiSummaryCard test (non-null summary), hunk-test import depth, clarity on writeKey/useAiGate-imports/full-file-replacement; rejected 3 false-positive import-path P0s (verified correct vs sibling imports). Plan pass-2: visual baselines use the CI linux loop (not local win32 --update-snapshots); added the missing PreSubmitValidatorCard unit test (sweep never opens SubmitDialog); split the e2e false-green guard across nav; named the 3 mocks Task 10 must clean; fixed ai-gating-sweep return-type annotation + line refs; reordered Task 7 to test-first; expanded the e2e capability-mock migration. Spec: realigned the egress-consent dependency to the deferred-tracking-issue decision (PR-body callout interim, issue at PR3b planning).
…ontext
The aimode test only exercised the happy path, so the new writeKey('ui.ai.mode')
arm (reached only on POST-failure rollback) and readKey('ui.ai.mode') (used to
capture priorValue) were never functionally verified — both could be wrong and
the test would stay green. Mirrors the per-key rollback convention already
established in __tests__/hooks.test.tsx for 'theme' and 'inbox.sections.*'.
Adds a rollback-path case: reject the POST, assert the mode reverts to its prior
value (not silently left as 'preview'), an error toast fires, and — the
discriminating assertion — inbox.sections is NOT corrupted with a spurious ''
key. That corruption is exactly what the generic inbox.sections fallthrough
would produce if the dedicated ui.ai.mode writeKey arm were missing; verified the
test fails (received ['']) when the arm is removed and passes ([]) when present.
style(ai): remove dead chip CSS orphaned by SampleBadge swap The `.aiSummaryChip` (AiSummaryCard.module.css) and `.ai-validator-card__chip` (tokens.css) rules were only consumed by the `muted` chip wrapper divs removed when the hardcoded sample strings were replaced with SampleBadge. Grep confirms no remaining consumers in frontend/, so drop the orphaned rules alongside the markup they styled. @
Task 7 review fixes:
- Add StaleDraftRow.sample.test.tsx asserting the SampleBadge renders in
the aiSuggestion branch in preview and is absent in off, matching the
per-surface convention established by AiHunkAnnotation.sample.test.tsx
and sampleSurfaces.test.tsx. The draft-suggestion half of this commit
series previously shipped untested.
- Replace the AiHunkAnnotation fixture's invalid tone 'suggestion' (not
an AnnotationTone) and the `as unknown as HunkAnnotation` double-cast
with a complete, real-tone ('calm') HunkAnnotation literal so the
fixture reflects the wire contract (path, hunkIndex, body, tone).
…truth) Drops `aiPreview: boolean` from `UiPreferences`, removes the `'aiPreview'` union member and all `readKey`/`writeKey` arms from `PreferencesContext`, and scrubs every typed preference-literal mock that still carried the field. `ui.aiMode` is now the sole source of truth for AI gating in the FE.
…migration # Conflicts: # frontend/src/pages/InboxPage.tsx
There was a problem hiding this comment.
Pull request overview
This PR migrates the frontend’s AI preference model from the legacy boolean ui.aiPreview to the backend’s tri-state ui.aiMode (off | preview | live), updates the Settings UI to an Off/Preview selector, and standardizes “sample content” labeling across all Preview AI surfaces via a single SampleBadge component.
Changes:
- Replace the Appearance “AI preview” switch with an Off | Preview segmented selector that writes
set('ui.ai.mode', ...), while treating aliveconfig as “Preview” in the UI without posting on render. - Update AI gating/capability derivation to use
ui.aiModeand adduseIsSampleMode(); remove the legacyaiPreviewpreference field from FE types/mocks/tests. - Introduce
SampleBadgeand wire it into all Preview/sample AI surfaces; migrate unit + e2e tests and update architecture/spec docs accordingly.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/styles/tokens.css | Removes now-unused pre-submit validator preview chip styling. |
| frontend/src/pages/InboxPage.tsx | Adds region-level SampleBadge for inbox enrichment surface. |
| frontend/src/hooks/useCapabilities.ts | Derives capabilities from ui.aiMode === 'preview'. |
| frontend/src/hooks/useAiGate.ts | Gates on aiMode !== 'off' and adds useIsSampleMode(). |
| frontend/src/hooks/useAiGate.unit.test.tsx | New unit coverage for two-factor gate + sample-mode predicate. |
| frontend/src/hooks/useAiGate.reactivity.test.tsx | Migrates reactivity regression test to ui.aiMode. |
| frontend/src/contexts/PreferencesContext.tsx | Migrates preference keying to ui.ai.mode and drops aiPreview. |
| frontend/src/contexts/PreferencesContext.aimode.test.tsx | New tests for ui.ai.mode dotted-key POST + rollback behavior. |
| frontend/src/components/Settings/panes/AppearancePane.tsx | Switch → SegmentedControl (Off/Preview) writing ui.ai.mode. |
| frontend/src/components/Settings/panes/AppearancePane.test.tsx | Updates settings tests to expect the new radiogroup-based control. |
| frontend/src/components/PrDetail/SubmitDialog/PreSubmitValidatorCard.tsx | Replaces legacy preview copy with SampleBadge. |
| frontend/src/components/PrDetail/Reconciliation/StaleDraftRow.tsx | Adds SampleBadge to stale-draft AI suggestion surface. |
| frontend/src/components/PrDetail/Reconciliation/StaleDraftRow.sample.test.tsx | New test asserting badge presence/absence by mode. |
| frontend/src/components/PrDetail/OverviewTab/OverviewTab.tsx | Sources PrDescription.aiPreview from useAiGate('summary'). |
| frontend/src/components/PrDetail/OverviewTab/AiSummaryCard.tsx | Replaces legacy preview copy with SampleBadge. |
| frontend/src/components/PrDetail/OverviewTab/AiSummaryCard.module.css | Removes now-unused “chip” styling. |
| frontend/src/components/PrDetail/FilesTab/FileTree.tsx | Adds region SampleBadge to file-tree focus region header. |
| frontend/src/components/PrDetail/FilesTab/DiffPane/AiHunkAnnotation.tsx | Adds inline solid SampleBadge in hunk annotation meta row. |
| frontend/src/components/PrDetail/FilesTab/DiffPane/AiHunkAnnotation.sample.test.tsx | New test asserting badge presence/absence by mode. |
| frontend/src/components/Ai/SampleBadge.tsx | New define-once Preview-only “Sample” marker component. |
| frontend/src/components/Ai/SampleBadge.module.css | Styling for SampleBadge (inline/region/solid variants). |
| frontend/src/components/Ai/SampleBadge.test.tsx | Unit tests for SampleBadge render conditions and variants. |
| frontend/src/components/Ai/AiComposerAssistant.tsx | Replaces legacy preview copy with SampleBadge + remaining text. |
| frontend/src/components/Ai/sampleSurfaces.test.tsx | New test asserting SampleBadge replaces legacy strings on key cards. |
| frontend/src/components/ActivityRail/ActivityRail.tsx | Adds region SampleBadge to Activity rail header. |
| frontend/src/components/ActivityRail/ActivityRail.sample.test.tsx | New test asserting ActivityRail badge presence/absence by mode. |
| frontend/src/api/types.ts | Adds AiMode and replaces ui.aiPreview with ui.aiMode in UI prefs. |
| frontend/e2e/settings-modal-visual.spec.ts | Migrates settings mocks to accept ui.ai.mode dotted-path POST. |
| frontend/e2e/settings-flow.spec.ts | Migrates settings flow to ui.aiMode + ui.ai.mode POST handling. |
| frontend/e2e/inbox.spec.ts | Migrates inbox AI toggle path/handlers to ui.ai.mode + aiMode coupling. |
| frontend/e2e/helpers/replace-mocks.ts | Updates default preferences fixture to ui.aiMode. |
| frontend/e2e/fixtures/preferences.ts | Updates preferences fixture to ui.aiMode. |
| frontend/e2e/ai-gating-sweep.spec.ts | Migrates sweep to radio selector + asserts SampleBadge cross-surface. |
| frontend/e2e/a11y-audit.spec.ts | Updates default preferences to ui.aiMode. |
| frontend/tests/useAiGate.test.tsx | Updates legacy mirror tests to ui.aiMode. |
| frontend/tests/PrHeader.test.tsx | Updates Ask AI gating test setup to aiMode: 'preview'. |
| frontend/tests/PreferencesContext.test.tsx | Updates mock preferences shape to ui.aiMode. |
| frontend/tests/OverviewTab.test.tsx | Updates overview tests for ui.aiMode and SampleBadge expectations. |
| frontend/tests/InboxPage.test.tsx | Updates inbox mocks to ui.aiMode and stubs useIsSampleMode. |
| frontend/tests/hooks.test.tsx | Updates hooks tests to expect ui.aiMode in preferences. |
| frontend/tests/header.test.tsx | Updates header tests’ preferences fixture to ui.aiMode. |
| frontend/tests/appearance-sync.test.tsx | Updates appearance sync test fixtures to ui.aiMode. |
| frontend/tests/app.test.tsx | Updates app test preference mocks to ui.aiMode. |
| frontend/tests/AiSummaryCard.test.tsx | Updates to assert SampleBadge (and removes legacy string assertion). |
| docs/specs/2026-06-07-v2-ai-p0-pr3a-fe-mode-migration-design.md | New design spec for PR3a migration + sample-label treatment. |
| docs/plans/2026-06-07-v2-ai-p0-pr3a-fe-mode-migration.md | New detailed implementation plan for PR3a tasks. |
| .ai/docs/architectural-invariants.md | Updates AI invariant to tri-state model + SampleBadge requirement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…il layout, invariant title
…styling hook, stale comments
Follow-ups filed (non-blocking)
Still deferred
|
|
@claude review |
|
Claude finished @prpande's task in 3m 41s —— View job PR Review: feat(ai): PR3a — FE tri-state ui.aiMode migration + define-once SampleBadgeTasks
SummaryThis is a well-scoped, clearly documented migration. The architecture is sound — the two-factor gate ( Stale Comments (non-blocking, but worth cleaning before merge)1. // shape: since #221 the SPA derives capabilities from the shared aiPreview
// preference (useCapabilities) rather than calling this endpointShould read 2. // capabilities.composerAssist + preferences.aiPreview via useAiGate,3. // returns []; under aiPreview the slot renders frontend-side canned data —4. // Canned validator results when aiPreview is on; [] otherwiseDesign Observations (no action required)
Live mode visual ambiguity: A hand-edited Test CoverageCoverage is comprehensive. One minor gap:
SecurityThe egress-consent callout is correct and the current P0 safeguard (Live → AllOff locally) is an appropriate latent-risk mitigation. The hard prerequisite tracking for PR3b is clear. No other security concerns. VerdictApproves with the stale comments noted. All Copilot round-1 and round-2 findings were addressed. The architecture (tri-state gate, define-once badge, reactive preferences rollback) is clean and the forward-compat shape for D112 is correctly scaffolded. The four stale |
|
Thanks — addressed in
Verified via direct-binary prettier + eslint + build (clean) and |
Overview
PR3a migrates the PRism frontend off the legacy boolean
ui.aiPreviewonto PR2's tri-stateui.aiMode(off | preview | live), ships an Off | Preview mode selector (Live is plumbed in the type but intentionally not rendered in P0), and adds a define-onceSampleBadgemarker to every Preview/sample AI surface. Frontend + docs only — zero backend change (consumes the PR2 wire contract that already acceptsui.ai.modeand emitsaiMode).Implements
docs/specs/2026-06-07-v2-ai-p0-pr3a-fe-mode-migration-design.mdperdocs/plans/2026-06-07-v2-ai-p0-pr3a-fe-mode-migration.md.Changes
ui.aiModeis the FE source of truth.AppearancePaneSwitch →SegmentedControl(Off/Preview), writingset('ui.ai.mode', mode). Aliveconfig displays as Preview with no POST on render.aiMode==='preview' ? AllOn : AllOff) — no/api/capabilitiesfetch; preserves the reactive architecture and its no-fetch regression test.useAiGatetwo-factor gate(capability) && aiMode!=='off'; newuseIsSampleMode()predicate (true only in Preview).SampleBadgerenders only in Preview; replaces the 3 scattered hardcoded "AI preview — sample content…" strings and adds marking to 5 previously-unmarked surfaces (hunk annotations, stale-draft suggestions, activity rail, inbox enrichment, file-focus).aiPreviewpreference field fully removed (type,PreferenceKey/readKey/writeKey arms, all mocks, both test trees). The component prop namedaiPreviewonFileTree/PrDescriptionis retained (carries theuseAiGatevalue).ui.ai.mode+ radio locators + cross-surface badge assertions (with explicit false-green guards)..ai/docs/architectural-invariants.mdupdated: binary-gate → tri-stateAiMode {Off,Preview,Live}+ a truthful-by-defaultSampleBadgeinvariant.frontend/__tests__/mirror suite ontoaiMode(a parallel test tree the original plan missed), and merged V2 (112 commits) into the branch — one orthogonal conflict inInboxPage.tsx(V2'sSpinner→InboxSkeletonloading refactor vs theSampleBadgeregion marker) resolved.PR3a ships no server-side egress-consent guard. PR3b's per-provider egress-consent gate MUST land before any P1 work registers a real
LiveAI seam — otherwise a hand-editedui.ai.mode=liveconfig could egress PR content to a provider with no user consent. In P0 this is latent (Live resolves to all-nine-flags-false; no real seams exist), but it is a hard prerequisite for P1. Filing the dedicated tracking issue is deferred to PR3b planning (maintainer decision); this callout is the interim record.No other security impact — no auth/crypto/endpoint/input-validation changes; backend untouched.
Testing
npm run build+npm run lintclean.linuxvisual-baseline regen happen via CI. The Switch→SegmentedControl swap and the SampleBadge additions repixel several visual specs, and CI compares thelinuxbaselines (local--update-snapshotsonly writeswin32), so the changed visual specs are expected to fail on the first CI run and be regenerated from thee2e-resultsartifact.V2: SHIP (all priorities clean).Related Work
docs/specs/2026-06-05-v2-ai-roadmap-design.md(§2.3 / §4 / §7 P0).V2).V2, notmain, per the v2 integration-branch convention.