Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c61e5c1
fix(therapy): restore the shared search command surface on the Therap…
claude Aug 18, 2026
6ed8572
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
0605b0e
docs(issues): capture the cross-mode home audit findings as inbox req…
claude Aug 18, 2026
c9291f3
Merge remote-tracking branch 'origin/claude/therapy-lightweight-home-…
claude Aug 18, 2026
4e6f8b0
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
6acc6ac
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
749afeb
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
8428884
docs(issues): capture the mode-home composer reserve CLS gap
claude Aug 18, 2026
1e10b4a
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
a3fba3a
Merge commit '1e10b4a8dfa6490230bfd3ab0a9b9e32ddb27df8' into claude/t…
claude Aug 18, 2026
8a7dc0c
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
5d07542
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
bbbc9b5
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
1f6ff42
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
49c5c2e
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 18, 2026
ad8c175
Merge branch 'main' into claude/therapy-lightweight-home-consistency-…
BigSimmo Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "61d609ea-0f7c-4934-ab71-b9a972f5a7be",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P2",
"type": "issue",
"summary": "/tools on a phone is the only mode home with no visible patient-identifiable-information warning",
"detail": "Tools is the sole route setting mobileHomeComposerPlacement: 'footer' (src/lib/search-shell-props.ts). showsComposerPrivacyNotice in master-search-header.tsx:1813 is 'usesPhoneSearchLayout ? isDesktopHomeComposer : true', so the phone footer dock suppresses both the 'Do not enter patient-identifiable information.' line and the Privacy and data processing link. The composer placement matches the documented exception in docs/search-chrome-behaviour.md row 2, but the docs do not record that the exception costs the governance copy. Needs an owner decision for a clinical product. Found during the PR #2160 cross-mode audit.",
"source": "PR #2160 cross-mode home audit",
"issueUlid": "01M0BE9G1GD8JBCV161HS2GQQB"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "6eb8b382-a129-4470-91ec-383bc580c25f",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "rec",
"summary": "Mode home copy drift: three placeholder-punctuation conventions, inconsistent heading levels, and a stale docs/site-map.md mode index",
"detail": "Placeholders use ASCII '...' (answer, documents, services, forms, favourites, dsm, specifiers, formulation, prescribing, tools, calculators), Unicode '…' (therapy-compass, factsheets, dictionary), and no terminator at all on differentials ('Ask or search a presentation'). Heading level is h2 on answer/documents/prescribing and h1 elsewhere, so the Documents home has an h2 and no h1 anywhere on the page. docs/site-map.md's mode page index covers 11 modes — Therapy, Factsheets, Dictionary and Calculators have no row — and CLAUDE.md still says '13 app modes' when app-modes.ts defines 15. Pick one convention per axis and refresh the generated docs. Found during the PR #2160 cross-mode audit.",
"source": "PR #2160 cross-mode home audit",
"issueUlid": "01M0BEA22B97VQK5QN24JSPFCM"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "72ec8611-0294-4e33-aa36-92668ed25cd6",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P2",
"type": "issue",
"summary": "Three standalone mode homes have no loading.tsx (/medications, /calculators, /dictionary) and the contract test cannot catch it",
"detail": "All three are in standaloneModeHomePaths (src/lib/search-route-ownership.ts) and chrome invariant 18 requires ModeHomeRouteLoading, but tests/mode-home-loading-contract.test.ts enumerates only ten routes and omits these three, so the gap is invisible to CI. /documents is legitimately exempt (dashboard-owned). Fix: add the three loading.tsx files and derive the contract test's route list from standaloneModeHomePaths instead of a hand-written list. Verified by direct filesystem check during the PR #2160 audit.",
"source": "PR #2160 cross-mode home audit",
"issueUlid": "01M0BE9FVW6K9YGQY9G8CYM421"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "b10cb24d-17fa-40ca-b8bb-90eee806ea0a",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "issue",
"summary": "Tools and Favourites bespoke home composer slots skip the SSR height reservation chrome invariant 15 requires",
"detail": "ModeHomeTemplate renders its composer slot with data-composer-reserve='pending' plus min-h tokens (mode-home-template.tsx:316-320) so the hero does not shift when the portal attaches. The two bespoke homes hand-roll the slot without either: favourites-command-library-page.tsx:1426 and tools-search-results-page.tsx:353, plus favourites-hub.tsx:187. Those three get no SSR height reservation, which is the CLS that invariant 15 exists to prevent. Found during the PR #2160 cross-mode audit.",
"source": "PR #2160 cross-mode home audit",
"issueUlid": "01M0BEA1WHYJ3R7Y86ZGZ9FEV4"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "c09532b5-549c-47e0-a4b7-5977435d1d0f",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P2",
"type": "issue",
"summary": "Mode homes: two competing title systems disagree for 8 of 13 modes (sharedHomePresentation vs hard-coded standalone titles)",
"detail": "src/lib/ui-copy.ts sharedHomePresentation drives the shared home /, while each standalone *-home-page.tsx hard-codes its own title. Its doc comment claims each entry mirrors the standalone home 'so a clinician sees the same words whichever door they came through' — untrue today: Documents/Clinical Documents, Services/Clinical Services, Forms/Clinical Forms, Differentials/Differential Diagnosis, Specifiers/Diagnostic Specifiers, Formulation/Clinical Formulation, Medication/Medication Guidance, Therapy/Therapy Compass. Either derive one list from the other or correct the comment. Found during the PR #2160 cross-mode audit.",
"source": "PR #2160 cross-mode home audit",
"issueUlid": "01M0BE929PTWKWE4Q142YGCRTF"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "c196b2fd-fde1-482b-9cac-355cc2b4ee04",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "issue",
"summary": "Three mode homes override the canonical APP_MODE_ICON glyph (services, forms, dictionary)",
"detail": "services uses Users (canonical route), forms uses FileText (canonical fileSignature — and identical to the Documents home tile), dictionary uses BookOpen (canonical bookMarked). Same class as the therapy-compass magnifier fixed in PR #2160, which now derives from appModeIcons. Each remaining one needs its own visual-baseline re-adoption, so they were left out of that PR. Found during the PR #2160 cross-mode audit.",
"source": "PR #2160 cross-mode home audit",
"issueUlid": "01M0BE9FPF0EKBGCMBD20EHNDF"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "db8e7ade-2f04-4255-b812-29900e801e47",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "issue",
"summary": "/favourites and /?mode=favourites render visibly different homes for the same mode",
"detail": "The standalone hero lockup was deliberately deleted from favourites-command-library-page.tsx (ledger #164), but the dashboard variant FavouritesHub (src/components/clinical-dashboard/favourites-hub.tsx:179) still renders ModeHomeHero with 'Favourites / Saved notes, sources, and sets.' So the same mode looks different depending on the door. Decide which treatment is canonical and apply it to both. Found during the PR #2160 cross-mode audit.",
"source": "PR #2160 cross-mode home audit",
"issueUlid": "01M0BEA1Q5V0EDR4MJSWW1F5GD"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "ef811fd9-df86-4a1f-bd5e-cd86233d251e",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P3",
"type": "rec",
"summary": "Mode home suggestion data is duplicated across three unrelated sources",
"detail": "searchCommandSurfaceByMode examples/suggestions (src/lib/search-command-surface.ts) drive the Try this ticket, rotating hint and prompt chips; per-page pills arrays (e.g. therapy-compass/screens/home-screen.tsx:14, services-home-page.tsx) drive the mode-home pill row; src/lib/tools-catalog.ts:348 is a third. Only the first drives the ticket, so after PR #2160 the Therapy home advertises two different suggestion sets — its five pills and the three ticket examples. Reconcile to one source per mode. Found during the PR #2160 cross-mode audit.",
"source": "PR #2160 cross-mode home audit",
"issueUlid": "01M0BEA27H90Y0FDZXZY53CRKF"
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
{
"version": 2,
"id": "fa9f768a-9eee-4a67-96b5-ded98afbecd9",
"createdOn": "2026-08-18",
"action": "add",
"payload": {
"pri": "P2",
"type": "issue",
"summary": "Mode-home composer reserve does not account for the suggestion ticket, so every ticket-bearing home carries a ~0.035 CLS shift",
"detail": "ModeHomeTemplate reserves the composer slot with --spacing-mode-home-composer-phone (6.625rem) / --spacing-mode-home-composer-wide (5.5rem), but the portal content is UniversalSearchCommandSurface, which renders SmartRotatingHint (phone ticket) and the sm+ rotating line/prompt-chip row ABOVE the composer inside that same slot. The reserve therefore under-accounts, and the portal attaching post-hydration shifts content — the defect class chrome invariant 15 exists to prevent. Evidence from the PR #2160 Lighthouse run: mobile-dsm baseline CLS 0.0353, mobile-forms 0.088, mobile-root 0.016, while mobile-therapy-compass was 0.000 purely because Therapy had no command-surface entry and so rendered no ticket. Restoring the ticket moved Therapy to 0.032, matching its peers. Fix: raise the reserve tokens to include the hint row height (or reserve it separately), which should take every mode home toward ~0. Touches all 15 mode homes, so it needs verify:phone-chrome plus Lighthouse and visual baseline re-adoption — deliberately not bundled into PR #2160.",
"source": "PR #2160 Lighthouse budget failure",
"issueUlid": "01M0BF075ZJVYQEMAJJ5Y2R1WF"
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -869,11 +869,13 @@ export function SearchResultsEmptyState({
const filtered = appliedFilters.length > 0;
const Title = headingLevel ? (`h${headingLevel}` as "h2" | "h3" | "h4" | "h5" | "h6") : "p";
// What the panel can actually offer, decided before the copy describes it.
// `searchCommandSurfaceByMode` is a `Partial<Record<…>>` and therapy-compass
// has no entry, so there is neither an example nor a cross-mode route there —
// yet the body said "Try an example, or jump to another mode", naming two
// controls the reader could not see. Copy that promises absent affordances is
// the same defect class as a label that does not match its handler.
// Both halves need a handler as well as data: most hosts render this panel
// without `onTryExample`/`onCrossMode` (only Services, Calculators and Forms
// wire the first), so a mode with a full `searchCommandSurfaceByMode` entry can
// still have no example and no cross-mode route to offer — yet the body said
// "Try an example, or jump to another mode", naming two controls the reader
// could not see. Copy that promises absent affordances is the same defect class
// as a label that does not match its handler.
const hasExample = Boolean(config?.examples[0] && onTryExample);
const hasCrossMode = crossModes.length > 0 && Boolean(onCrossMode);
// Degraded outranks filtered, which outranks a plain miss. A search whose
Expand Down
9 changes: 7 additions & 2 deletions src/components/therapy-compass/screens/home-screen.tsx
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
"use client";

import { GitCompareArrows, Network, Search, Sparkles, Waypoints } from "lucide-react";
import { GitCompareArrows, Network, Sparkles, Waypoints } from "lucide-react";
import { useRouter } from "next/navigation";

import { ModeHomeMain, ModeHomeTemplate } from "@/components/mode-home-template";
import { appModeIcons } from "@/lib/app-mode-icons";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
import { therapyHrefWithSearchParams, therapyScreenHref } from "@/lib/therapy-compass-navigation";

Expand DownExpand Up@@ -38,7 +39,11 @@ export function HomeScreen() {
testId="therapy-compass"
title="Therapy"
subtitle={therapyCountCopy}
icon={Search}
// The mode's identity glyph is derived from APP_MODE_ICON rather than
// chosen here, so this medallion cannot drift from the one nav, the mode
// picker and the shared home `/` all render. It was a hard-coded magnifier,
// which made the same mode show two different identities by door.
icon={appModeIcons["therapy-compass"]}
actionsLabel="Therapy workflows"
desktopComposerSlotId={modeHomeDesktopComposerSlotId}
actions={[
Expand Down
18 changes: 18 additions & 0 deletions src/lib/search-command-surface.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -178,6 +178,24 @@ const searchCommandSurfaceByMode: Partial<Record<AppModeId, SearchCommandSurface
// src/lib/app-modes.ts — so the command panel must not query the remote index.
remoteSearchEnabled: false,
},
"therapy-compass": {
// Every example was run through the real scorer (`scoreTherapyCandidate`,
// src/lib/therapy-ranking.ts) against the generated 205-record catalogue, so
// the ticket never advertises a query the catalogue cannot answer — the same
// rule the dictionary entry above states. Measured match counts at the time
// of writing: trauma-focused CBT 188, behavioural activation 30, insomnia 4.
examples: ["trauma-focused CBT", "behavioural activation", "insomnia"],
suggestions: [
{ text: "trauma-focused CBT", meta: "Trauma" },
{ text: "behavioural activation", meta: "Mood" },
{ text: "insomnia", meta: "CBT" },
],
crossModes: ["documents", "dsm", "answer"],
// Therapy reads the local generated catalogue under
// public/therapy-compass-data — like Dictionary and Calculators, its command
// panel must not query the remote index.
remoteSearchEnabled: false,
},
};

export function searchCommandSurfaceConfig(modeId: AppModeId): SearchCommandSurfaceConfig | null {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/search-shell-props.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,7 +80,7 @@ export function searchShellPropsForPathname(pathname: string): SearchShellPathPr
}

if (pathname.startsWith("/therapy-compass")) {
return { initialMode: "therapy-compass" };
return { initialMode: "therapy-compass", desktopSearchPlacement: "hero" };
}

if (pathname.startsWith("/factsheets")) {
Expand Down
23 changes: 23 additions & 0 deletions tests/search-command-surface.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";

import { describe, expect, it } from "vitest";

import { appModeIds } from "@/lib/app-modes";
import {
commandDropdownCanDisplay,
commandDropdownMinimumWidthMediaQuery,
Expand DownExpand Up@@ -47,6 +48,28 @@ describe("search command surface", () => {
expect(specifiers?.crossModes).toContain("formulation");
});

it("covers every app mode, so no mode silently loses the command surface", () => {
// `UniversalSearchCommandSurface` early-returns on a null config, which takes
// the phone "Try this" ticker, the sm+ rotating hint, the prompt-chip row and
// the whole command dropdown with it. therapy-compass shipped in exactly that
// state — the single uncovered mode — so pin the whole set rather than the
// one mode that happened to be missing.
for (const modeId of appModeIds) {
const config = searchCommandSurfaceConfig(modeId);
expect(config, `${modeId} has no search command surface config`).not.toBeNull();
expect(config?.examples.length, `${modeId} has no examples`).toBeGreaterThan(0);
expect(config?.suggestions.length, `${modeId} has no suggestions`).toBeGreaterThan(0);
}
});

it("keeps therapy suggestions local to the generated catalogue", () => {
const therapy = searchCommandSurfaceConfig("therapy-compass");
expect(therapy?.examples).toContain("trauma-focused CBT");
expect(therapy?.crossModes).toContain("documents");
// Therapy reads public/therapy-compass-data, not the remote index.
expect(therapy?.remoteSearchEnabled).toBe(false);
});

it("keeps calculator suggestions local while preserving remote typeahead elsewhere", () => {
expect(commandSurfaceRemoteSearchEnabled("calculators")).toBe(false);
expect(commandSurfaceRemoteSearchEnabled("documents")).toBe(true);
Expand Down
10 changes: 6 additions & 4 deletions tests/search-results-header-band.dom.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -1431,10 +1431,12 @@ describe("SearchResultsEmptyState", () => {
// owns that role on every search route, and a second one made singular
// `getByRole("status")` queries across the suite ambiguous.
expect(screen.getByText("No matches for “unmatched therapy”")).toBeVisible();
// `searchCommandSurfaceByMode` is a `Partial<Record<…>>` with no
// therapy-compass entry, so this mode has neither an example nor a
// cross-mode route. The body must not tell the reader to "try an example, or
// jump to another mode" when the panel renders no control for either.
// No `onTryExample` or `onCrossMode` handler is passed here — matching the
// real Therapy call site (therapy-compass/screens/search-screen.tsx) — so
// this panel has neither an example nor a cross-mode route to offer, even
// though the mode now carries a full `searchCommandSurfaceByMode` entry. The
// body must not tell the reader to "try an example, or jump to another mode"
// when the panel renders no control for either.
expect(screen.getByText("Check the spelling, or try a broader term.")).toBeVisible();
expect(screen.queryByRole("button", { name: /^Try:/ })).toBeNull();
expect(screen.queryByRole("button", { name: /^Search in / })).toBeNull();
Expand Down
3 changes: 3 additions & 0 deletions tests/search-shell-props.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,8 +40,11 @@ describe("searchShellPropsForPathname", () => {
});

it("maps therapy and home fallbacks", () => {
// Therapy was the one standalone mode home not declaring the hero placement,
// which left it on a different composer code path from its twelve peers.
expect(searchShellPropsForPathname("/therapy-compass/search")).toEqual({
initialMode: "therapy-compass",
desktopSearchPlacement: "hero",
});
expect(searchShellPropsForPathname("/")).toEqual({ initialMode: "answer" });
});
Expand Down
Loading