Skip to content
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -623,3 +623,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-08-04 | claude/top-search-design-mockups-w53znc | b432448e4893a42d07558aff0dc04be797971231 | PR #1611 — results-band shelf Clear filter-only, memo deps, restored tests | Fixed two Qodo findings from merged #1555; mutation-tested guard added | tsc 0; eslint 0; vitest 4 files/59 tests; verify:pr-local blocked by lock parity (node 24.13 vs jsdom@30) |
| 2026-08-04 | claude/search-bar-decisions-doc | a7dea7f777255ade72878820a636413aaf9588af | search-bar handoff doc replacement + review fixes | Docs-only review fixes: mode/shelf accounting, Sort consumers, #230/#170 precision; removed unquoted-output claim from prior row | prettier --check . ; check:outstanding-issues ; docs:check-links ; docs:check-index |
| 2026-08-04 | claude/search-bar-decisions-doc | 3b4cd6e6bf1f36fb8aff098ce7d333641e0859d3 | search-bar handoff doc replacement + review fixes | Fixed CodeRabbit/Codex findings; Bugbot hosted stuck queued, local Bugbot-equivalent confirmed two P2 doc errors and rejected sheets-are-target finding. verify:pr-local PASS (docs scope). Decisive: prettier All matched files use Prettier code style!; outstanding-issues 228 rows next-id=231; docs link check passed: 1615; docs/codebase-index coverage OK | verify:pr-local (docs); prettier --check; check:outstanding-issues; docs:check-links; docs:check-index; check:branch-review-ledger |
| 2026-08-04 | codex/fix-mode-switching-and-loading-issues | 3e3b224a2ec13928d1e28173b1fc4c75d202d7d2 | PR #1607 unblock/fix | clean — behind 0, merge-tree clean, 0 unresolved threads, required CI in progress (no code fix) | merge-tree clean; behind_by 0; Unit/Build/Static/ProdUI in progress; no failing required |
90 changes: 85 additions & 5 deletions src/components/clinical-dashboard/global-search-shell.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -56,6 +56,7 @@ import {
import { useSettingsGuideFlow } from "@/components/clinical-dashboard/use-settings-guide-flow";
import { cn } from "@/components/ui-primitives";
import {
appModeDefinition,
appModeHomeHref,
isAppModeId,
isAppModeVisible,
Expand DownExpand Up@@ -117,6 +118,16 @@ type GlobalSearchShellProps = {
fallback?: ReactNode;
};

type PendingModeNavigation = {
mode: AppModeId;
pathname: string;
/** Destination search string (no leading `?`) so same-pathname homes wait for query clear. */
searchParamString: string;
/** URL at the moment the mode push was issued — used to detect superseding navigations. */
sourcePathname: string;
sourceSearchParamString: string;
};

export function GlobalSearchShell(props: GlobalSearchShellProps) {
const pathname = usePathname() ?? "/";

Expand DownExpand Up@@ -387,6 +398,7 @@ function GlobalStandaloneSearchShellBody({
const [syncedSearchParamString, setSyncedSearchParamString] = useState(searchParamString);
const [syncedPathname, setSyncedPathname] = useState(pathname);
const [searchMode, setSearchMode] = useState<AppModeId>(resolvedSearchMode);
const [pendingModeNavigation, setPendingModeNavigation] = useState<PendingModeNavigation | null>(null);
const [queryMode, setQueryMode] = useState<ClinicalQueryMode>(
() => readSearchNavigationContext(searchParams).queryMode,
);
Expand DownExpand Up@@ -475,6 +487,37 @@ function GlobalStandaloneSearchShellBody({
setScopeFilters(nextSearchContext.scopeFilters);
}

// Imperative mode-menu navigation does not have Link's immediate pending UI:
// Next keeps the previous RSC page visible while it waits for the destination
// payload. Replace that stale page with the neutral route skeleton as soon as
// a mode is chosen, then release it when the destination lands — or when any
// other committed URL change supersedes the in-flight mode push (Back, New
// chat, sidebar link, a second mode pick). Destination checks include the
// query string so same-pathname returns (e.g. `/services?q=&run=1` → `/services`)
// keep the skeleton until the home URL actually commits; mode is still checked
// for `/` modes such as Answer, Documents, and Medication.
if (pendingModeNavigation) {
const reachedDestination =
pathname === pendingModeNavigation.pathname &&
resolvedSearchMode === pendingModeNavigation.mode &&
searchParamString === pendingModeNavigation.searchParamString;
const supersededWhilePending =
pathname !== pendingModeNavigation.sourcePathname ||
searchParamString !== pendingModeNavigation.sourceSearchParamString;
if (reachedDestination || supersededWhilePending) {
setPendingModeNavigation(null);
}
}

useEffect(() => {
if (!pendingModeNavigation) return undefined;
// A failed/blocked client navigation must not strand the application behind
// a permanent loading surface. Normal prefetched mode switches clear this as
// soon as the URL lands; this is only a conservative recovery path.
const timeout = window.setTimeout(() => setPendingModeNavigation(null), 10_000);
return () => window.clearTimeout(timeout);
}, [pendingModeNavigation]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

useEffect(() => {
// Submitted result views must not keep the dock focused. Composer focus
// pins both chrome edges (keyboard safety), which is what left Forms /
Expand DownExpand Up@@ -616,11 +659,39 @@ function GlobalStandaloneSearchShellBody({
openAccountSetup("favourites");
return;
}
setQuery("");
// Same-mode picks are load-bearing: the checked mode-menu option and every
// ModeActionPopup quick action route through changeMode to leave a detail /
// submitted URL and land on the clean mode home. Skip only a true no-op
// (already exactly on that home) when nothing else is in flight.
const href = appModeHomeHref(mode, { queryMode, scopeFilters });
const destination = new URL(href, window.location.origin);
const destinationSearch = destination.search.startsWith("?") ? destination.search.slice(1) : destination.search;
const alreadyOnDestination = pathname === destination.pathname && searchParamString === destinationSearch;

setMobileMenuOpen(false);

if (alreadyOnDestination) {
// Re-selecting the current mode while a different mode push is in flight
// must cancel the pending skeleton and re-affirm the current home so the
// in-flight navigation does not leave the user on the wrong page.
if (pendingModeNavigation && pendingModeNavigation.mode !== mode) {
setPendingModeNavigation(null);
router.push(href);
}
return;
}

setQuery("");
// Let the URL sync (render-time) own searchMode. Optimistic setSearchMode
// before pathname updates was the namespaced mode-switch reserve flip.
navigateToMode(mode);
setPendingModeNavigation({
mode,
pathname: destination.pathname,
searchParamString: destinationSearch,
sourcePathname: pathname,
sourceSearchParamString: searchParamString,
});
router.push(href);
}

function startNewAnswerChat() {
Expand DownExpand Up@@ -764,7 +835,7 @@ function GlobalStandaloneSearchShellBody({
documentTotal={0}
query={query}
searchMode={searchMode}
loading={false}
loading={pendingModeNavigation !== null}
selectedDocumentIds={[]}
queryMode={queryMode}
scopeFilters={scopeFilters}
Expand DownExpand Up@@ -930,7 +1001,7 @@ function GlobalStandaloneSearchShellBody({
Rendered in normal flow (sticky={false}) so it never contends with
the universal collapsing header or page-flow search chrome.
*/}
{searchMode !== "specifiers" && searchMode !== "formulation" ? (
{!pendingModeNavigation && searchMode !== "specifiers" && searchMode !== "formulation" ? (
<PageSecondaryNavigation
modeId={searchMode}
pathname={pathname}
Expand All@@ -942,7 +1013,16 @@ function GlobalStandaloneSearchShellBody({
) : null}
{/* Paint RSC mode-home HTML immediately. A ClientHydrationBoundary here
blanked every standalone mode until JS mounted (hard-load LCP hit). */}
<SearchCommandProvider value={searchCommandContextValue}>{children}</SearchCommandProvider>
<SearchCommandProvider value={searchCommandContextValue}>
{pendingModeNavigation ? (
<div aria-busy="true" aria-live="polite" data-testid="mode-navigation-loading">
<span className="sr-only">Loading {appModeDefinition(pendingModeNavigation.mode).label}</span>
<ModeHomeRouteLoading />
</div>
) : (
children
)}
</SearchCommandProvider>
</div>
</div>
</PhoneFooterLayerFrame>
Expand Down
14 changes: 13 additions & 1 deletion src/components/clinical-dashboard/master-search-header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -863,7 +863,19 @@ export function MasterSearchHeader({
const href = appModeHomeHref(modeId);
if (prefetchedModeHrefsRef.current.has(href)) return;
prefetchedModeHrefsRef.current.add(href);
router.prefetch(href);
router.prefetch(href, {
// Next's client cache can invalidate a prefetched RSC payload while this
// long-lived shared header remains mounted. Let the next pointer/focus
// intent warm it again instead of permanently treating the stale entry as
// prefetched for the rest of the session.
onInvalidate: () => {
prefetchedModeHrefsRef.current.delete(href);
},
// Next 16.2.12's public guide documents onInvalidate as the only optional
// field, while its bundled AppRouterInstance type incorrectly exposes the
// internal required `kind`. Keep the public API shape without importing a
// private router enum.
} as Parameters<typeof router.prefetch>[1]);
}

function openModeMenuWithFocus(index: number) {
Expand Down
29 changes: 19 additions & 10 deletions src/components/document-viewer/source-panels.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -797,7 +797,10 @@ export const IndexedTextPanel = memo(function IndexedTextPanel({
.map((chunk) => chunk.id)
.join(",")}`;
const previousAutoOpenDriverRef = useRef<string | null>(null);
const manualClosedDriverRef = useRef<string | null>(null);
// Track manual close in state (not only a ref) so selected/active chunk
// disclosures can stay React-controlled — imperative `.open = true` alone was
// lost across re-renders and left deep-linked hits collapsed in Production UI.
const [manualClosedDriver, setManualClosedDriver] = useState<string | null>(null);
const [compactOpen, setCompactOpen] = useState(Boolean(selectedChunkId));
// Deep-linked chunks and in-document search must keep the panel revealed even
// when the exclusive accordion briefly closes it (section jumps / sibling
Expand All@@ -808,20 +811,21 @@ export const IndexedTextPanel = memo(function IndexedTextPanel({
setPrevForceReveal(forceReveal);
if (forceReveal) setCompactOpen(true);
}
if (previousAutoOpenDriverRef.current !== autoOpenDriver) {
previousAutoOpenDriverRef.current = autoOpenDriver;
if (manualClosedDriver !== null) setManualClosedDriver(null);
}
const autoOpenSuppressed = Boolean(autoOpenDriver) && manualClosedDriver === autoOpenDriver;

useEffect(() => {
if (previousAutoOpenDriverRef.current !== autoOpenDriver) {
previousAutoOpenDriverRef.current = autoOpenDriver;
manualClosedDriverRef.current = null;
}
if (!autoOpenDriver || !autoOpenTargetId || manualClosedDriverRef.current === autoOpenDriver) return;
if (!autoOpenDriver || !autoOpenTargetId || autoOpenSuppressed) return;
const targetDisclosure = document.getElementById(`${idPrefix}-${autoOpenTargetId}`);
if (!(targetDisclosure instanceof HTMLDetailsElement)) return;
if (topLevelDisclosureRef.current) topLevelDisclosureRef.current.open = true;
const wasOpen = targetDisclosure.open;
openNestedSourceDisclosure(topLevelDisclosureRef.current, targetDisclosure);
if (!wasOpen) targetDisclosure.scrollIntoView({ block: "nearest", behavior: resolveScrollBehavior() });
}, [autoOpenDriver, autoOpenTargetId, idPrefix, targetAvailability]);
}, [autoOpenDriver, autoOpenTargetId, autoOpenSuppressed, idPrefix, targetAvailability]);

function moveHit(delta: number) {
if (visibleChunks.length === 0) return;
Expand All@@ -835,14 +839,14 @@ export const IndexedTextPanel = memo(function IndexedTextPanel({
const isDriverDisclosure = disclosure.id === `${idPrefix}-${autoOpenTargetId}`;
if (disclosure.open) {
disclosure.open = false;
if (isDriverDisclosure && autoOpenDriver) manualClosedDriverRef.current = autoOpenDriver;
if (isDriverDisclosure && autoOpenDriver) setManualClosedDriver(autoOpenDriver);
return;
}
if (isDriverDisclosure && autoOpenDriver) manualClosedDriverRef.current = null;
if (isDriverDisclosure && autoOpenDriver) setManualClosedDriver(null);
if (!isDriverDisclosure && autoOpenDriver && autoOpenTargetId) {
const driverDisclosure = document.getElementById(`${idPrefix}-${autoOpenTargetId}`);
if (driverDisclosure instanceof HTMLDetailsElement && driverDisclosure.open) {
manualClosedDriverRef.current = autoOpenDriver;
setManualClosedDriver(autoOpenDriver);
}
}
openNestedSourceDisclosure(topLevelDisclosureRef.current, disclosure);
Expand DownExpand Up@@ -998,6 +1002,10 @@ export const IndexedTextPanel = memo(function IndexedTextPanel({
visibleChunks.map((chunk) => {
const selected = selectedChunkId === chunk.id;
const active = activeHit?.id === chunk.id;
const isAutoOpenTarget = Boolean(autoOpenTargetId) && chunk.id === autoOpenTargetId;
// Keep deep-linked / active-hit passages React-controlled so a
// later render cannot collapse the citation the URL asked for.
const forceChunkOpen = isAutoOpenTarget && !autoOpenSuppressed;
const status = selected
? "Highlighted quoted passage"
: active
Expand All@@ -1012,6 +1020,7 @@ export const IndexedTextPanel = memo(function IndexedTextPanel({
data-testid={selected ? "highlighted-indexed-source-chunk" : "indexed-source-passage-disclosure"}
data-source-chunk-id={chunk.id}
data-source-active-hit={active || undefined}
open={forceChunkOpen ? true : undefined}
className={cn(
sourceCard,
"group/source-row overflow-hidden p-0 transition source-print",
Expand Down
3 changes: 2 additions & 1 deletion tests/audit-navigation-auth-regressions.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -134,7 +134,8 @@ describe("audit navigation and auth regressions", () => {
);

expect(masterSearchHeaderSource).toContain("function prefetchModeHome(modeId: AppModeId)");
expect(masterSearchHeaderSource).toContain("router.prefetch(href)");
expect(masterSearchHeaderSource).toContain("router.prefetch(href,");
expect(masterSearchHeaderSource).toContain("onInvalidate:");
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.
Expand Down
38 changes: 37 additions & 1 deletion tests/document-section-summary.dom.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -70,7 +70,9 @@ describe("IndexedTextPanel condensed reveal", () => {

const panel = screen.getByTestId("source-chunk-indexed-text-panel") as HTMLDetailsElement;
expect(panel.open).toBe(true);
expect(screen.getByTestId("highlighted-indexed-source-chunk")).toBeVisible();
const highlighted = screen.getByTestId("highlighted-indexed-source-chunk") as HTMLDetailsElement;
expect(highlighted).toBeVisible();
await waitFor(() => expect(highlighted.open).toBe(true));
expect(panel.querySelector("summary")).toHaveAttribute("aria-disabled", "true");

fireEvent.click(panel.querySelector("summary")!);
Expand All@@ -82,6 +84,7 @@ describe("IndexedTextPanel condensed reveal", () => {
});
await waitFor(() => expect(panel.open).toBe(true));
expect(screen.getByTestId("highlighted-indexed-source-chunk")).toBeVisible();
expect(highlighted.open).toBe(true);
});

it("keeps in-document search results revealed without a selected chunk", async () => {
Expand DownExpand Up@@ -128,6 +131,39 @@ describe("IndexedTextPanel condensed reveal", () => {
expect(screen.getByText("Hit 1 of 1")).toBeVisible();
});

it("keeps the deep-linked nested chunk disclosure open under condensed view", async () => {
const props = {
loading: false,
selectedPage: basePage,
chunks: [
baseChunk,
{
...baseChunk,
id: "chunk-2",
chunk_index: 1,
content: "Lithium levels are checked 5 to 7 days after initiation",
},
],
search: "",
documentSearchResults: [] as [],
searchingDocument: false,
documentSearchError: null,
idPrefix: "source-chunk",
sectionId: "source-text" as const,
selectedChunkId: "chunk-1",
onSearchChange: vi.fn(),
compact: true,
};
const { rerender } = render(<IndexedTextPanel {...props} />);

const highlighted = screen.getByTestId("highlighted-indexed-source-chunk") as HTMLDetailsElement;
await waitFor(() => expect(highlighted.open).toBe(true));

// A re-render must not collapse the React-controlled deep-link disclosure.
rerender(<IndexedTextPanel {...props} chunks={[...props.chunks]} />);
expect(highlighted.open).toBe(true);
});
Comment thread
BigSimmo marked this conversation as resolved.

it("allows plain condensed panels to collapse and stay collapsed", async () => {
render(
<IndexedTextPanel
Expand Down
8 changes: 6 additions & 2 deletions tests/mode-home-loading-contract.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,7 +35,11 @@ describe("mode-home loading contract", () => {
);
expect(shellSource).not.toMatch(/import\s*\{[^}]*ClientHydrationBoundary/);
expect(shellSource).not.toMatch(/<ClientHydrationBoundary\b/);
expect(shellSource).toContain("SearchCommandProvider value={searchCommandContextValue}>{children}");
// Children stay under SearchCommandProvider; pending mode navigation may
// temporarily swap in ModeHomeRouteLoading instead of blanking the provider.
expect(shellSource).toMatch(
/<SearchCommandProvider value=\{searchCommandContextValue\}>[\s\S]*?\{pendingModeNavigation \? \([\s\S]*?<ModeHomeRouteLoading \/>[\s\S]*?\) : \(\s*children\s*\)\}/,
);
});

it("keeps route children outside useSearchParams Suspense on standalone shells", () => {
Expand All@@ -56,7 +60,7 @@ describe("mode-home loading contract", () => {
/function GlobalStandaloneSearchShellClient[\s\S]*?<Suspense fallback=\{null\}>[\s\S]*?ShellSearchParamsBridge/,
);
expect(shellSource).toMatch(
/function GlobalStandaloneSearchShellBody[\s\S]*?SearchCommandProvider value=\{searchCommandContextValue\}>\{children\}/,
/function GlobalStandaloneSearchShellBody[\s\S]*?<SearchCommandProvider value=\{searchCommandContextValue\}>[\s\S]*?\{pendingModeNavigation \? \([\s\S]*?<ModeHomeRouteLoading \/>[\s\S]*?\) : \(\s*children\s*\)\}/,
);
expect(shellSource).not.toMatch(/function GlobalStandaloneSearchShellBody[\s\S]*?useSearchParams\(\)/);
// Secondary nav is mounted inside the standalone body; it must consume the
Expand Down
Loading
Loading