diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 5e8c496da..25dbc5b3f 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -20,6 +20,8 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | Date | Branch or ref | Reviewed HEAD | Scope | Outcome | Checks | | ---------- | -------------------------------------- | ---------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-10 | codex/design-ux-review-fixes | 648abfa3f7c91395b5eeca543f70e0b6ea59e9e0 | design-system + UX + design | Five issue groups confirmed; scoped fixes applied in the worktree. | `npm run check:type-scale`; focused Vitest (19/19); `npm run typecheck`; `npm run lint`; `npm run sitemap:check`; browser/API-backed checks awaiting approval | +| 2026-07-11 | codex/design-ux-review-integration | 98093ec7b | branch-integration-review | Replayed the reviewed design and UX fixes onto current `origin/main`, preserved the lightweight evidence-panel boundary, and retained the merged quality fixes. | `npm run check:type-scale`; combined focused Vitest (8 files, 42 tests); runtime/action/sitemap/type-scale/lint stages of `verify:cheap`; typecheck blocked by stale worktree dependencies pending hosted clean install; `git diff --check` | | 2026-07-09 | example/branch | abc1234 | branch-cleanup | Example: already merged into `main`; no unique patch content. | `git log --right-only --cherry-pick main...example/branch`; `git diff --name-status main...example/branch` | | 2026-07-10 | codex/pr-testing-streamlining | 155c801cd58f797037d8aaa8b885405a1c599249 | working-tree diff: PR testing streamlining | Changes requested: 2 P1 clinical-gate defects and 7 P2/P3 scope, local parity, and UI-process defects. | `npm run check:ci-scope`; `npm run check:github-actions`; `npm run check:codex-autofix-workflow`; `npm run eval:rag:offline`; `npm run test:e2e:critical`; targeted scope classifications; `git diff --check` | | 2026-07-10 | codex/pr-testing-streamlining | 155c801cd58f797037d8aaa8b885405a1c599249 | working-tree remediation review | All recorded P1-P3 findings fixed; no remaining high-confidence issue in the changed scope. | `npm run verify:cheap`; `npm run verify:pr-local`; `npm run eval:rag:offline`; `npm run test:e2e:critical`; `npm run test:e2e:advisory`; CI YAML parse; scope/action/Codex guards; `git diff --check` | diff --git a/package.json b/package.json index 5b9d1e9e7..830a4b8c2 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test:e2e:advisory": "node scripts/run-playwright.mjs --project=chromium --grep-invert @critical", "test:e2e:chromium": "node scripts/run-playwright.mjs --project=chromium", "test:e2e:visual": "node scripts/run-playwright.mjs --config=playwright.visual.config.ts", - "verify:cheap": "npm run check:runtime && npm run check:github-actions && npm run sitemap:check && npm run lint && npm run typecheck && npm run test", + "verify:cheap": "npm run check:runtime && npm run check:github-actions && npm run sitemap:check && npm run check:type-scale && npm run lint && npm run typecheck && npm run test", "verify:pr-local": "node scripts/verify-pr-local.mjs", "verify:ui": "npm run check:runtime && npm run test:e2e:chromium", "verify:release": "npm run check:runtime && npm run lint && npm run typecheck && npm run test && npm run build && npm run test:e2e && npm run check:production-readiness && npm run governance:release && npm run eval:quality:release", @@ -68,7 +68,7 @@ "check:indexing": "tsx scripts/check-indexing.ts", "check:m13-migration": "tsx scripts/check-m13-migration.ts", "check:july8-live-batch": "tsx scripts/check-july8-live-batch.ts", - "check:type-scale": "node scripts/check-type-scale.mjs", + "check:type-scale": "node scripts/check-type-scale.mjs --strict", "recover:ingestion": "tsx scripts/recover-ingestion-queue.ts", "registry:seed": "tsx scripts/seed-registry-records.ts", "registry:embed": "tsx scripts/embed-registry-records.ts", diff --git a/scripts/check-type-scale.mjs b/scripts/check-type-scale.mjs index 9276d487f..67bf46a2d 100644 --- a/scripts/check-type-scale.mjs +++ b/scripts/check-type-scale.mjs @@ -3,7 +3,7 @@ // (e.g. text-[12px], text-[1.45rem]) that bypass the design type scale. // // The scale lives in the @theme block of src/app/globals.css: named steps -// text-4xs … text-2xl-minus, on top of Tailwind's default xs/sm/base/lg/xl/2xl. +// text-4xs … text-3xl-minus, on top of Tailwind's default xs/sm/base/lg/xl/2xl/3xl. // Arbitrary text-[] values re-introduce off-scale sizes; this check // tracks that drift. Colour utilities (text-[color:var(--…)]) are the sanctioned // token-access form and are intentionally NOT flagged. diff --git a/src/app/globals.css b/src/app/globals.css index e7962a80b..4c24cbc66 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -67,6 +67,7 @@ --text-base-minus: 0.9375rem; --text-lg-minus: 1.0625rem; --text-2xl-minus: 1.375rem; + --text-3xl-minus: 1.625rem; /* Font families: bind Tailwind's font-sans / font-mono to the loaded Geist faces (variables set on by next/font). font-mono is used for @@ -234,6 +235,7 @@ --shadow-elevated: 0 1px 2px rgb(12 24 34 / 8%), 0 10px 20px rgb(12 24 34 / 9%), 0 24px 48px rgb(12 24 34 / 10%); --shadow-lux: inset 0 1px 0 rgb(255 255 255 / 64%), 0 12px 34px rgb(8 16 24 / 7%); --shadow-inset: inset 0 1px 0 rgb(255 255 255 / 58%); + --shadow-card: var(--shadow-tight); --ring-focus: 0 0 0 3px color-mix(in srgb, var(--focus) 30%, transparent); --space-1: 0.25rem; --space-2: 0.5rem; diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 1a1d9341a..23400f575 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -155,6 +155,13 @@ import { type AppModeSearchKind, } from "@/lib/app-modes"; import { documentsSearchHref } from "@/lib/document-flow-routes"; +import { + readSearchNavigationContext, + routedSubmissionContextChanged, + searchNavigationContextSignature, + searchSubmissionSignature, + type SearchNavigationContext, +} from "@/lib/search-navigation-context"; import { rankFormRecords } from "@/lib/forms"; import { rankServiceRecords } from "@/lib/services"; import { useRegistryRecords } from "@/lib/use-registry-records"; @@ -656,6 +663,7 @@ export function ClinicalDashboard({ }: { initialSearchMode?: AppModeId; initialQuery?: string; focusSearch?: boolean; autoRunSearch?: boolean } = {}) { const router = useRouter(); const searchParams = useSearchParams(); + const [initialSearchNavigationContext] = useState(() => readSearchNavigationContext(searchParams)); const mainRef = useRef(null); const [mainScrollRoot, setMainScrollRoot] = useState(null); const assignMainRef = useCallback((node: HTMLElement | null) => { @@ -712,7 +720,7 @@ export function ClinicalDashboard({ const [documentMatches, setDocumentMatches] = useState([]); const [searchRelevance, setSearchRelevance] = useState(null); const [searchFacets, setSearchFacets] = useState(null); - const [queryMode, setQueryMode] = useState("auto"); + const [queryMode, setQueryMode] = useState(initialSearchNavigationContext.queryMode); const activeModeSearch = appModeSearchConfig(searchMode); const activeModeResultKind = appModeResultKind(searchMode); const requestQueryMode = appModeQueryMode(searchMode, queryMode); @@ -724,6 +732,8 @@ export function ClinicalDashboard({ autoRunSearch && searchParams.get("run") === "1" && submittedUrlModeMatchesActive ? (searchParams.get("q") ?? searchParams.get("query") ?? "").trim() : ""; + const routedSearchContext = useMemo(() => readSearchNavigationContext(searchParams), [searchParams]); + const routedSearchContextSignature = searchNavigationContextSignature(routedSearchContext); // Record matches come from the owner-scoped registry API (mock fixtures in // demo mode); ranking stays client-side so live-typing behaviour is @@ -830,7 +840,7 @@ export function ClinicalDashboard({ setAnswerProgress(null); setDifferentialEvidenceQuery(null); }, [resetAnswerThread]); - const [scopeFilters, setScopeFilters] = useState({}); + const [scopeFilters, setScopeFilters] = useState(initialSearchNavigationContext.scopeFilters); const [searchScope, setSearchScope] = useState(null); const [sourceGovernanceWarnings, setSourceGovernanceWarnings] = useState([]); const [answerViewMode, setAnswerViewMode] = useState("high_yield"); @@ -1588,6 +1598,9 @@ export function ClinicalDashboard({ const searchParamString = searchParams.toString(); if (lastSyncedSearchParamsRef.current === searchParamString) return; lastSyncedSearchParamsRef.current = searchParamString; + const nextSearchContext = readSearchNavigationContext(new URLSearchParams(searchParamString)); + setQueryMode(nextSearchContext.queryMode); + setScopeFilters(nextSearchContext.scopeFilters); if (searchParams.get("run") === "1") return; const mode = searchParams.get("mode"); @@ -1864,7 +1877,7 @@ export function ClinicalDashboard({ searchAbortRef.current?.abort(); } - function applySearchResult(payload: SearchResultModePayload, displayQuery?: string) { + function applySearchResult(payload: SearchResultModePayload, displayQuery?: string, archivePreviousAnswer = true) { if (payload.kind === "documents") { setDocumentMatches(payload.documentMatches); setSources(payload.sources); @@ -1878,7 +1891,7 @@ export function ClinicalDashboard({ const answerData = payload.payload; // Archive the previous exchange before the new answer replaces it, so the // thread keeps every turn visible in the same window. - const priorTurn = latestAnswerTurnRef.current; + const priorTurn = archivePreviousAnswer ? latestAnswerTurnRef.current : null; if (priorTurn) { const turnId = `answer-turn-${++answerTurnSeqRef.current}`; setPriorAnswerTurns((turns) => [...turns, { id: turnId, ...priorTurn }].slice(-maxStoredAnswerTurns)); @@ -1915,11 +1928,17 @@ export function ClinicalDashboard({ if (answerData.demoMode) setDemoMode(true); } - async function executeSearch(searchText: string, targetMode: AppModeId = searchMode, filtersOverride = scopeFilters) { + async function executeSearch( + searchText: string, + targetMode: AppModeId = searchMode, + filtersOverride = scopeFilters, + queryModeOverride = queryMode, + replaceExistingAnswer = false, + ) { const trimmedQuery = searchText.trim(); if (!trimmedQuery) return; const modeSearch = appModeSearchConfig(targetMode); - const targetQueryMode = appModeQueryMode(targetMode, queryMode); + const targetQueryMode = appModeQueryMode(targetMode, queryModeOverride); const isDifferentialsMode = modeSearch.resultKind === "differentials"; // Note: no automatic mode-default label scope for Services/Forms. Applying // one on every search routed resolveSearchScope's label path over the whole @@ -2009,7 +2028,7 @@ export function ClinicalDashboard({ // previous turn's question before retrieval. The raw text the user typed // is what the thread displays (via displayQuery below). const isAnswerRequest = modeSearch.resultKind === "answer"; - const priorTurnQuery = isAnswerRequest ? latestAnswerTurnRef.current?.query : undefined; + const priorTurnQuery = isAnswerRequest && !replaceExistingAnswer ? latestAnswerTurnRef.current?.query : undefined; const isAnswerFollowUp = isAnswerRequest && Boolean(priorTurnQuery); const requestQuery = isAnswerRequest ? buildAnswerFollowUpQuery(priorTurnQuery, trimmedQuery) : trimmedQuery; @@ -2093,16 +2112,33 @@ export function ClinicalDashboard({ // M10: discard a stale response — a newer search owns the UI state. if (requestId === searchRequestSeqRef.current) { - applySearchResult(successfulPayload, trimmedQuery); + applySearchResult(successfulPayload, trimmedQuery, !replaceExistingAnswer); if (isDifferentialsMode) setDifferentialEvidenceQuery(trimmedQuery); if (successfulPayload.kind === "answer") { + // Explicit composer submissions do not pass through the URL auto-run + // effect. Seed their completed context so a later in-place route to + // the same query with different intent/scope is recognized as a + // replacement search instead of leaving the old answer on screen. + autoRunSearchSignatureRef.current = searchSubmissionSignature(targetMode, trimmedQuery, { + queryMode: targetQueryMode, + scopeFilters: filtersOverride, + }); // The composer is a draft box in a conversation: clear it so the // user can type the next follow-up immediately. setQuery(""); // Keep only the latest question in the URL; the full thread lives in // React state until refresh or New chat. modeChangeFromUiRef.current = true; - window.history.replaceState(null, "", appModeHomeHref(targetMode, { query: trimmedQuery, run: true })); + window.history.replaceState( + null, + "", + appModeHomeHref(targetMode, { + query: trimmedQuery, + run: true, + queryMode: queryModeOverride, + scopeFilters: filtersOverride, + }), + ); if (isAnswerFollowUp) { window.requestAnimationFrame(() => { const main = mainRef.current; @@ -2140,21 +2176,33 @@ export function ClinicalDashboard({ setAnswerProgress(null); rememberRecentQuery(trimmedSearchText); window.requestAnimationFrame(() => mainRef.current?.scrollTo({ top: 0, behavior: "smooth" })); - if (updateUrl) router.replace(appModeHomeHref("prescribing", { query: trimmedSearchText })); + if (updateUrl) { + router.replace(appModeHomeHref("prescribing", { query: trimmedSearchText, queryMode, scopeFilters })); + } } - async function ask(searchText = query) { + async function ask(searchText = query, contextOverride?: SearchNavigationContext, replaceExistingAnswer = false) { const trimmedQuery = searchText.trim(); + const effectiveQueryMode = contextOverride?.queryMode ?? queryMode; + const effectiveScopeFilters = contextOverride?.scopeFilters ?? scopeFilters; if (searchMode === "documents" && trimmedQuery) { rememberRecentQuery(trimmedQuery); - router.push(documentsSearchHref({ query: trimmedQuery, focus: true, run: true })); + router.push( + documentsSearchHref({ + query: trimmedQuery, + focus: true, + run: true, + queryMode: effectiveQueryMode, + scopeFilters: effectiveScopeFilters, + }), + ); return; } if (searchMode === "prescribing") { setMedicationSearchQuery(searchText); return; } - await executeSearch(searchText, searchMode, scopeFilters); + await executeSearch(searchText, searchMode, effectiveScopeFilters, effectiveQueryMode, replaceExistingAnswer); } const askRef = useRef(ask); askRef.current = ask; @@ -2165,20 +2213,27 @@ export function ClinicalDashboard({ const canAutoRunMode = searchMode === "documents" || searchMode === "prescribing" || canRunSearch; if (!autoRunSearch || !submittedSearchText || !canAutoRunMode || loading) return; if (searchMode === "answer" && !answerThreadBootstrapped) return; + const previousSignature = autoRunSearchSignatureRef.current; + const signature = searchSubmissionSignature(searchMode, submittedSearchText, routedSearchContext); + const routedContextChanged = routedSubmissionContextChanged( + previousSignature, + searchMode, + submittedSearchText, + routedSearchContext, + ); // Once an answer is on screen, composer edits are follow-up drafts and must // only run on explicit submit — not on every query keystroke while run=1 // keeps autoRunSearch enabled from the URL. - if (searchMode === "answer" && answer) return; + if (searchMode === "answer" && answer && !routedContextChanged) return; // After reload, the URL query matches the restored latest turn — do not // archive it again into a duplicate prior turn. - if (searchMode === "answer" && latestAnswerQuery?.trim() === submittedSearchText) { - autoRunSearchSignatureRef.current = `${searchMode}:${submittedSearchText}`; + if (searchMode === "answer" && latestAnswerQuery?.trim() === submittedSearchText && !routedContextChanged) { + autoRunSearchSignatureRef.current = signature; return; } - const signature = `${searchMode}:${submittedSearchText}`; if (autoRunSearchSignatureRef.current === signature) return; autoRunSearchSignatureRef.current = signature; - void askRef.current(submittedSearchText); + void askRef.current(submittedSearchText, routedSearchContext, routedContextChanged); }, [ autoRunSearch, canRunSearch, @@ -2189,6 +2244,8 @@ export function ClinicalDashboard({ answer, answerThreadBootstrapped, latestAnswerQuery, + routedSearchContext, + routedSearchContextSignature, ]); function pickRecentQuery(recentQuery: string) { @@ -2222,7 +2279,7 @@ export function ClinicalDashboard({ setMedicationSearchQuery(crossQuery); } setSearchMode(mode); - router.push(appModeHomeHref(mode, { query: crossQuery, focus: true, run: true })); + router.push(appModeHomeHref(mode, { query: crossQuery, focus: true, run: true, queryMode, scopeFilters })); } async function submitAnswerFeedback(feedbackType: AnswerFeedbackType) { @@ -2306,8 +2363,16 @@ export function ClinicalDashboard({ window.requestAnimationFrame(() => mainRef.current?.scrollTo({ top: 0, behavior: "smooth" })); } - function updateDocumentSearchUrl(searchText: string, mode: AppModeId = "documents") { - window.history.replaceState(null, "", appModeHomeHref(mode, { query: searchText })); + function updateDocumentSearchUrl( + searchText: string, + mode: AppModeId = "documents", + filtersOverride: SearchScopeFilters = scopeFilters, + ) { + window.history.replaceState( + null, + "", + appModeHomeHref(mode, { query: searchText, queryMode, scopeFilters: filtersOverride }), + ); } async function runDocumentSearchShortcut( @@ -2327,7 +2392,17 @@ export function ClinicalDashboard({ setAnswerProgress(null); rememberRecentQuery(trimmedSearchText); window.requestAnimationFrame(() => mainRef.current?.scrollTo({ top: 0, behavior: "smooth" })); - if (updateUrl) router.push(documentsSearchHref({ query: trimmedSearchText, focus: true, run: true })); + if (updateUrl) { + router.push( + documentsSearchHref({ + query: trimmedSearchText, + focus: true, + run: true, + queryMode, + scopeFilters: filtersOverride, + }), + ); + } return; } if (!canRunSearch) { @@ -2352,7 +2427,7 @@ export function ClinicalDashboard({ setAnswerViewMode("high_yield"); rememberRecentQuery(trimmedSearchText); window.requestAnimationFrame(() => mainRef.current?.scrollTo({ top: 0, behavior: "smooth" })); - if (updateUrl) updateDocumentSearchUrl(trimmedSearchText, targetMode); + if (updateUrl) updateDocumentSearchUrl(trimmedSearchText, targetMode, filtersOverride); const requestId = ++searchRequestSeqRef.current; @@ -2482,7 +2557,7 @@ export function ClinicalDashboard({ setSourceGovernanceWarnings([]); setDocumentMatches([]); setSearchMode(mode); - router.push(appModeHomeHref(mode)); + router.push(appModeHomeHref(mode, { queryMode, scopeFilters })); } function focusComposerInput() { @@ -3411,7 +3486,7 @@ export function ClinicalDashboard({ onClearQuery={() => { setQuery(""); setModeSearchSubmitted(false); - router.replace(appModeHomeHref("favourites", { focus: true })); + router.replace(appModeHomeHref("favourites", { focus: true, queryMode, scopeFilters })); }} onAddFavourite={() => setActionNotice({ tone: "success", message: "Favourite creation is ready to connect." }) diff --git a/src/components/clinical-dashboard/account-setup-dialog.tsx b/src/components/clinical-dashboard/account-setup-dialog.tsx index 9f5b7f790..e60942d87 100644 --- a/src/components/clinical-dashboard/account-setup-dialog.tsx +++ b/src/components/clinical-dashboard/account-setup-dialog.tsx @@ -117,7 +117,7 @@ export function AccountSetupDialog({ open, onClose }: { open: boolean; onClose:

Set up your workspace

diff --git a/src/components/clinical-dashboard/clinical-output-helpers.tsx b/src/components/clinical-dashboard/clinical-output-helpers.tsx index c9d784fb2..aa0177236 100644 --- a/src/components/clinical-dashboard/clinical-output-helpers.tsx +++ b/src/components/clinical-dashboard/clinical-output-helpers.tsx @@ -184,7 +184,7 @@ export function AnswerViewModeControl({ aria-label={`Show ${mode.label.toLowerCase()} answer view`} title={mode.label} className={cn( - "inline-flex min-h-9 min-w-0 flex-1 basis-[4.75rem] items-center justify-center gap-1.5 rounded-md px-2 text-xs font-semibold transition sm:flex-none sm:basis-auto sm:px-2.5", + "inline-flex min-h-tap min-w-0 flex-1 basis-[4.75rem] items-center justify-center gap-1.5 rounded-md px-2 text-xs font-semibold transition sm:flex-none sm:basis-auto sm:px-2.5 lg:min-h-9", active ? "bg-[color:var(--primary)] text-[color:var(--primary-contrast)] shadow-sm" : "text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)]", diff --git a/src/components/clinical-dashboard/evidence-panels.tsx b/src/components/clinical-dashboard/evidence-panels.tsx index f930d10cd..60581db88 100644 --- a/src/components/clinical-dashboard/evidence-panels.tsx +++ b/src/components/clinical-dashboard/evidence-panels.tsx @@ -685,7 +685,7 @@ export function ClinicalNotesChecklistPanel({ @@ -1051,7 +1051,7 @@ export function MasterSearchHeader({
- + Label filters {activeLabelFilterCount ? `${activeLabelFilterCount} active` : "Medication, site, action, intent"} @@ -1063,7 +1063,7 @@ export function MasterSearchHeader({ diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx index 35786ae0d..5e6404c0c 100644 --- a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx +++ b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx @@ -311,7 +311,7 @@ function MedicationResults({

Medication search

-

+

{resultCount} prescribing matches

diff --git a/src/components/clinical-dashboard/medication-record-page.tsx b/src/components/clinical-dashboard/medication-record-page.tsx index 816c52b3c..6e5411b60 100644 --- a/src/components/clinical-dashboard/medication-record-page.tsx +++ b/src/components/clinical-dashboard/medication-record-page.tsx @@ -185,7 +185,7 @@ function MedicationRecordDetail({