diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 185d036a2..4583a93dd 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -622,5 +622,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-19 | all remote feature branches and registered worktrees against `origin/main` through PR #899 | 8242fa63d5f5b79fc770c9ae4f633e3a784b80e1 | branch/worktree cleanup, useful-work recovery, and protected-main merge closure | Deleted 122 stale or closed remote feature refs with exact SHA leases; four additional merged PR branches were removed by the protected-main PR workflow. Removed 32 obsolete, superseded, or merge-proven worktree registrations. Recovered useful dirty RAG work into PR #901 (deterministic and opt-in semantic reranking) and PR #902 (retrieval phase latency telemetry), preserved follow-up decisions in `docs/process-hardening.md`, and recovered four missing historical review rows. PRs #897, #899, #901, and #902 are merged with green exact-head checks and zero unresolved review threads. A detached full-repo-review worktree is deliberately retained because its ownership/activity could not be safely disproved; one unregistered `node_modules` junction residue is also retained because deletion was denied by local safety policy. | Fresh fetch/prune; full GitHub PR/check/thread inventory; `git worktree list --porcelain`; cherry-pick-aware right-only logs; exact leased remote deletes; exact-old-value local ref deletes; clean-worktree, path, and merged-PR proof before every removal. PR #899 local proof: focused Vitest 31/31, changed-file ESLint, `verify:cheap` 317 files / 2,879 tests, and `verify:ui` 239/239; exact-head hosted checks all passed. PR #901 local proof: `verify:cheap` 316 files / 2,870 tests; PR #902 focused Vitest 8/8 plus ESLint and typecheck. No OpenAI, Supabase, live clinical, deployment, or production-data workflow ran; provider-backed semantic canary evaluation remains approval-gated. | | 2026-07-19 | main / `codex/supabase-database-review` | 4034d2e60ebb6616130ff17bf3cb69368f36f8f6 + reviewed working diff | live `Clinical KB Database` security, migration, schema-drift, integrity, and performance review against current repo | Confirmed and remediated a P1 privacy defect: 601 private-document title-vocabulary rows were reachable by the service-role query corrector; the live public-only sync/backfill now reports zero private or out-of-scope rows. Applied the committed retrieval-count bound, audit-metadata minimization, registry cleanup/index, public-title corrector, and atomic summary-rate-limit migrations. The missing FK and registry indexes are present and no invalid indexes remain. A second P1 was found in the untracked live `ingestion-worker`: gateway JWT verification accepted any project JWT before privileged direct-Postgres job processing. Recovered the deployed source into the repo, restricted it to POST plus a gateway-verified `service_role` claim, expanded the Deno checker to every tracked Edge Function, and deployed exact-matching v13 with JWT verification enabled. Review also exposed a repo mirror/test gap: the count-clamp migration was not reflected in `schema.sql`; the branch now mirrors it and locks both sources in the focused test. Remaining hosted blocker: `postgres` cannot assume managed `supabase_admin`, so the fail-closed default-ACL migrations and final title-word constraint/trigger migration remain unapplied; the intentional service-role-only table still produces one INFO no-policy advisor. | Supabase connector project identity, migration and Edge Function inventory, full drift snapshot comparison, security/performance advisors, catalog integrity/ACL/index queries, Vault JWT-role compatibility check, post-apply invariants, exact deployed-source hashes, and unauthenticated live rejection (401); focused retrieval/schema/drift Vitest 82/83 with only manifest freshness failing; Edge/retrieval auth 9/9; Deno check for both functions; offline RAG 36 cases / 294 tests; function-grant guard; scoped ESLint, Prettier, and `git diff --check`. `check:supabase-project` was attempted but stopped before provider contact because local project env vars are unset. `drift:manifest` was blocked because Docker Desktop could not start and was cleaned up. `verify:cheap`, `verify:pr-local`, production-readiness, OpenAI, hosted CI, broader deployment, and commit/push were not run. | | 2026-07-19 | cursor/safari-edge-to-edge-f46b (PR #933) | 15061964dd2fdf9665f72b7282f5cc81c736e57f | final Safari edge-to-edge / phone dock reserve review + merge readiness | No high-confidence P0-P1. Confirmed implementation: shared reserve module collapses to 0.75rem when dock hides; shell uses block scrollport + inner mobile-composer-reserve-pad so clearance contributes to scrollHeight; child dock-sized env(safe-area) pads removed; DocumentViewer owns its dock pad. Review polish: formulation/specifier max-sm:min-h-0 alignment, document-route ownership simplification, hidden-pad CSS token guard. Residual P2/P3 only: differentials compare zero-inset backdrop margin, idle 2rem vs max(2rem,safe-area) ~2px, unused-looking #main-content padding transition still needed by ClinicalDashboard. Merge-ready. | Local: format/lint/typecheck/knip/budgets pass; unit 2952 passed with only pre-existing pdf-extraction-budget (python ENOENT, also fails on clean main); production build + client-bundle secret scan pass; focused Chromium composer suite 6/6 (forms hide, tablet/desktop clearance, differentials compare, service-detail endpoint, document-viewer hide, long-answer dock). Hosted CI on prior head fully green including Production UI; polish head re-checked before merge. No OpenAI/live Supabase/provider calls. | +| 2026-07-19 | cursor/hide-favourites-guest-mode-a26d (PR #934) | 23d05539320427ea3f994eaec3c2e95daa481492 | Favourites guest mode-switcher/sidebar auth gate review | No P0/P1. Primary request correctly wired: MasterSearchHeader fails closed; ClinicalDashboard and global-search-shell both pass session favouritesAccessible to mode menu + desktop/mobile showAccountLibrary; selectSearchMode/changeMode guarded; /favourites stays on-page with signup for guests. Residual P2: guest discovery still possible via composer cross-mode Favourites chips and Tools catalog Saved workflows (content remains gated on /favourites). P2 test gap: wiring tests are source-string, not MasterSearchHeader DOM. P3: header passes precomputed access as authenticated plus demoMode (redundant OR, intentional demo reopen). | Focused Vitest favourites-auth-gate + app-modes 26/26; prior verify:cheap green on branch; browser guest check: sidebar omits Your library/Favourites, /favourites shows signup gate; mode-menu automation inconclusive. No OpenAI/Supabase/hosted CI. | +| 2026-07-19 | cursor/hide-favourites-guest-mode-a26d (PR #934) | 04192653 | Final Favourites total guest-blackout review + merge readiness | No P0/P1. Fixed residual P2s: rankToolRecords now fails closed by default; mode/chip filters stop double-ORing demoMode; gated deep-link mode chrome falls back to Answer; MasterSearchHeader mode-menu DOM tests added. Guest discovery closed for mode menu, sidebar, cross-mode chips, Tools Saved, universal tools ranking, also-matches, prefetch. Intentional residual: direct /favourites signup gate; mockup routes. PR body appears overwritten by unrelated Sync PR policy content โ€” agent posted corrective comment (403 on body edit). | Focused Vitest 37/37; verify:cheap green; verify:pr-local green (build + client-secret scan + RAG fixtures); browser guest Tools/sidebar/favourites proof earlier. No OpenAI/Supabase writes. | | 2026-07-19 | cursor/pr-policy-body-cleanup-f46b (PR #942) + PR #933 closeout | 7c8e6aadf0890b143372fb96f13d9de47a416db9 | post-merge CI triage for #933 PR-policy red check | PR #933 product merge (`bd864de0`) already on main with green post-merge main CI (Static/Unit/Build/Production UI/SAST/Docker). Sole remaining red check on #933 was post-ready PR policy against a stale synced body with unchecked governance boxes (from leftover `PR_POLICY_BODY.md` introduced by #932). Token cannot edit merged PR bodies (403). Removed the stale template via #942 so Sync PR policy body no longer reapplies unchecked governance. Local composer regression 6/6 on main; reserve unit 11/11. No product regression. | Hosted #933 pre-merge + main push green; #942 required checks green then squash-merged; focused Chromium composer 6/6; reserve Vitest 11/11. No OpenAI/Supabase provider calls. | | 2026-07-19 | cursor/documents-search-header-3eab / PR #936 | a7feaa3033180b672cfafaaaf75dc75088ebf052 | documents search header redesign final review + merge readiness | No remaining high-confidence P0-P1. Implemented identity-first results chrome, unified Sort/type-filter/Library toolbar, removed documents Also-in-library strip, relocated ScopeAndGovernanceNotice under controls, fixed Prettier CI failure and memo-busting empty warnings default, synced accurate PR policy body then removed the stale leftover, and repeatedly merged origin/main so squash auto-merge is not blocked behind/dirty. Hosted required checks including Production UI passed on the integrated head. | Local: typecheck/lint/format; focused Playwright documents `@critical` + deferred source/admin + forms sort persistence; design-system/icon-scale/maintainability; build + RAG fixtures; verify:pr-local units with known pdf-extraction-budget env artifact also on clean main. Hosted: PR policy, Static, Unit, Build, Production UI, Advisory UI, PR required green. No OpenAI/live Supabase writes. | diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index cea973079..1b2faa658 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -95,6 +95,8 @@ import { resolveMobileComposerReserve, } from "@/components/clinical-dashboard/mobile-composer-reserve"; import { UniversalSearchAlsoMatches } from "@/components/clinical-dashboard/universal-search-also-matches"; +import { FavouritesGuestGate } from "@/components/clinical-dashboard/favourites-guest-gate"; +import { useDashboardShellActions } from "@/components/clinical-dashboard/use-dashboard-shell-actions"; import { useScrollHideReporter } from "@/components/clinical-dashboard/use-hide-on-scroll"; import { SearchCommandProvider } from "@/components/clinical-dashboard/search-command-context"; import { @@ -110,7 +112,6 @@ import { setupNeedsSlowRecheck, setupRecheckPollMs, shorterPollDelay, - sessionFavouritesAccessible, } from "@/components/clinical-dashboard/clinical-dashboard-helpers"; import { answerRecovery, errorCopy } from "@/lib/ui-copy"; import { @@ -675,7 +676,6 @@ export function ClinicalDashboard({ const [activeHash, setActiveHash] = useState("#search"); const [guideOpen, setGuideOpen] = useState(false); const [settingsOpen, setSettingsOpen] = useState(false); - const [accountSetupOpen, setAccountSetupOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useSidebarCollapsed(); const [documentsDrawerOpen, setDocumentsDrawerOpen] = useState(false); @@ -814,6 +814,31 @@ export function ClinicalDashboard({ authUnavailableFallback: browserAuthUnavailableDemoFallback, localNoAuthMode, }); + const sidebarIdentity = useMemo(() => deriveSidebarIdentity(auth.session?.user.email), [auth.session?.user.email]); + const { + favouritesAccessible, + accountSetupOpen, + accountSetupIntent, + closeAccountSetup, + closeTransientSurfaces: closeDashboardTransientSurfaces, + openAccountSetup, + openGuide, + closeGuide, + openSettings, + closeSettings, + openAccountProfile, + prefetchApplications, + } = useDashboardShellActions({ + authenticated: auth.status === "authenticated", + demoMode: clientDemoMode, + signedIn: sidebarIdentity.signedIn, + setGuideOpen, + setSettingsOpen, + setMobileSidebarOpen, + setDocumentsDrawerOpen, + setUploadDrawerOpen, + prefetch: (href) => router.prefetch(href), + }); const answerThreadOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null); const previousAnswerThreadOwnerIdRef = useRef(answerThreadOwnerId); useEffect(() => { @@ -888,43 +913,6 @@ export function ClinicalDashboard({ canUseDegradedLocalSearchApis || canUseNonProductionDemoFallback || canAttemptDeployedPublicSearch; - const closeDashboardTransientSurfaces = useCallback( - (except?: "guide" | "settings" | "accountSetup" | "mobileSidebar" | "documents" | "upload") => { - if (except !== "guide") setGuideOpen(false); - if (except !== "settings") setSettingsOpen(false); - if (except !== "accountSetup") setAccountSetupOpen(false); - if (except !== "mobileSidebar") setMobileSidebarOpen(false); - if (except !== "documents") setDocumentsDrawerOpen(false); - if (except !== "upload") setUploadDrawerOpen(false); - }, - [], - ); - const openGuide = useCallback(() => { - closeDashboardTransientSurfaces("guide"); - setGuideOpen(true); - }, [closeDashboardTransientSurfaces]); - const closeGuide = useCallback(() => setGuideOpen(false), []); - const openSettings = useCallback(() => { - closeDashboardTransientSurfaces("settings"); - setSettingsOpen(true); - }, [closeDashboardTransientSurfaces]); - const closeSettings = useCallback(() => setSettingsOpen(false), []); - const sidebarIdentity = useMemo(() => deriveSidebarIdentity(auth.session?.user.email), [auth.session?.user.email]); - const openAccountProfile = useCallback(() => { - if (sidebarIdentity.signedIn) { - closeDashboardTransientSurfaces("settings"); - setSettingsOpen(true); - return; - } - closeDashboardTransientSurfaces("accountSetup"); - setAccountSetupOpen(true); - }, [closeDashboardTransientSurfaces, sidebarIdentity.signedIn]); - const closeAccountSetup = useCallback(() => setAccountSetupOpen(false), []); - const prefetchApplications = useCallback(() => { - router.prefetch("/?mode=tools"); - router.prefetch("/favourites"); - router.prefetch("/differentials"); - }, [router]); const openLibraryHealthTarget = useCallback( (target: LibraryHealthTarget) => { if (!canUseAdministrativeApis) { @@ -2482,6 +2470,10 @@ export function ClinicalDashboard({ } function crossModeSearch(mode: AppModeId, crossQuery: string) { + if (mode === "favourites" && !favouritesAccessible) { + openAccountSetup("favourites"); + return; + } modeChangeFromUiRef.current = true; if (mode === "differentials") clearDifferentialModeResultState(); setCommandScopes([]); @@ -2763,6 +2755,10 @@ export function ClinicalDashboard({ } function selectSearchMode(mode: AppModeId) { + if (mode === "favourites" && !favouritesAccessible) { + openAccountSetup("favourites"); + return; + } modeChangeFromUiRef.current = true; if (mode === "differentials") clearDifferentialModeResultState(); setQuery(""); @@ -3476,7 +3472,7 @@ export function ClinicalDashboard({ theme={theme} onToggleTheme={toggleTheme} onPrefetchApplications={prefetchApplications} - showAccountLibrary={sessionFavouritesAccessible(auth.status, clientDemoMode)} + showAccountLibrary={favouritesAccessible} />
@@ -3493,6 +3489,8 @@ export function ClinicalDashboard({ realDataReady={canRunSearch} onQueryChange={setQuery} onSearchModeChange={selectSearchMode} + canAccessFavourites={favouritesAccessible} + onRequestAccountSetup={() => openAccountSetup("favourites")} onAsk={ask} onClearQuery={() => { setQuery(""); @@ -3848,7 +3846,7 @@ export function ClinicalDashboard({ /> ) : activeModeResultKind === "tools" ? ( - ) : activeModeResultKind === "favourites" ? ( + ) : activeModeResultKind === "favourites" && favouritesAccessible ? ( + ) : activeModeResultKind === "favourites" ? ( + openAccountSetup("favourites")} /> ) : activeModeResultKind === "documents" || activeModeResultKind === "services" ? ( searchMode === "prescribing" ? ( - +
diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index 71e52e1aa..7ecfd2e2a 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -26,11 +26,14 @@ import { type FormEvent, useMemo, useState } from "react"; import { ModeHomeHero, ModeHomeVerificationFooter } from "@/components/mode-home-template"; import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context"; +import { useFavouritesAccess } from "@/components/clinical-dashboard/use-favourites-access"; import { cn, toneInfo, toneSuccess, toneWarning } from "@/components/ui-primitives"; import { Sheet } from "@/components/ui/sheet"; +import { isLocalNoAuthMode, resolveClientDemoMode } from "@/lib/client-env"; import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; +import { useAuthSession } from "@/lib/supabase/client"; import { - toolCatalogRecords, + toolCatalogRecordsForSession, type ToolCatalogArea, type ToolCatalogRecord, type ToolCatalogStatus, @@ -92,14 +95,19 @@ const launcherIconById: Record = { favourites: Star, }; -const launcherApps: LauncherApp[] = toolCatalogRecords.map((record) => ({ - ...record, - icon: launcherIconById[record.id] ?? Sparkles, -})); +function launcherAppsForSession(canAccessFavourites: boolean): LauncherApp[] { + return toolCatalogRecordsForSession({ + authenticated: canAccessFavourites, + demoMode: false, + }).map((record) => ({ + ...record, + icon: launcherIconById[record.id] ?? Sparkles, + })); +} const toolsLauncherCopy = { heading: "Tools", - description: "Assessment, prescribing, documents, and saved work.", + description: "Assessment, prescribing, documents, and clinical workflows.", allSectionLabel: "All tools", countNoun: "tools", emptyTitle: "No tools match", @@ -109,7 +117,7 @@ const toolsLauncherCopy = { openSelectedAriaLabel: "Open selected tool", }; -const quickActions = [ +const quickActionsBase = [ { label: "Ask", desktopLabel: "Ask evidence", icon: Search, id: "clinical-kb-search" }, { label: "Compare", desktopLabel: "Compare", icon: Brain, id: "differentials" }, { label: "Prescribe", desktopLabel: "Prescribe", icon: Pill, id: "medication-prescribing" }, @@ -120,7 +128,7 @@ const quickActions = [ { label: "More", desktopLabel: "More", icon: Sparkles, id: "favourites" }, ] as const; -const desktopFilters: Array<{ id: LauncherFilter; label: string }> = [ +const desktopFiltersBase: Array<{ id: LauncherFilter; label: string }> = [ { id: "all", label: "All tools" }, { id: "assessment", label: "Assess" }, { id: "reference", label: "Evidence" }, @@ -137,17 +145,18 @@ const mobileFilters: Array<{ id: LauncherFilter; label: string; hasMenu?: boolea { id: "more", label: "More", hasMenu: true }, ]; -export const applicationsLauncherItemCount = launcherApps.length; +/** Full catalog length (includes Favourites). Prefer session-filtered lists in UI. */ +export const applicationsLauncherItemCount = launcherAppsForSession(true).length; -function appById(id: string) { - return launcherApps.find((app) => app.id === id) ?? launcherApps[0]; +function appById(id: string, apps: LauncherApp[]) { + return apps.find((app) => app.id === id) ?? apps[0]; } -function initialToolId(query: string | undefined) { +function initialToolId(query: string | undefined, apps: LauncherApp[]) { const normalized = query?.trim().toLowerCase(); if (!normalized) return "risk-safety"; return ( - launcherApps.find((app) => + apps.find((app) => [app.title, app.mobileTitle, app.description, app.bestFor, app.detail, app.area, ...app.keywords] .filter(Boolean) .join(" ") @@ -157,6 +166,14 @@ function initialToolId(query: string | undefined) { ); } +function quickActionsForSession(canAccessFavourites: boolean) { + return canAccessFavourites ? quickActionsBase : quickActionsBase.filter((action) => action.id !== "favourites"); +} + +function desktopFiltersForSession(canAccessFavourites: boolean) { + return canAccessFavourites ? desktopFiltersBase : desktopFiltersBase.filter((filter) => filter.id !== "saved"); +} + function appIconTone(app: LauncherApp) { if (app.id === "risk-safety") return iconToneClasses.safety; if (app.id === "medication-prescribing") return iconToneClasses.medication; @@ -266,14 +283,25 @@ function ToolChips({ app, includeStatus = false }: { app: LauncherApp; includeSt ); } -function QuickActions({ onSelect, mobile }: { onSelect: (id: string) => void; mobile?: boolean }) { +function QuickActions({ + onSelect, + mobile, + apps, + canAccessFavourites, +}: { + onSelect: (id: string) => void; + mobile?: boolean; + apps: LauncherApp[]; + canAccessFavourites: boolean; +}) { + const quickActions = quickActionsForSession(canAccessFavourites); return (
{quickActions.slice(0, mobile ? 8 : 6).map((action) => { - const app = appById(action.id); + const app = appById(action.id, apps); const Icon = action.icon; return (
@@ -786,7 +831,11 @@ export function ApplicationsLauncherWorkspace({

{copy.allSectionLabel}

- +
Sort by A to Z diff --git a/src/components/clinical-dashboard/favourites-guest-gate.tsx b/src/components/clinical-dashboard/favourites-guest-gate.tsx new file mode 100644 index 000000000..38836fae2 --- /dev/null +++ b/src/components/clinical-dashboard/favourites-guest-gate.tsx @@ -0,0 +1,22 @@ +"use client"; + +/** + * Inline gate when Favourites content would otherwise render in the dashboard + * for a signed-out non-demo session (defense in depth; /favourites is canonical). + */ +export function FavouritesGuestGate({ onOpenAccountSetup }: { onOpenAccountSetup: () => void }) { + return ( +
+

Favourites are tied to your account.

+

Sign in or create an account to continue.

+ +
+ ); +} diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 58c3e2baa..ad26d0dda 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -439,7 +439,7 @@ function GlobalStandaloneSearchShellClient({ function prefetchApplications() { router.prefetch("/?mode=tools"); - router.prefetch("/favourites"); + if (favouritesAccessible) router.prefetch("/favourites"); router.prefetch("/differentials"); router.prefetch("/dsm"); router.prefetch("/specifiers"); @@ -521,6 +521,13 @@ function GlobalStandaloneSearchShellClient({ } function crossModeSearch(mode: AppModeId, crossQuery: string) { + if (mode === "favourites" && !favouritesAccessible) { + setGuideOpen(false); + setSettingsOpen(false); + setMobileMenuOpen(false); + openAccountSetup("favourites"); + return; + } setQuery(crossQuery); setCommandScopes([]); setSearchMode(mode); diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 6adb76e71..423fc5790 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -185,7 +185,7 @@ export function MasterSearchHeader({ onMobileBack, hideOnScroll, onBottomComposerHiddenChange, - canAccessFavourites = true, + canAccessFavourites = false, onRequestAccountSetup, }: { demoMode: boolean; @@ -266,19 +266,25 @@ export function MasterSearchHeader({ /** * Favourites are account-scoped. When false, omit Favourites from the mode menu * and route favourites actions to account setup instead of switching mode. - * Defaults to true for backward-compatible call sites (tests/story fixtures). + * Defaults to false (fail closed) so guests never see Favourites unless the host + * explicitly grants access from the current session / demo mode. */ canAccessFavourites?: boolean; /** Invoked when the user tries to open Favourites without access. */ onRequestAccountSetup?: () => void; }) { + // Hosts pass the precomputed session decision in canAccessFavourites (auth || demo). + // Do not OR demoMode again here โ€” that would reopen Favourites when props diverge. const visibleAppModeOptions = visibleAppModeDefinitionsForSession({ authenticated: canAccessFavourites, demoMode: false, }); const trimmedQuery = query.trim(); const selectedSearch = appModeSearchConfig(searchMode); - const selectedAppMode = appModeDefinition(searchMode); + // Guests on /favourites keep the route gate, but the mode trigger must not claim + // Favourites is a selectable guest mode when it is omitted from the menu. + const modeTriggerId = searchMode === "favourites" && !canAccessFavourites ? "answer" : searchMode; + const selectedAppMode = appModeDefinition(modeTriggerId); const selectedSearchable = isSearchableAppMode(searchMode); const isAnswerFooterComposer = searchMode === "answer"; const isWorkflowHeader = headerVariant === "workflow"; @@ -1297,6 +1303,7 @@ export function MasterSearchHeader({ ) : null} { + if (targetMode === "favourites" && !canAccessFavourites) { + onRequestAccountSetup?.(); + return; + } if (onCrossModeSearch) { onCrossModeSearch(targetMode, crossQuery); return; diff --git a/src/components/clinical-dashboard/search-results-header-band.tsx b/src/components/clinical-dashboard/search-results-header-band.tsx index c87c77920..4c63beb2d 100644 --- a/src/components/clinical-dashboard/search-results-header-band.tsx +++ b/src/components/clinical-dashboard/search-results-header-band.tsx @@ -175,15 +175,18 @@ export function SearchResultsEmptyState({ onClearScopes, onTryExample, onCrossMode, + canAccessFavourites = false, }: { modeId: AppModeId; query: string; onClearScopes?: () => void; onTryExample?: (example: string) => void; onCrossMode?: (modeId: AppModeId) => void; + canAccessFavourites?: boolean; }) { const command = useSearchCommand(); const config = searchCommandSurfaceConfig(modeId); + const crossModes = (config?.crossModes ?? []).filter((target) => canAccessFavourites || target !== "favourites"); const activeScopes = command?.commandScopes ?? []; return ( @@ -222,7 +225,7 @@ export function SearchResultsEmptyState({ Try: {config.examples[0]} ) : null} - {config?.crossModes.slice(0, 2).map((target) => + {crossModes.slice(0, 2).map((target) => onCrossMode ? (