From 0a0b4e2b15fc970328b4011fc4b30b1ea745af4a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:07:29 +0800 Subject: [PATCH 01/10] feat: use global footer search on tools phones --- docs/search-chrome-behaviour.md | 2 +- .../global-search-shell.tsx | 29 ++++++++++++------- .../mobile-composer-reserve.ts | 13 +++++---- src/lib/search-shell-props.ts | 7 ++++- tests/mobile-composer-reserve.test.ts | 20 ++++++++++--- tests/search-shell-props.test.ts | 8 +++++ tests/ui-tools.spec.ts | 8 ++--- 7 files changed, 61 insertions(+), 26 deletions(-) diff --git a/docs/search-chrome-behaviour.md b/docs/search-chrome-behaviour.md index df06a54db6..f28f5e564a 100644 --- a/docs/search-chrome-behaviour.md +++ b/docs/search-chrome-behaviour.md @@ -7,7 +7,7 @@ This repo uses one shared search experience across the global shell, dashboard r | Page state | Composer placement | Reserve owner | | ---------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | | Answer home / standalone mode homes | In-flow hero composer on phones and larger breakpoints | Page content; no fixed phone dock reserve | -| Tools directory (`/tools`, no submitted query) | In-flow composer above the all-tools results directory | Page content; no fixed phone dock reserve | +| Tools directory (`/tools`, no submitted query) | Compact bottom dock on phones; in-flow hero composer from `sm` | Shell dock reserve on phones; page content from `sm` | | Submitted/search-result views | Compact bottom dock on phones; in normal page flow on tablets and desktops | Shell/dashboard `--mobile-composer-reserve` on phones; page content on desktop | | Answer result view | Overlaid glass header plus answer composer dock | Dashboard `#main-content` top/bottom reserves | | Document detail/source routes | `DocumentViewer` floating composer | `DocumentViewer` content padding | diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 1e37cb3c4e..703b7d94e6 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -108,6 +108,8 @@ type GlobalSearchShellProps = { initialMode?: AppModeId; availableModeIds?: readonly AppModeId[]; desktopSearchPlacement?: "default" | "hero"; + /** Override the phone placement for a standalone mode home's shared composer. */ + mobileHomeComposerPlacement?: "hero" | "footer"; /** Hide the shared search composer on routes that provide their own search surface. */ searchComposerVisible?: boolean; /** Keep the global header/search while allowing a route to use the full desktop canvas. */ @@ -311,6 +313,7 @@ function GlobalStandaloneSearchShellBody({ initialMode = "answer", availableModeIds, desktopSearchPlacement = "default", + mobileHomeComposerPlacement = "hero", searchComposerVisible = true, hideDesktopSidebar = false, chromeVisible = true, @@ -444,9 +447,14 @@ function GlobalStandaloneSearchShellBody({ searchComposerVisible && !isDifferentialPresentationWorkflow && (!isInfoPage || isToolDetailWithFooterSearch(pathname)); + const heroOwnsPhoneComposer = isStandaloneModeHome && mobileHomeComposerPlacement === "hero"; + // This flag controls sm+ padding, where every standalone home (including + // Tools) keeps its composer in flow. Phone clearance is resolved separately + // from heroOwnsPhoneComposer below. const reservesFloatingComposer = shouldShowSearchComposer && !isStandaloneModeHome; - // Standalone mode homes keep the in-flow hero pill at every width (no phone - // dock reserve). Document viewer routes own their own floating composer, so + // Most standalone mode homes keep the in-flow hero pill at every width. Tools + // deliberately uses the shared footer on phones. Document viewer routes own + // their own floating composer, so // the shell keeps only a small pad and lets DocumentViewer manage clearance. // Release the large bottom reserve only when the phone bottom composer is // actually hidden (MasterSearchHeader's bottomComposerHidden). Header-only @@ -461,7 +469,7 @@ function GlobalStandaloneSearchShellBody({ resolveShellVisibleMobileComposerReserve({ shouldShowSearchComposer, pageOwnedComposerRoute: isPageOwnedComposerRoute(pathname), - isStandaloneModeHome, + heroOwnsPhoneComposer, searchMode, differentialsCompareAddonActive, }), @@ -886,17 +894,18 @@ function GlobalStandaloneSearchShellBody({ } mobileBottomSearchAddonKind={differentialsCompareAddonActive ? "differentials-compare" : undefined} desktopSearchPlacement={desktopSearchPlacement === "hero" && isStandaloneModeHome ? "hero" : "default"} - showPhoneSuggestionTickerOnHome={isStandaloneModeHome || (pathname === "/" && !hasSubmittedModeSearch)} + showPhoneSuggestionTickerOnHome={ + heroOwnsPhoneComposer || (pathname === "/" && !hasSubmittedModeSearch) + } searchComposerVisible={shouldShowSearchComposer} desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined} desktopPageComposerSlotId={ shouldShowSearchComposer && !isStandaloneModeHome ? desktopPageComposerSlotId : undefined } - // Standalone mode homes keep the in-flow hero pill at every width, - // phones included — the composer sits in the middle of the hero and - // scrolls with the content, matching the answer home rather than - // docking to the bottom edge. - heroComposerBreakpoint="all" + // Most standalone homes keep the in-flow hero pill at every width. + // Tools keeps that placement from sm up but uses the same global + // footer dock as submitted views on phones. + heroComposerBreakpoint={mobileHomeComposerPlacement === "footer" ? "sm-up" : "all"} // Phones: #main-content owns vertical scroll, so hide-on-scroll // collapses the top bar to hand space back to content. // Tablet and desktop portal search into normal page flow. The outer @@ -936,7 +945,7 @@ function GlobalStandaloneSearchShellBody({ data-chrome-transitioning={chromeTransitioning ? "true" : undefined} data-phone-scroll-owner={activeScrollOwner} data-phone-footer-owner={ - isStandaloneModeHome + heroOwnsPhoneComposer ? "hero" : isPageOwnedComposerRoute(pathname) ? "page" diff --git a/src/components/clinical-dashboard/mobile-composer-reserve.ts b/src/components/clinical-dashboard/mobile-composer-reserve.ts index 326655f798..8c1bb6aa2e 100644 --- a/src/components/clinical-dashboard/mobile-composer-reserve.ts +++ b/src/components/clinical-dashboard/mobile-composer-reserve.ts @@ -113,7 +113,8 @@ export function resolveShellVisibleMobileComposerReserve(input: { /** @deprecated Prefer pageOwnedComposerRoute */ documentViewerOwnedRoute?: boolean; pageOwnedComposerRoute?: boolean; - isStandaloneModeHome: boolean; + /** The standalone hero owns the phone composer instead of the shared footer dock. */ + heroOwnsPhoneComposer: boolean; searchMode: string; differentialsCompareAddonActive: boolean; patientDetailsAddonActive?: boolean; @@ -124,11 +125,11 @@ export function resolveShellVisibleMobileComposerReserve(input: { const pageOwned = input.pageOwnedComposerRoute ?? input.documentViewerOwnedRoute ?? false; return pageOwned ? mobileComposerHiddenReserve : mobileComposerIdleReserve; } - // Standalone mode homes keep the in-flow hero pill at every width (phones - // included), so the composer sits in the content flow rather than docking to - // the bottom edge. Reserve only the idle content pad — matching the answer - // home hero — so no empty band opens below the pill. - if (input.isStandaloneModeHome) return mobileComposerIdleReserve; + // An all-width hero composer sits in content flow rather than docking to the + // bottom edge. Reserve only the idle content pad so no empty band opens below + // the pill. Standalone homes whose phone placement is the shared footer use + // the normal shell dock reserve below. + if (input.heroOwnsPhoneComposer) return mobileComposerIdleReserve; if (input.searchMode === "answer") return mobileComposerVisibleReserve.shellAnswer; if (input.differentialsCompareAddonActive) return mobileComposerVisibleReserve.differentialsCompare; if (input.patientDetailsAddonActive) return mobileComposerVisibleReserve.patientDetails; diff --git a/src/lib/search-shell-props.ts b/src/lib/search-shell-props.ts index 3abf405a01..7c2d162546 100644 --- a/src/lib/search-shell-props.ts +++ b/src/lib/search-shell-props.ts @@ -4,6 +4,7 @@ export type SearchShellPathProps = { initialMode: AppModeId; availableModeIds?: AppModeId[]; desktopSearchPlacement?: "default" | "hero"; + mobileHomeComposerPlacement?: "hero" | "footer"; searchComposerVisible?: boolean; mobileChromeVisible?: boolean; }; @@ -67,7 +68,11 @@ export function searchShellPropsForPathname(pathname: string): SearchShellPathPr } if (pathname.startsWith("/tools")) { - return { initialMode: "tools", desktopSearchPlacement: "hero" }; + return { + initialMode: "tools", + desktopSearchPlacement: "hero", + mobileHomeComposerPlacement: "footer", + }; } if (pathname.startsWith("/calculators")) { diff --git a/tests/mobile-composer-reserve.test.ts b/tests/mobile-composer-reserve.test.ts index 91d5ecd7b1..457d55ab42 100644 --- a/tests/mobile-composer-reserve.test.ts +++ b/tests/mobile-composer-reserve.test.ts @@ -43,7 +43,7 @@ describe("mobile composer reserve contract", () => { resolveShellVisibleMobileComposerReserve({ shouldShowSearchComposer: false, documentViewerOwnedRoute: true, - isStandaloneModeHome: false, + heroOwnsPhoneComposer: false, searchMode: "documents", differentialsCompareAddonActive: false, }), @@ -55,13 +55,25 @@ describe("mobile composer reserve contract", () => { resolveShellVisibleMobileComposerReserve({ shouldShowSearchComposer: true, documentViewerOwnedRoute: false, - isStandaloneModeHome: true, + heroOwnsPhoneComposer: true, searchMode: "services", differentialsCompareAddonActive: false, }), ).toBe(mobileComposerIdleReserve); }); + it("uses the shared compact dock reserve when a standalone home delegates phones to the footer", () => { + expect( + resolveShellVisibleMobileComposerReserve({ + shouldShowSearchComposer: true, + documentViewerOwnedRoute: false, + heroOwnsPhoneComposer: false, + searchMode: "tools", + differentialsCompareAddonActive: false, + }), + ).toBe(mobileComposerVisibleReserve.shellDock); + }); + it("uses the compact dock reserve for non-answer dashboard docks when the hero does not own phones", () => { for (const searchMode of ["documents", "services", "forms", "tools", "favourites"]) { expect( @@ -188,7 +200,7 @@ describe("mobile composer reserve contract", () => { resolveShellVisibleMobileComposerReserve({ shouldShowSearchComposer: true, documentViewerOwnedRoute: false, - isStandaloneModeHome: false, + heroOwnsPhoneComposer: false, searchMode: "differentials", differentialsCompareAddonActive: true, }), @@ -213,7 +225,7 @@ describe("mobile composer reserve contract", () => { resolveShellVisibleMobileComposerReserve({ shouldShowSearchComposer: false, pageOwnedComposerRoute: true, - isStandaloneModeHome: false, + heroOwnsPhoneComposer: false, searchMode: "tools", differentialsCompareAddonActive: false, }), diff --git a/tests/search-shell-props.test.ts b/tests/search-shell-props.test.ts index 08299a4bd8..982d7f7605 100644 --- a/tests/search-shell-props.test.ts +++ b/tests/search-shell-props.test.ts @@ -28,6 +28,14 @@ describe("searchShellPropsForPathname", () => { }); }); + it("keeps tools in the hero from tablet up and uses the shared phone footer", () => { + expect(searchShellPropsForPathname("/tools")).toEqual({ + initialMode: "tools", + desktopSearchPlacement: "hero", + mobileHomeComposerPlacement: "footer", + }); + }); + it("maps therapy and home fallbacks", () => { expect(searchShellPropsForPathname("/therapy-compass/search")).toEqual({ initialMode: "therapy-compass", diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 877af366be..6ef04b83ee 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -488,9 +488,10 @@ test.describe("Clinical KB tools directory and legacy launcher", () => { await expect(page.getByLabel("Mode Tools")).toBeVisible(); await expect(visibleGlobalSearchInput(page)).toHaveCount(1); if (viewport.name === "mobile") { - // Phones keep the compact shared search in the tools-home hero slot. - await expect(page.getByTestId("tools-home").getByTestId("global-search-input")).toBeVisible(); - await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); + // Tools delegates its phone composer to the same shared global footer + // used by submitted views; tablet and desktop retain the hero slot. + await expect(page.getByTestId("tools-home").getByTestId("global-search-input")).toHaveCount(0); + await expect(page.locator("form.answer-footer-search-dock")).toBeVisible(); } else { await expect(page.getByTestId("tools-home").getByTestId("global-search-input")).toBeVisible(); } @@ -985,7 +986,6 @@ test.describe("Clinical KB tools directory and legacy launcher", () => { { path: "/differentials", testId: "differentials-home" }, { path: "/factsheets", testId: "factsheets-home-main" }, { path: "/favourites", testId: "favourites-hub" }, - { path: "/tools", testId: "tools-search-results-page" }, ] as const) { await gotoLauncher(page, home.path); const homeSurface = page.getByTestId(home.testId); From 3b2c8fe260fac57e7b370f3c1706e063b65a10fb Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 16 Aug 2026 01:27:38 +0800 Subject: [PATCH 02/10] style: format tools search chrome --- src/components/clinical-dashboard/global-search-shell.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 703b7d94e6..87ddb83424 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -894,9 +894,7 @@ function GlobalStandaloneSearchShellBody({ } mobileBottomSearchAddonKind={differentialsCompareAddonActive ? "differentials-compare" : undefined} desktopSearchPlacement={desktopSearchPlacement === "hero" && isStandaloneModeHome ? "hero" : "default"} - showPhoneSuggestionTickerOnHome={ - heroOwnsPhoneComposer || (pathname === "/" && !hasSubmittedModeSearch) - } + showPhoneSuggestionTickerOnHome={heroOwnsPhoneComposer || (pathname === "/" && !hasSubmittedModeSearch)} searchComposerVisible={shouldShowSearchComposer} desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined} desktopPageComposerSlotId={ From 6cb888d8a2795a8f711b7f2ee2e06338d485da2b Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 16 Aug 2026 07:13:17 +0800 Subject: [PATCH 03/10] test(tools): update phone dock reserve contract --- tests/phone-dock-addon-contract.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/phone-dock-addon-contract.test.ts b/tests/phone-dock-addon-contract.test.ts index 6f45f7a8cb..f776c0f43b 100644 --- a/tests/phone-dock-addon-contract.test.ts +++ b/tests/phone-dock-addon-contract.test.ts @@ -107,7 +107,7 @@ describe("reserve resolvers honour the patient-details addon", () => { expect( resolveShellVisibleMobileComposerReserve({ shouldShowSearchComposer: true, - isStandaloneModeHome: false, + heroOwnsPhoneComposer: false, searchMode: "prescribing", differentialsCompareAddonActive: false, patientDetailsAddonActive: true, @@ -119,7 +119,7 @@ describe("reserve resolvers honour the patient-details addon", () => { expect( resolveShellVisibleMobileComposerReserve({ shouldShowSearchComposer: true, - isStandaloneModeHome: false, + heroOwnsPhoneComposer: false, searchMode: "differentials", differentialsCompareAddonActive: true, patientDetailsAddonActive: true, @@ -133,7 +133,7 @@ describe("reserve resolvers honour the patient-details addon", () => { expect( resolveShellVisibleMobileComposerReserve({ shouldShowSearchComposer: true, - isStandaloneModeHome: true, + heroOwnsPhoneComposer: true, searchMode: "prescribing", differentialsCompareAddonActive: false, patientDetailsAddonActive: true, From 358335dc8e3640b57af09efd972d72a3defa0b9a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:09:29 +0800 Subject: [PATCH 04/10] fix: preserve Tools phone search suggestions --- ...ccaa00cfc3bd7fedeb56a5a019e93f3966580d3d41434e2a47.record.md | 1 + src/components/clinical-dashboard/global-search-shell.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/branch-review-records/1798e0af4644bdccaa00cfc3bd7fedeb56a5a019e93f3966580d3d41434e2a47.record.md diff --git a/docs/branch-review-records/1798e0af4644bdccaa00cfc3bd7fedeb56a5a019e93f3966580d3d41434e2a47.record.md b/docs/branch-review-records/1798e0af4644bdccaa00cfc3bd7fedeb56a5a019e93f3966580d3d41434e2a47.record.md new file mode 100644 index 0000000000..88903cd3a0 --- /dev/null +++ b/docs/branch-review-records/1798e0af4644bdccaa00cfc3bd7fedeb56a5a019e93f3966580d3d41434e2a47.record.md @@ -0,0 +1 @@ +| 2026-08-16 | PR-1993 | 0df502749540c4e4de789bfad12837726b1944d8 | PR #1993 Tools phone footer search review | Fixed: preserved the tappable standalone-home suggestion ticker while retaining the Tools phone shared-footer placement. | Static phone-home truth table passed; exact one-line fix diff verified; prior exact-head CI isolated to the audit assertion; distinct manual adversarial pass found no additional high-confidence defects. | diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 87ddb83424..ab549142ab 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -894,7 +894,7 @@ function GlobalStandaloneSearchShellBody({ } mobileBottomSearchAddonKind={differentialsCompareAddonActive ? "differentials-compare" : undefined} desktopSearchPlacement={desktopSearchPlacement === "hero" && isStandaloneModeHome ? "hero" : "default"} - showPhoneSuggestionTickerOnHome={heroOwnsPhoneComposer || (pathname === "/" && !hasSubmittedModeSearch)} + showPhoneSuggestionTickerOnHome={isStandaloneModeHome || (pathname === "/" && !hasSubmittedModeSearch)} searchComposerVisible={shouldShowSearchComposer} desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined} desktopPageComposerSlotId={ From 14c8474a7116af00fbaaae6c2463cbc447991e7a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:28:53 +0800 Subject: [PATCH 05/10] chore(ci): stage exact-head Tools phone fix --- .github/workflows/codex-fix-pr-1993.yml | 117 ++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .github/workflows/codex-fix-pr-1993.yml diff --git a/.github/workflows/codex-fix-pr-1993.yml b/.github/workflows/codex-fix-pr-1993.yml new file mode 100644 index 0000000000..6094e60a2b --- /dev/null +++ b/.github/workflows/codex-fix-pr-1993.yml @@ -0,0 +1,117 @@ +name: Codex fix PR 1993 + +on: + push: + branches: + - codex/tools-universal-footer-search + +permissions: + contents: write + +jobs: + fix: + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - name: Checkout exact PR head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: package-lock.json + + - name: Apply the Tools phone ownership correction + shell: bash + run: | + set -euo pipefail + python - <<'PY' + from pathlib import Path + + dashboard = Path("src/components/ClinicalDashboard.tsx") + text = dashboard.read_text() + old = ''' // Any mounted mode home (answer, documents, prescribing, differentials, tools, + // favourites) keeps the in-flow hero pill on phones ("all") per the + // page-ownership contract. Only result/submitted views fall back to "sm-up" + // so phones get the compact bottom dock. + const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up"; + ''' + new = ''' // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the + // deliberate exception: its content-rich directory uses the shared compact + // footer on phones while retaining the hero composer from sm upward. + const heroComposerBreakpoint = + showDesktopHomeComposer && activeModeResultKind !== "tools" ? "all" : "sm-up"; + ''' + if text.count(old) != 1: + raise SystemExit(f"expected one ClinicalDashboard ownership block, found {text.count(old)}") + dashboard.write_text(text.replace(old, new)) + + spec = Path("tests/ui-tools.spec.ts") + text = spec.read_text() + old = ''' await expect(page.getByTestId("tools-search-results-page")).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "All tools" })).toBeVisible(); + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); + await expectNoPageHorizontalOverflow(page); + ''' + new = ''' await expect(page.getByTestId("tools-search-results-page")).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "All tools" })).toBeVisible(); + if (viewport.name === "phone") { + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toHaveCount(0); + await expect(page.locator("form.answer-footer-search-dock")).toBeVisible(); + } else { + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); + await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); + } + await expectNoPageHorizontalOverflow(page); + ''' + if text.count(old) != 1: + raise SystemExit(f"expected one mode-picker assertion block, found {text.count(old)}") + text = text.replace(old, new) + + old = ''' await expect(page.getByRole("heading", { level: 2, name: "Clinical KB Search" }).first()).toBeVisible(); + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); + await expectNoPageHorizontalOverflow(page); + ''' + new = ''' await expect(page.getByRole("heading", { level: 2, name: "Clinical KB Search" }).first()).toBeVisible(); + if (width < 640) { + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toHaveCount(0); + await expect(page.locator("form.answer-footer-search-dock")).toBeVisible(); + } else { + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); + await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); + } + await expectNoPageHorizontalOverflow(page); + ''' + if text.count(old) != 1: + raise SystemExit(f"expected one breakpoint assertion block, found {text.count(old)}") + spec.write_text(text.replace(old, new)) + PY + + - name: Install development dependencies + run: npm ci --include=dev --ignore-scripts + + - name: Format and run focused offline verification + shell: bash + run: | + set -euo pipefail + npx prettier --write src/components/ClinicalDashboard.tsx tests/ui-tools.spec.ts + npx prettier --check src/components/ClinicalDashboard.tsx tests/ui-tools.spec.ts + npm run typecheck + npm test -- tests/search-shell-props.test.ts tests/mobile-composer-reserve.test.ts tests/phone-dock-addon-contract.test.ts + + - name: Commit the focused fix and remove this temporary workflow + shell: bash + run: | + set -euo pipefail + git rm .github/workflows/codex-fix-pr-1993.yml + git diff --check + git status --short + git config user.name "Codex" + git config user.email "codex@users.noreply.github.com" + git add src/components/ClinicalDashboard.tsx tests/ui-tools.spec.ts + git commit -m "fix(tools): use the shared phone footer across Tools homes" + git push origin "HEAD:${GITHUB_REF_NAME}" From d9d83419b5e8ee7d345b58c2ef872cf44439c3e8 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 16 Aug 2026 20:32:25 +0800 Subject: [PATCH 06/10] chore(ci): make Tools repair resilient to formatting --- .github/workflows/codex-fix-pr-1993.yml | 37 ++++++++++++------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/.github/workflows/codex-fix-pr-1993.yml b/.github/workflows/codex-fix-pr-1993.yml index 6094e60a2b..8208404c1d 100644 --- a/.github/workflows/codex-fix-pr-1993.yml +++ b/.github/workflows/codex-fix-pr-1993.yml @@ -34,29 +34,29 @@ jobs: dashboard = Path("src/components/ClinicalDashboard.tsx") text = dashboard.read_text() - old = ''' // Any mounted mode home (answer, documents, prescribing, differentials, tools, + old_line = ' const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";' + new_lines = ''' const heroComposerBreakpoint = + showDesktopHomeComposer && activeModeResultKind !== "tools" ? "all" : "sm-up";''' + if text.count(old_line) != 1: + raise SystemExit(f"expected one hero composer assignment, found {text.count(old_line)}") + text = text.replace(old_line, new_lines) + text = text.replace( + ''' // Any mounted mode home (answer, documents, prescribing, differentials, tools, // favourites) keeps the in-flow hero pill on phones ("all") per the // page-ownership contract. Only result/submitted views fall back to "sm-up" - // so phones get the compact bottom dock. - const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up"; - ''' - new = ''' // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the + // so phones get the compact bottom dock.''', + ''' // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the // deliberate exception: its content-rich directory uses the shared compact - // footer on phones while retaining the hero composer from sm upward. - const heroComposerBreakpoint = - showDesktopHomeComposer && activeModeResultKind !== "tools" ? "all" : "sm-up"; - ''' - if text.count(old) != 1: - raise SystemExit(f"expected one ClinicalDashboard ownership block, found {text.count(old)}") - dashboard.write_text(text.replace(old, new)) + // footer on phones while retaining the hero composer from sm upward.''', + ) + dashboard.write_text(text) spec = Path("tests/ui-tools.spec.ts") text = spec.read_text() old = ''' await expect(page.getByTestId("tools-search-results-page")).toBeVisible(); await expect(page.getByRole("heading", { level: 1, name: "All tools" })).toBeVisible(); await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); - await expectNoPageHorizontalOverflow(page); - ''' + await expectNoPageHorizontalOverflow(page);''' new = ''' await expect(page.getByTestId("tools-search-results-page")).toBeVisible(); await expect(page.getByRole("heading", { level: 1, name: "All tools" })).toBeVisible(); if (viewport.name === "phone") { @@ -66,16 +66,14 @@ jobs: await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); } - await expectNoPageHorizontalOverflow(page); - ''' + await expectNoPageHorizontalOverflow(page);''' if text.count(old) != 1: raise SystemExit(f"expected one mode-picker assertion block, found {text.count(old)}") text = text.replace(old, new) old = ''' await expect(page.getByRole("heading", { level: 2, name: "Clinical KB Search" }).first()).toBeVisible(); await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); - await expectNoPageHorizontalOverflow(page); - ''' + await expectNoPageHorizontalOverflow(page);''' new = ''' await expect(page.getByRole("heading", { level: 2, name: "Clinical KB Search" }).first()).toBeVisible(); if (width < 640) { await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toHaveCount(0); @@ -84,8 +82,7 @@ jobs: await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); } - await expectNoPageHorizontalOverflow(page); - ''' + await expectNoPageHorizontalOverflow(page);''' if text.count(old) != 1: raise SystemExit(f"expected one breakpoint assertion block, found {text.count(old)}") spec.write_text(text.replace(old, new)) From e2d2447b5b101913ee53ed4bf8ba52a160d92e9f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:30:54 +0800 Subject: [PATCH 07/10] chore(ci): harden PR 1993 repair gate --- .github/workflows/codex-fix-pr-1993.yml | 166 +++++++++++++++++++++--- 1 file changed, 145 insertions(+), 21 deletions(-) diff --git a/.github/workflows/codex-fix-pr-1993.yml b/.github/workflows/codex-fix-pr-1993.yml index 8208404c1d..cef907b847 100644 --- a/.github/workflows/codex-fix-pr-1993.yml +++ b/.github/workflows/codex-fix-pr-1993.yml @@ -8,10 +8,14 @@ on: permissions: contents: write +concurrency: + group: codex-fix-pr-1993 + cancel-in-progress: false + jobs: fix: runs-on: ubuntu-24.04 - timeout-minutes: 25 + timeout-minutes: 35 steps: - name: Checkout exact PR head uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -25,7 +29,34 @@ jobs: cache: npm cache-dependency-path: package-lock.json - - name: Apply the Tools phone ownership correction + - name: Snapshot authoritative head and base + shell: bash + run: | + set -euo pipefail + git fetch --no-tags origin \ + "+refs/heads/${GITHUB_REF_NAME}:refs/remotes/origin/${GITHUB_REF_NAME}" \ + "+refs/heads/main:refs/remotes/origin/main" + remote_head="$(git rev-parse "refs/remotes/origin/${GITHUB_REF_NAME}")" + if [[ "$remote_head" != "$GITHUB_SHA" ]]; then + echo "PR head moved from $GITHUB_SHA to $remote_head; refusing to overwrite it." + exit 78 + fi + { + echo "REVIEWED_HEAD=$GITHUB_SHA" + echo "BASE_SHA=$(git rev-parse refs/remotes/origin/main)" + } >> "$GITHUB_ENV" + + - name: Merge latest base when needed + shell: bash + run: | + set -euo pipefail + git config user.name "Codex" + git config user.email "codex@users.noreply.github.com" + if ! git merge-base --is-ancestor "$BASE_SHA" HEAD; then + git merge --no-commit --no-ff "$BASE_SHA" + fi + + - name: Apply the verified Tools phone correction shell: bash run: | set -euo pipefail @@ -34,22 +65,70 @@ jobs: dashboard = Path("src/components/ClinicalDashboard.tsx") text = dashboard.read_text() + old_comment = "\n".join( + [ + ' // Any mounted mode home (answer, documents, prescribing, differentials, tools,', + ' // favourites) keeps the in-flow hero pill on phones ("all") per the', + ' // page-ownership contract. Only result/submitted views fall back to "sm-up"', + ' // so phones get the compact bottom dock.', + ] + ) + new_comment = "\n".join( + [ + " // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the", + " // deliberate exception: its content-rich directory uses the shared compact", + " // footer on phones while retaining the hero composer from sm upward.", + ] + ) + if text.count(old_comment) != 1: + raise SystemExit(f"expected one hero ownership comment, found {text.count(old_comment)}") + text = text.replace(old_comment, new_comment) + old_line = ' const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";' new_lines = ''' const heroComposerBreakpoint = showDesktopHomeComposer && activeModeResultKind !== "tools" ? "all" : "sm-up";''' if text.count(old_line) != 1: raise SystemExit(f"expected one hero composer assignment, found {text.count(old_line)}") - text = text.replace(old_line, new_lines) - text = text.replace( - ''' // Any mounted mode home (answer, documents, prescribing, differentials, tools, - // favourites) keeps the in-flow hero pill on phones ("all") per the - // page-ownership contract. Only result/submitted views fall back to "sm-up" - // so phones get the compact bottom dock.''', - ''' // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the - // deliberate exception: its content-rich directory uses the shared compact - // footer on phones while retaining the hero composer from sm upward.''', + dashboard.write_text(text.replace(old_line, new_lines)) + + contract = Path("tests/mobile-composer-reserve.test.ts") + text = contract.read_text() + old_intro = "\n".join( + [ + ' it("derives hero phone ownership from the mounted hero slot; any mode home uses all-widths breakpoint", () => {', + " // Any mounted mode home (answer, documents, prescribing, tools, favourites)", + ' // needs "all" (phones keep the in-flow hero pill) per the page-ownership', + ' // contract. Only result/submitted views use "sm-up" so phones get the compact', + " // bottom dock. desktopHomeComposerSlotId is undefined on result views, so", + " // heroOwnsPhoneComposer stays false there regardless of the breakpoint value.", + ] ) - dashboard.write_text(text) + new_intro = "\n".join( + [ + ' it("derives hero phone ownership from the mounted hero slot while Tools delegates phones to the footer", () => {', + " // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the", + " // deliberate exception: its content-rich directory delegates phones to the", + ' // compact footer while retaining the hero from sm upward. Result/submitted', + ' // views also use "sm-up"; desktopHomeComposerSlotId is undefined there, so', + " // heroOwnsPhoneComposer stays false regardless of the breakpoint value.", + ] + ) + if text.count(old_intro) != 1: + raise SystemExit(f"expected one mobile ownership contract intro, found {text.count(old_intro)}") + text = text.replace(old_intro, new_intro) + + old_expectation = ( + ' expect(dashboard).toContain(\'const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";\');' + ) + new_expectation = ''' expect(dashboard).toMatch( + /const heroComposerBreakpoint =\s*showDesktopHomeComposer && activeModeResultKind !== "tools" \? "all" : "sm-up";/, + ); + expect(dashboard).not.toContain( + 'const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";', + );''' + if text.count(old_expectation) != 1: + raise SystemExit(f"expected one stale breakpoint assertion, found {text.count(old_expectation)}") + contract.write_text(text.replace(old_expectation, new_expectation)) spec = Path("tests/ui-tools.spec.ts") text = spec.read_text() @@ -95,20 +174,65 @@ jobs: shell: bash run: | set -euo pipefail - npx prettier --write src/components/ClinicalDashboard.tsx tests/ui-tools.spec.ts - npx prettier --check src/components/ClinicalDashboard.tsx tests/ui-tools.spec.ts + npx prettier --write \ + src/components/ClinicalDashboard.tsx \ + tests/mobile-composer-reserve.test.ts \ + tests/ui-tools.spec.ts + npx prettier --check \ + src/components/ClinicalDashboard.tsx \ + tests/mobile-composer-reserve.test.ts \ + tests/ui-tools.spec.ts + npx eslint \ + src/components/ClinicalDashboard.tsx \ + tests/mobile-composer-reserve.test.ts \ + tests/ui-tools.spec.ts \ + --max-warnings 0 npm run typecheck - npm test -- tests/search-shell-props.test.ts tests/mobile-composer-reserve.test.ts tests/phone-dock-addon-contract.test.ts + npm test -- \ + tests/search-shell-props.test.ts \ + tests/mobile-composer-reserve.test.ts \ + tests/phone-dock-addon-contract.test.ts - - name: Commit the focused fix and remove this temporary workflow + - name: Append immutable review record + shell: bash + run: | + set -euo pipefail + npm run ledger:append -- \ + --date 2026-08-16 \ + --scope "PR #1993 Tools phone footer ownership and current-base review" \ + --base "$BASE_SHA" \ + --head "$REVIEWED_HEAD" \ + --checks "Prettier; ESLint on changed files; TypeScript; targeted Vitest; immutable ledger validation" \ + --outcome "Confirmed and fixed dashboard Tools phone composer ownership, aligned stale responsive contracts, removed the temporary repair workflow, and kept the branch current with main." + npm run check:branch-review-ledger + + - name: Commit the verified fix and remove this temporary workflow shell: bash run: | set -euo pipefail git rm .github/workflows/codex-fix-pr-1993.yml + git add \ + src/components/ClinicalDashboard.tsx \ + tests/mobile-composer-reserve.test.ts \ + tests/ui-tools.spec.ts \ + docs/branch-review-records git diff --check + git diff --cached --check git status --short - git config user.name "Codex" - git config user.email "codex@users.noreply.github.com" - git add src/components/ClinicalDashboard.tsx tests/ui-tools.spec.ts - git commit -m "fix(tools): use the shared phone footer across Tools homes" - git push origin "HEAD:${GITHUB_REF_NAME}" + + git fetch --no-tags origin \ + "+refs/heads/${GITHUB_REF_NAME}:refs/remotes/origin/${GITHUB_REF_NAME}" \ + "+refs/heads/main:refs/remotes/origin/main" + current_remote_head="$(git rev-parse "refs/remotes/origin/${GITHUB_REF_NAME}")" + current_base="$(git rev-parse refs/remotes/origin/main)" + if [[ "$current_remote_head" != "$REVIEWED_HEAD" ]]; then + echo "PR head moved from $REVIEWED_HEAD to $current_remote_head; refusing to overwrite it." + exit 78 + fi + if [[ "$current_base" != "$BASE_SHA" ]]; then + echo "Base moved from $BASE_SHA to $current_base; refusing to leave the PR stale." + exit 78 + fi + + git commit -m "fix(tools): complete shared phone footer ownership" + git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}" From 58821ca016b39933eaa9bf2a756672a71171923f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:33:56 +0800 Subject: [PATCH 08/10] chore(ci): correct PR 1993 ledger gate --- .github/workflows/codex-fix-pr-1993.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codex-fix-pr-1993.yml b/.github/workflows/codex-fix-pr-1993.yml index cef907b847..a6ebd2caf8 100644 --- a/.github/workflows/codex-fix-pr-1993.yml +++ b/.github/workflows/codex-fix-pr-1993.yml @@ -120,7 +120,7 @@ jobs: old_expectation = ( ' expect(dashboard).toContain(\'const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";\');' ) - new_expectation = ''' expect(dashboard).toMatch( + new_expectation = r''' expect(dashboard).toMatch( /const heroComposerBreakpoint =\s*showDesktopHomeComposer && activeModeResultKind !== "tools" \? "all" : "sm-up";/, ); expect(dashboard).not.toContain( @@ -199,8 +199,8 @@ jobs: set -euo pipefail npm run ledger:append -- \ --date 2026-08-16 \ - --scope "PR #1993 Tools phone footer ownership and current-base review" \ - --base "$BASE_SHA" \ + --ref "$GITHUB_REF_NAME" \ + --scope "PR #1993 Tools phone footer ownership against main $BASE_SHA" \ --head "$REVIEWED_HEAD" \ --checks "Prettier; ESLint on changed files; TypeScript; targeted Vitest; immutable ledger validation" \ --outcome "Confirmed and fixed dashboard Tools phone composer ownership, aligned stale responsive contracts, removed the temporary repair workflow, and kept the branch current with main." From cbfe0742c1b7bdb2520d8343e57ed2a44eb8c92f Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 16 Aug 2026 16:35:27 +0000 Subject: [PATCH 09/10] fix(tools): complete shared phone footer ownership --- .github/workflows/codex-fix-pr-1993.yml | 238 ------------------ ...b1e7c8c43197488836bb70a79c6d366b.record.md | 1 + src/components/ClinicalDashboard.tsx | 9 +- tests/mobile-composer-reserve.test.ts | 17 +- tests/ui-tools.spec.ts | 16 +- 5 files changed, 29 insertions(+), 252 deletions(-) delete mode 100644 .github/workflows/codex-fix-pr-1993.yml create mode 100644 docs/branch-review-records/f0bc08ea7f5c5c924273de2608597dddb1e7c8c43197488836bb70a79c6d366b.record.md diff --git a/.github/workflows/codex-fix-pr-1993.yml b/.github/workflows/codex-fix-pr-1993.yml deleted file mode 100644 index a6ebd2caf8..0000000000 --- a/.github/workflows/codex-fix-pr-1993.yml +++ /dev/null @@ -1,238 +0,0 @@ -name: Codex fix PR 1993 - -on: - push: - branches: - - codex/tools-universal-footer-search - -permissions: - contents: write - -concurrency: - group: codex-fix-pr-1993 - cancel-in-progress: false - -jobs: - fix: - runs-on: ubuntu-24.04 - timeout-minutes: 35 - steps: - - name: Checkout exact PR head - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - - name: Set up Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version-file: .nvmrc - cache: npm - cache-dependency-path: package-lock.json - - - name: Snapshot authoritative head and base - shell: bash - run: | - set -euo pipefail - git fetch --no-tags origin \ - "+refs/heads/${GITHUB_REF_NAME}:refs/remotes/origin/${GITHUB_REF_NAME}" \ - "+refs/heads/main:refs/remotes/origin/main" - remote_head="$(git rev-parse "refs/remotes/origin/${GITHUB_REF_NAME}")" - if [[ "$remote_head" != "$GITHUB_SHA" ]]; then - echo "PR head moved from $GITHUB_SHA to $remote_head; refusing to overwrite it." - exit 78 - fi - { - echo "REVIEWED_HEAD=$GITHUB_SHA" - echo "BASE_SHA=$(git rev-parse refs/remotes/origin/main)" - } >> "$GITHUB_ENV" - - - name: Merge latest base when needed - shell: bash - run: | - set -euo pipefail - git config user.name "Codex" - git config user.email "codex@users.noreply.github.com" - if ! git merge-base --is-ancestor "$BASE_SHA" HEAD; then - git merge --no-commit --no-ff "$BASE_SHA" - fi - - - name: Apply the verified Tools phone correction - shell: bash - run: | - set -euo pipefail - python - <<'PY' - from pathlib import Path - - dashboard = Path("src/components/ClinicalDashboard.tsx") - text = dashboard.read_text() - old_comment = "\n".join( - [ - ' // Any mounted mode home (answer, documents, prescribing, differentials, tools,', - ' // favourites) keeps the in-flow hero pill on phones ("all") per the', - ' // page-ownership contract. Only result/submitted views fall back to "sm-up"', - ' // so phones get the compact bottom dock.', - ] - ) - new_comment = "\n".join( - [ - " // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the", - " // deliberate exception: its content-rich directory uses the shared compact", - " // footer on phones while retaining the hero composer from sm upward.", - ] - ) - if text.count(old_comment) != 1: - raise SystemExit(f"expected one hero ownership comment, found {text.count(old_comment)}") - text = text.replace(old_comment, new_comment) - - old_line = ' const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";' - new_lines = ''' const heroComposerBreakpoint = - showDesktopHomeComposer && activeModeResultKind !== "tools" ? "all" : "sm-up";''' - if text.count(old_line) != 1: - raise SystemExit(f"expected one hero composer assignment, found {text.count(old_line)}") - dashboard.write_text(text.replace(old_line, new_lines)) - - contract = Path("tests/mobile-composer-reserve.test.ts") - text = contract.read_text() - old_intro = "\n".join( - [ - ' it("derives hero phone ownership from the mounted hero slot; any mode home uses all-widths breakpoint", () => {', - " // Any mounted mode home (answer, documents, prescribing, tools, favourites)", - ' // needs "all" (phones keep the in-flow hero pill) per the page-ownership', - ' // contract. Only result/submitted views use "sm-up" so phones get the compact', - " // bottom dock. desktopHomeComposerSlotId is undefined on result views, so", - " // heroOwnsPhoneComposer stays false there regardless of the breakpoint value.", - ] - ) - new_intro = "\n".join( - [ - ' it("derives hero phone ownership from the mounted hero slot while Tools delegates phones to the footer", () => {', - " // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the", - " // deliberate exception: its content-rich directory delegates phones to the", - ' // compact footer while retaining the hero from sm upward. Result/submitted', - ' // views also use "sm-up"; desktopHomeComposerSlotId is undefined there, so', - " // heroOwnsPhoneComposer stays false regardless of the breakpoint value.", - ] - ) - if text.count(old_intro) != 1: - raise SystemExit(f"expected one mobile ownership contract intro, found {text.count(old_intro)}") - text = text.replace(old_intro, new_intro) - - old_expectation = ( - ' expect(dashboard).toContain(\'const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";\');' - ) - new_expectation = r''' expect(dashboard).toMatch( - /const heroComposerBreakpoint =\s*showDesktopHomeComposer && activeModeResultKind !== "tools" \? "all" : "sm-up";/, - ); - expect(dashboard).not.toContain( - 'const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";', - );''' - if text.count(old_expectation) != 1: - raise SystemExit(f"expected one stale breakpoint assertion, found {text.count(old_expectation)}") - contract.write_text(text.replace(old_expectation, new_expectation)) - - spec = Path("tests/ui-tools.spec.ts") - text = spec.read_text() - old = ''' await expect(page.getByTestId("tools-search-results-page")).toBeVisible(); - await expect(page.getByRole("heading", { level: 1, name: "All tools" })).toBeVisible(); - await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); - await expectNoPageHorizontalOverflow(page);''' - new = ''' await expect(page.getByTestId("tools-search-results-page")).toBeVisible(); - await expect(page.getByRole("heading", { level: 1, name: "All tools" })).toBeVisible(); - if (viewport.name === "phone") { - await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toHaveCount(0); - await expect(page.locator("form.answer-footer-search-dock")).toBeVisible(); - } else { - await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); - await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); - } - await expectNoPageHorizontalOverflow(page);''' - if text.count(old) != 1: - raise SystemExit(f"expected one mode-picker assertion block, found {text.count(old)}") - text = text.replace(old, new) - - old = ''' await expect(page.getByRole("heading", { level: 2, name: "Clinical KB Search" }).first()).toBeVisible(); - await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); - await expectNoPageHorizontalOverflow(page);''' - new = ''' await expect(page.getByRole("heading", { level: 2, name: "Clinical KB Search" }).first()).toBeVisible(); - if (width < 640) { - await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toHaveCount(0); - await expect(page.locator("form.answer-footer-search-dock")).toBeVisible(); - } else { - await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); - await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); - } - await expectNoPageHorizontalOverflow(page);''' - if text.count(old) != 1: - raise SystemExit(f"expected one breakpoint assertion block, found {text.count(old)}") - spec.write_text(text.replace(old, new)) - PY - - - name: Install development dependencies - run: npm ci --include=dev --ignore-scripts - - - name: Format and run focused offline verification - shell: bash - run: | - set -euo pipefail - npx prettier --write \ - src/components/ClinicalDashboard.tsx \ - tests/mobile-composer-reserve.test.ts \ - tests/ui-tools.spec.ts - npx prettier --check \ - src/components/ClinicalDashboard.tsx \ - tests/mobile-composer-reserve.test.ts \ - tests/ui-tools.spec.ts - npx eslint \ - src/components/ClinicalDashboard.tsx \ - tests/mobile-composer-reserve.test.ts \ - tests/ui-tools.spec.ts \ - --max-warnings 0 - npm run typecheck - npm test -- \ - tests/search-shell-props.test.ts \ - tests/mobile-composer-reserve.test.ts \ - tests/phone-dock-addon-contract.test.ts - - - name: Append immutable review record - shell: bash - run: | - set -euo pipefail - npm run ledger:append -- \ - --date 2026-08-16 \ - --ref "$GITHUB_REF_NAME" \ - --scope "PR #1993 Tools phone footer ownership against main $BASE_SHA" \ - --head "$REVIEWED_HEAD" \ - --checks "Prettier; ESLint on changed files; TypeScript; targeted Vitest; immutable ledger validation" \ - --outcome "Confirmed and fixed dashboard Tools phone composer ownership, aligned stale responsive contracts, removed the temporary repair workflow, and kept the branch current with main." - npm run check:branch-review-ledger - - - name: Commit the verified fix and remove this temporary workflow - shell: bash - run: | - set -euo pipefail - git rm .github/workflows/codex-fix-pr-1993.yml - git add \ - src/components/ClinicalDashboard.tsx \ - tests/mobile-composer-reserve.test.ts \ - tests/ui-tools.spec.ts \ - docs/branch-review-records - git diff --check - git diff --cached --check - git status --short - - git fetch --no-tags origin \ - "+refs/heads/${GITHUB_REF_NAME}:refs/remotes/origin/${GITHUB_REF_NAME}" \ - "+refs/heads/main:refs/remotes/origin/main" - current_remote_head="$(git rev-parse "refs/remotes/origin/${GITHUB_REF_NAME}")" - current_base="$(git rev-parse refs/remotes/origin/main)" - if [[ "$current_remote_head" != "$REVIEWED_HEAD" ]]; then - echo "PR head moved from $REVIEWED_HEAD to $current_remote_head; refusing to overwrite it." - exit 78 - fi - if [[ "$current_base" != "$BASE_SHA" ]]; then - echo "Base moved from $BASE_SHA to $current_base; refusing to leave the PR stale." - exit 78 - fi - - git commit -m "fix(tools): complete shared phone footer ownership" - git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}" diff --git a/docs/branch-review-records/f0bc08ea7f5c5c924273de2608597dddb1e7c8c43197488836bb70a79c6d366b.record.md b/docs/branch-review-records/f0bc08ea7f5c5c924273de2608597dddb1e7c8c43197488836bb70a79c6d366b.record.md new file mode 100644 index 0000000000..688e3f4f9d --- /dev/null +++ b/docs/branch-review-records/f0bc08ea7f5c5c924273de2608597dddb1e7c8c43197488836bb70a79c6d366b.record.md @@ -0,0 +1 @@ +| 2026-08-16 | codex/tools-universal-footer-search | 58821ca016b39933eaa9bf2a756672a71171923f | PR #1993 Tools phone footer ownership against main 0b95d063b44712ce409d9fbfe5bf8e706b10ccaf | Confirmed and fixed dashboard Tools phone composer ownership, aligned stale responsive contracts, removed the temporary repair workflow, and kept the branch current with main. | Prettier; ESLint on changed files; TypeScript; targeted Vitest; immutable ledger validation | diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 223d050489..3d31ed7949 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3057,11 +3057,10 @@ export function ClinicalDashboard({ const desktopHomeComposerSlotId = showDesktopHomeComposer ? modeHomeDesktopComposerSlotId : undefined; const desktopResultComposerSlotId = !desktopHomeComposerSlotId && searchMode !== "answer" ? desktopPageComposerSlotId : undefined; - // Any mounted mode home (answer, documents, prescribing, differentials, tools, - // favourites) keeps the in-flow hero pill on phones ("all") per the - // page-ownership contract. Only result/submitted views fall back to "sm-up" - // so phones get the compact bottom dock. - const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up"; + // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the + // deliberate exception: its content-rich directory uses the shared compact + // footer on phones while retaining the hero composer from sm upward. + const heroComposerBreakpoint = showDesktopHomeComposer && activeModeResultKind !== "tools" ? "all" : "sm-up"; const heroOwnsPhoneComposer = Boolean(desktopHomeComposerSlotId) && heroComposerBreakpoint === "all"; const hasMobileBottomSearch = searchMode !== "answer" && !heroOwnsPhoneComposer; // Favourites and Tools are content-rich hubs: they share the centred hero but diff --git a/tests/mobile-composer-reserve.test.ts b/tests/mobile-composer-reserve.test.ts index 457d55ab42..deb59201cf 100644 --- a/tests/mobile-composer-reserve.test.ts +++ b/tests/mobile-composer-reserve.test.ts @@ -113,16 +113,19 @@ describe("mobile composer reserve contract", () => { ).toBe(mobileComposerIdleReserve); }); - it("derives hero phone ownership from the mounted hero slot; any mode home uses all-widths breakpoint", () => { - // Any mounted mode home (answer, documents, prescribing, tools, favourites) - // needs "all" (phones keep the in-flow hero pill) per the page-ownership - // contract. Only result/submitted views use "sm-up" so phones get the compact - // bottom dock. desktopHomeComposerSlotId is undefined on result views, so - // heroOwnsPhoneComposer stays false there regardless of the breakpoint value. + it("derives hero phone ownership from the mounted hero slot while Tools delegates phones to the footer", () => { + // Most mounted mode homes keep the in-flow hero pill on phones. Tools is the + // deliberate exception: its content-rich directory delegates phones to the + // compact footer while retaining the hero from sm upward. Result/submitted + // views also use "sm-up"; desktopHomeComposerSlotId is undefined there, so + // heroOwnsPhoneComposer stays false regardless of the breakpoint value. const dashboard = source("src/components/ClinicalDashboard.tsx"); const header = source("src/components/clinical-dashboard/master-search-header.tsx"); expect(dashboard).toContain('(activeModeResultKind === "favourites" && favouritesAccessible)'); - expect(dashboard).toContain('const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";'); + expect(dashboard).toMatch( + /const heroComposerBreakpoint =\s*showDesktopHomeComposer && activeModeResultKind !== "tools" \? "all" : "sm-up";/, + ); + expect(dashboard).not.toContain('const heroComposerBreakpoint = showDesktopHomeComposer ? "all" : "sm-up";'); expect(dashboard).toContain( 'const heroOwnsPhoneComposer = Boolean(desktopHomeComposerSlotId) && heroComposerBreakpoint === "all";', ); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 33d37d7ce6..86ce537561 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -444,7 +444,13 @@ test.describe("Clinical KB tools directory and legacy launcher", () => { await expect(page.getByTestId("tools-search-results-page")).toBeVisible(); await expect(page.getByRole("heading", { level: 1, name: "All tools" })).toBeVisible(); - await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); + if (viewport.name === "phone") { + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toHaveCount(0); + await expect(page.locator("form.answer-footer-search-dock")).toBeVisible(); + } else { + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); + await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); + } await expectNoPageHorizontalOverflow(page); }); } @@ -602,7 +608,13 @@ test.describe("Clinical KB tools directory and legacy launcher", () => { await expect(page.getByRole("heading", { level: 1, name: "All tools" })).toBeVisible(); await expect(page.getByRole("region", { name: "Tool results" })).toBeVisible(); await expect(page.getByRole("heading", { level: 2, name: "Clinical KB Search" }).first()).toBeVisible(); - await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); + if (width < 640) { + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toHaveCount(0); + await expect(page.locator("form.answer-footer-search-dock")).toBeVisible(); + } else { + await expect(page.getByTestId("tools-results-home-composer").getByTestId("global-search-input")).toBeVisible(); + await expect(page.locator("form.answer-footer-search-dock")).toHaveCount(0); + } await expectNoPageHorizontalOverflow(page); } From 7c13db36c1b442217f60ffb983f2b047345b7d07 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 17 Aug 2026 00:37:48 +0800 Subject: [PATCH 10/10] test(tools): cover nested phone footer routing --- tests/search-shell-props.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/search-shell-props.test.ts b/tests/search-shell-props.test.ts index 982d7f7605..b39b076779 100644 --- a/tests/search-shell-props.test.ts +++ b/tests/search-shell-props.test.ts @@ -29,11 +29,14 @@ describe("searchShellPropsForPathname", () => { }); it("keeps tools in the hero from tablet up and uses the shared phone footer", () => { - expect(searchShellPropsForPathname("/tools")).toEqual({ + const expectedToolsShell = { initialMode: "tools", desktopSearchPlacement: "hero", mobileHomeComposerPlacement: "footer", - }); + } as const; + + expect(searchShellPropsForPathname("/tools")).toEqual(expectedToolsShell); + expect(searchShellPropsForPathname("/tools/interaction-checker")).toEqual(expectedToolsShell); }); it("maps therapy and home fallbacks", () => {