diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index ec877ab9fa..5cc5472fa4 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -1175,3 +1175,9 @@ This file is append-only. Never rewrite or delete an existing review record; app
| 2026-07-28 | PR #1289 / `codex/rag-reliability-final` | `a1ca6a016490e4d4b564edd3d553d87fed3071df` | Protected-main RAG reliability, clinical-governance and release review | APPROVE. Independent retrieval, governance and fallback reviews found and fixed three merge blockers: global chunk-query alias overreach was narrowed to the measured clozapine blood-count action shape; legacy private source reviews remain on the deployed v1 RPC while unapplied-v2 paths fail explicitly; and source-backed review fallback is now a zero-tolerance blocking metric with reconciled evidence. Final rereviews found no P0-P2. PR #1288 was superseded without force-push after GitGuardian correctly rejected a token-shaped fake fixture; the clean replacement tree is byte-identical to the reviewed tree and both secret scanners pass. The additive BMJ attestation migration remains unapplied and BMJ stays unverified pending qualified human action. | Exact application tree `verify:pr-local` PASS: format, zero-warning lint, typecheck, 403 files and 4,101 tests passed with 2 skipped, production build/client-secret scan, and 36 offline RAG fixtures. Live 36-case canary PASS with document/content recall 1.0, zero failed cases and zero per-case document/content RR regressions; three cache-bypassed affected-path answer probes PASS with zero provider requests and zero generation cost. Earlier coverage PASS: 399 files, 4,062 passed and 2 skipped, RAG 86.83% statements and 90.79% lines. Hosted build, static, unit coverage, migration replay, Supabase Preview, Production UI, policy, Semgrep, Gitleaks and GitGuardian passed on the implementation tree; final evidence-only head requires the normal hosted rerun. |
| 2026-07-28 | PR #1289 / `codex/rag-reliability-final` | `d49af8acb42ccedbfe6c8b3f03d30769ba906ec7` | Bugbot review (exact head) | APPROVE. No high-confidence P0–P2. Application `src/`/`supabase/`/`tests/`/`scripts/` trees are unchanged vs prior APPROVE tip `a1ca6a01`; tip delta is docs evidence + clean merge from `main`. No unresolved `cursor[bot]` Bugbot threads. Residual risk: unapplied BMJ attestation migration (`#022`) correctly fails closed with `503 source_review_v2_unavailable` until qualified hosted apply. | Focused high-risk Vitest 358 + 1502 passed; merge-tree CLEAN vs `origin/main`; no provider-backed checks; no PR comment mutations. |
| 2026-07-28 | PR #1289 / `codex/rag-reliability-final` | `ab6ca036937bff1acaefbda8a5581d6d75f489b3` | Final current-main sync review | APPROVE pending fresh exact-head required checks. Merged current `origin/main` without conflict after its already-reviewed document-search and focus-path changes; no protected RAG, evaluation, migration, or RAG fixture surface changed from the live-canary application tree, and no P0-P2 finding remains. | `git merge-tree --write-tree` CLEAN before sync; branch-ledger guard and `git diff --check` PASS; prior exact application-tree `verify:pr-local` and live 36-case canary remain applicable; fresh hosted checks required. |
+| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `82c17f76` | Mode-switch prefetch review + merge restore | Prefetch mode homes on menu open; later reconciled with main per-option prefetch. Ledger restored append-only from main after mojibake rewrite. | Focused nav tests; check:branch-review-ledger; no provider checks. |
+| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `bc5b51c2` | CodeRabbit duplicate-ledger disposition | DISPOSITIONED / not actionable as a PR product delete. Near-duplicates already exist on origin/main as non-identical historical records; check:branch-review-ledger passes on main. Removing main-owned history from a feature PR would violate append-only. | substring counts on origin/main; ledger guard PASS; no provider checks. |
+| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `191707ee` | CodeRabbit malformed-ledger-row disposition | DISPOSITIONED / not actionable on this PR. MD056 cell-count warnings on ~700-722 are identical on origin/main. Fix belongs in dedicated main ledger hygiene if desired. | cell-count scan main vs tip; check:branch-review-ledger PASS; no provider checks. |
+| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `f4b55751` | CodeRabbit behavioral prefetch-test disposition | RESOLVED. Behavioral DOM coverage for menu-open prefetch paths (later adapted to per-option model). | focused vitest PASS; no provider checks. |
+| 2026-07-27 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `81005d18` | Codex mojibake-ledger disposition | RESOLVED. Historical rows restored byte-for-byte from origin/main; append-only thereafter. | exact prefix check; check:branch-review-ledger PASS; no provider checks. |
+| 2026-07-28 | PR #1275 / `codex/identify-and-fix-performance-issues-during-mode-switch` | `01469840` | CI/merge conflict closeout + Bugbot triage | RESOLVED merge conflict with origin/main (7ae4eb49 per-option prefetchModeHome). Dropped superseded bulk prefetchModeHomes; menu-open warms only highlighted option; focus/pointer scanning retained. CodeRabbit duplicate/malformed ledger threads dispositioned (main-owned). No open cursor[bot] Bugbot product defects. | merge-tree clean; focused vitest 12/12; ledger restored from main + appends; no provider checks. |
diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx
index d493f7afa9..373a4482af 100644
--- a/src/components/clinical-dashboard/master-search-header.tsx
+++ b/src/components/clinical-dashboard/master-search-header.tsx
@@ -852,9 +852,22 @@ export function MasterSearchHeader({
setScopeSheetOpen(false);
}
+ // Mode options are buttons (not Links), so Next cannot discover destinations.
+ // Prefetch only the mode the user is about to choose — the highlighted option
+ // on open, then whichever option receives focus/pointer while scanning.
+ function prefetchModeHome(modeId: AppModeId) {
+ if (modeId === searchMode) return;
+ const href = appModeHomeHref(modeId);
+ if (prefetchedModeHrefsRef.current.has(href)) return;
+ prefetchedModeHrefsRef.current.add(href);
+ router.prefetch(href);
+ }
+
function openModeMenuWithFocus(index: number) {
closeModeSurfaces();
const nextIndex = (index + visibleAppModeOptions.length) % visibleAppModeOptions.length;
+ const highlighted = visibleAppModeOptions[nextIndex];
+ if (highlighted) prefetchModeHome(highlighted.id);
const phoneLayout = currentUsesPhoneSearchLayout();
setUsesPhoneSearchLayout(phoneLayout);
setModeMenuFocusIndex(nextIndex);
@@ -872,6 +885,8 @@ export function MasterSearchHeader({
setModeMenuOpen(false);
return;
}
+ const highlighted = visibleAppModeOptions[selectedModeIndex];
+ if (highlighted) prefetchModeHome(highlighted.id);
setUsesPhoneSearchLayout(currentUsesPhoneSearchLayout());
setModeMenuFocusIndex(selectedModeIndex);
setModeMenuOpen(true);
@@ -923,14 +938,6 @@ export function MasterSearchHeader({
}
}
- function prefetchModeHome(modeId: AppModeId) {
- if (modeId === searchMode) return;
- const href = appModeHomeHref(modeId);
- if (prefetchedModeHrefsRef.current.has(href)) return;
- prefetchedModeHrefsRef.current.add(href);
- router.prefetch(href);
- }
-
function renderModeMenuOptions() {
return visibleAppModeOptions.map((mode, index) => {
const Icon = appModeIcons[mode.id];
diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts
index 1ef55e78f8..bfc15b223c 100644
--- a/tests/audit-navigation-auth-regressions.test.ts
+++ b/tests/audit-navigation-auth-regressions.test.ts
@@ -122,12 +122,29 @@ describe("audit navigation and auth regressions", () => {
"function renderModeMenuOptions()",
"const restoreActionMenuFocusRef",
);
+ const openModeMenuWithFocus = sourceSegment(
+ masterSearchHeaderSource,
+ "function openModeMenuWithFocus(",
+ "function toggleModeMenu(",
+ );
+ const toggleModeMenu = sourceSegment(
+ masterSearchHeaderSource,
+ "function toggleModeMenu(",
+ "function handleModeTriggerKeyDown(",
+ );
expect(masterSearchHeaderSource).toContain("function prefetchModeHome(modeId: AppModeId)");
expect(masterSearchHeaderSource).toContain("router.prefetch(href)");
expect(modeOptions).toContain("onFocus={() => prefetchModeHome(mode.id)}");
expect(modeOptions).toContain("onPointerEnter={() => prefetchModeHome(mode.id)}");
+ // Menu-open paths warm only the highlighted option — never every visible home.
+ expect(openModeMenuWithFocus).toContain("prefetchModeHome(highlighted.id)");
+ expect(toggleModeMenu).toContain("prefetchModeHome(highlighted.id)");
+ expect(masterSearchHeaderSource).not.toContain("function prefetchModeHomes(");
expect(masterSearchHeaderSource).not.toContain("visibleAppModeOptions.forEach((mode) => router.prefetch");
+ expect(masterSearchHeaderSource).not.toContain(
+ "new Set(visibleAppModeOptions.map((mode) => appModeHomeHref(mode.id)))",
+ );
});
it("defers cross-mode search on narrow screens until expansion except for completed answers", () => {
diff --git a/tests/mode-menu-prefetch.dom.test.tsx b/tests/mode-menu-prefetch.dom.test.tsx
new file mode 100644
index 0000000000..d84c8c7241
--- /dev/null
+++ b/tests/mode-menu-prefetch.dom.test.tsx
@@ -0,0 +1,115 @@
+/** @vitest-environment jsdom */
+
+import { render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import { MasterSearchHeader } from "@/components/clinical-dashboard/master-search-header";
+import { appModeHomeHref, visibleAppModeDefinitionsForSession } from "@/lib/app-modes";
+
+const router = vi.hoisted(() => ({
+ push: vi.fn(),
+ replace: vi.fn(),
+ prefetch: vi.fn(),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => router,
+}));
+
+vi.mock("@/lib/supabase/client", () => ({
+ useAuthSession: () => ({
+ status: "signed_out",
+ session: null,
+ isConfigured: true,
+ error: null,
+ signInWithEmail: vi.fn(),
+ signOut: vi.fn(),
+ }),
+}));
+
+vi.mock("@/components/clinical-dashboard/use-saved-registry-favourites", () => ({
+ useSavedRegistryFavourites: () => [],
+}));
+
+vi.mock("@/components/clinical-dashboard/search-command-context", () => ({
+ useSearchCommand: () => null,
+}));
+
+vi.mock("@/components/clinical-dashboard/universal-search-also-matches", () => ({
+ UniversalSearchAlsoMatches: () => null,
+}));
+
+function headerProps() {
+ return {
+ demoMode: false,
+ documents: [],
+ query: "",
+ searchMode: "answer" as const,
+ loading: false,
+ selectedDocumentIds: [] as string[],
+ queryMode: "auto" as const,
+ scopeFilters: {},
+ realDataReady: true,
+ canAccessFavourites: false,
+ onQueryChange: () => undefined,
+ onSearchModeChange: vi.fn(),
+ onAsk: () => undefined,
+ onClearQuery: () => undefined,
+ onClearScope: () => undefined,
+ onQueryModeChange: () => undefined,
+ onScopeFiltersChange: () => undefined,
+ onToggleScope: () => undefined,
+ queryModeOptions: [{ value: "auto" as const, label: "Auto" }],
+ };
+}
+
+function guestModeHomes() {
+ return visibleAppModeDefinitionsForSession({ authenticated: false, demoMode: false });
+}
+
+describe("mode menu home prefetch", () => {
+ beforeEach(() => {
+ router.push.mockReset();
+ router.replace.mockReset();
+ router.prefetch.mockReset();
+ });
+
+ it("prefetches a mode home when the user points at that option", async () => {
+ const user = userEvent.setup();
+ const documents = guestModeHomes().find((mode) => mode.id === "documents");
+ expect(documents).toBeTruthy();
+ const documentsHref = appModeHomeHref("documents");
+
+ render();
+ await user.click(screen.getByRole("button", { name: /Mode Answer/i }));
+ const menu = await screen.findByRole("menu", { name: "Choose app mode" });
+
+ // Opening on the current mode is a no-op; scanning another option warms it.
+ expect(router.prefetch).not.toHaveBeenCalledWith(documentsHref);
+ await user.hover(within(menu).getByRole("menuitemradio", { name: /Documents/i }));
+ expect(router.prefetch).toHaveBeenCalledWith(documentsHref);
+ const prefetched = new Set(router.prefetch.mock.calls.map(([href]) => href as string));
+ expect(prefetched.has(documentsHref)).toBe(true);
+ expect(prefetched.size).toBeLessThan(guestModeHomes().length);
+ });
+
+ it("prefetches the highlighted mode when openModeMenuWithFocus targets another home", async () => {
+ const user = userEvent.setup();
+ const modes = guestModeHomes();
+ const answerIndex = modes.findIndex((mode) => mode.id === "answer");
+ expect(answerIndex).toBeGreaterThanOrEqual(0);
+ const previous = modes[(answerIndex - 1 + modes.length) % modes.length];
+ expect(previous.id).not.toBe("answer");
+ const previousHref = appModeHomeHref(previous.id);
+
+ render();
+ const trigger = screen.getByRole("button", { name: /Mode Answer/i });
+ trigger.focus();
+ await user.keyboard("{ArrowUp}");
+ await screen.findByRole("menu", { name: "Choose app mode" });
+
+ expect(router.prefetch).toHaveBeenCalledWith(previousHref);
+ expect(new Set(router.prefetch.mock.calls.map(([href]) => href)).size).toBe(1);
+ });
+});