From 2d7ea51904e41882855fcea4b20795ef47a24e1f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 09:20:43 +0000 Subject: [PATCH 01/11] fix(sidebar): default the desktop sidebar to collapsed New users previously got the labelled (expanded) sidebar with collapse as a remembered choice. Flip the default to collapsed, matching the existing already-closed mobile drawer default, so the sidebar starts closed on all devices until a user explicitly expands it. --- .../use-sidebar-collapsed.ts | 10 +++++----- tests/ui-smoke.spec.ts | 19 ++++++++++--------- tests/ui-tools-collapse.spec.ts | 7 ++++--- tests/ui-tools.spec.ts | 4 ++-- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/components/clinical-dashboard/use-sidebar-collapsed.ts b/src/components/clinical-dashboard/use-sidebar-collapsed.ts index ff419375b4..ac9c7a6ba2 100644 --- a/src/components/clinical-dashboard/use-sidebar-collapsed.ts +++ b/src/components/clinical-dashboard/use-sidebar-collapsed.ts @@ -18,11 +18,11 @@ function getSnapshot() { } try { const storedValue = window.localStorage.getItem(storageKey); - // New users get the labelled (expanded) sidebar: eight icon-only - // destinations demand recall/hover; collapsing stays a remembered choice. - return storedValue === null ? false : storedValue === "1"; + // New users get the collapsed sidebar by default; expanding stays a + // remembered choice. + return storedValue === null ? true : storedValue === "1"; } catch { - return false; + return true; } } @@ -35,7 +35,7 @@ function subscribe(onChange: () => void) { }; } -const useSidebarCollapsedStore = createBrowserStore(subscribe, getSnapshot, false); +const useSidebarCollapsedStore = createBrowserStore(subscribe, getSnapshot, true); /** * Desktop sidebar collapse state shared across shells and persisted per diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 2814675b0b..76ee0e731a 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1164,24 +1164,24 @@ test.describe("Clinical KB UI smoke coverage", () => { await expectNoPageHorizontalOverflow(page); }); - test("desktop sidebar defaults to the labelled state for new users", async ({ page }) => { + test("desktop sidebar defaults to the collapsed state for new users", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockDemoApi(page); await gotoApp(page, "/?mode=answer"); await waitForDemoDashboardReady(page); - // No stored preference (PT-10): the labelled navigation remains the default, - // so first-run desktop shows the labelled sidebar; collapse is remembered. + // No stored preference (PT-10): the collapsed icon rail is the default, + // so first-run desktop shows the collapsed sidebar; expanding is remembered. await expect(page.locator("#clinical-tools-sidebar")).toBeVisible(); - await expect(page.getByRole("button", { name: "Collapse sidebar" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Expand sidebar" })).toHaveCount(0); + await expect(page.getByRole("button", { name: "Expand sidebar" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Collapse sidebar" })).toHaveCount(0); }); test("desktop sidebar mode sync and accessibility affordances stay coherent", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockDemoApi(page); - // This journey exercises the remembered-collapsed rail; new users now - // default to the labelled sidebar, so seed the stored preference. + // This journey starts from the collapsed rail (now the default for new + // users too) and exercises expanding/collapsing it. await page.addInitScript(() => window.localStorage.setItem("clinical-kb-sidebar-collapsed", "1")); await gotoApp(page, "/?mode=tools"); @@ -1231,8 +1231,9 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page.getByRole("button", { name: "Open Clinical Guide menu" })).toHaveCount(0); await expect(page.getByRole("button", { name: "Expand sidebar" })).toHaveCount(0); - // With the labelled default the expanded panel exists in the DOM but stays - // display:none below lg; tablet must still only present the icon rail. + // If the expanded panel exists in the DOM (e.g. a remembered expanded + // preference), it stays display:none below lg; tablet must still only + // present the icon rail. await expect(page.locator("#clinical-tools-sidebar")).toBeHidden(); await expect(page.getByLabel("Clinical Guide collapsed sidebar")).toBeVisible(); diff --git a/tests/ui-tools-collapse.spec.ts b/tests/ui-tools-collapse.spec.ts index 6a1aae73b4..eac1f0056d 100644 --- a/tests/ui-tools-collapse.spec.ts +++ b/tests/ui-tools-collapse.spec.ts @@ -9,9 +9,10 @@ async function goto(page: Page, path: string) { await expect(page.locator("#main-content").first()).toBeVisible({ timeout: 15_000 }); } -// The shell's expanded sidebar (now the desktop default) contributes its own -// "Search recent chats" searchbox, so mockup searches must be scoped to the -// page content instead of grabbing the first searchbox on the page. +// The shell's sidebar (expanded when a remembered preference restores it) +// contributes its own "Search recent chats" searchbox, so mockup searches +// must be scoped to the page content instead of grabbing the first +// searchbox on the page. function mockupSearch(page: Page) { return page.locator("#main-content").getByRole("searchbox").first(); } diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 87077e42c9..51971650bb 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -528,8 +528,8 @@ test.describe("Clinical KB tools launcher", () => { test("mode toggle stays global on the services home route", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); - // Asserts the collapsed rail affordance below; seed the remembered - // preference now that new users default to the labelled sidebar. + // Asserts the collapsed rail affordance below; explicit for clarity even + // though collapsed is now the default for new users too. await page.addInitScript(() => window.localStorage.setItem("clinical-kb-sidebar-collapsed", "1")); await gotoLauncher(page, "/?mode=answer"); From f101a40cb2996e4af418b716f29614c19992b7fc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 09:40:59 +0000 Subject: [PATCH 02/11] fix(test): assert the collapsed rail, not the absent expanded panel #clinical-tools-sidebar only mounts when the sidebar is expanded, so after flipping the default to collapsed the "defaults to collapsed" test must assert the collapsed rail is visible and the expanded panel is absent, not that the expanded panel is visible. --- tests/ui-smoke.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 76ee0e731a..21b1c4d440 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1171,8 +1171,11 @@ test.describe("Clinical KB UI smoke coverage", () => { await waitForDemoDashboardReady(page); // No stored preference (PT-10): the collapsed icon rail is the default, - // so first-run desktop shows the collapsed sidebar; expanding is remembered. - await expect(page.locator("#clinical-tools-sidebar")).toBeVisible(); + // so first-run desktop shows the collapsed rail, not the labelled panel; + // expanding is remembered. #clinical-tools-sidebar only mounts when + // expanded, so its absence (not just hidden) is the collapsed signal. + await expect(page.getByLabel("Clinical Guide collapsed sidebar")).toBeVisible(); + await expect(page.locator("#clinical-tools-sidebar")).toHaveCount(0); await expect(page.getByRole("button", { name: "Expand sidebar" })).toBeVisible(); await expect(page.getByRole("button", { name: "Collapse sidebar" })).toHaveCount(0); }); From a0eec3237d863d1388080e6678e0f493294ad11e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 15:28:14 +0000 Subject: [PATCH 03/11] fix(test): seed sidebar preference in journeys vacated by default flip Tablet smoke must seed expanded so #clinical-tools-sidebar mounts and toBeHidden() covers the display:none-below-lg path. Forced-colors a11y must target the solid --command New chat in the expanded panel, not the collapsed rail icon. Refresh the stale "default to labelled" comment. Co-authored-by: BigSimmo --- tests/ui-accessibility.spec.ts | 6 +++++- tests/ui-smoke.spec.ts | 12 +++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts index 677d19e9b5..38479713d8 100644 --- a/tests/ui-accessibility.spec.ts +++ b/tests/ui-accessibility.spec.ts @@ -374,10 +374,14 @@ test.describe("Clinical KB accessibility coverage", () => { await page.emulateMedia({ forcedColors: "active" }); await page.setViewportSize({ width: 1440, height: 1000 }); await mockMinimalDashboardApi(page); + // Target the expanded sidebar's solid --command "New chat" button. With the + // collapsed-by-default rail, getByRole("New chat").first() would hit the + // icon-rail control (text-muted), not the solid label this regression guards. + await page.addInitScript(() => window.localStorage.setItem("clinical-kb-sidebar-collapsed", "0")); await gotoApp(page); await expectDashboardUsable(page); - const newChat = page.getByRole("button", { name: "New chat" }).first(); + const newChat = page.locator("#clinical-tools-sidebar").getByRole("button", { name: "New chat" }); await expect(newChat).toBeVisible(); const { canvas, buttonLabelColor, tokenColors } = await newChat.evaluate((button) => { const probe = document.createElement("span"); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 10e65f9b43..80e104c3e1 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1229,14 +1229,16 @@ test.describe("Clinical KB UI smoke coverage", () => { test("tablet shows icon rail without drawer trigger or expand control @critical", async ({ page }) => { await page.setViewportSize({ width: 768, height: 1024 }); await mockDemoApi(page); + // Seed expanded preference so #clinical-tools-sidebar mounts. Without this + // seed the panel is absent (count 0) and toBeHidden() would pass vacuously; + // we need the remembered-expanded path where the panel exists but stays + // display:none below lg while tablet still only presents the icon rail. + await page.addInitScript(() => window.localStorage.setItem("clinical-kb-sidebar-collapsed", "0")); await gotoApp(page, "/?mode=answer"); await waitForDemoDashboardReady(page); await expect(page.getByRole("button", { name: "Open Clinical Guide menu" })).toHaveCount(0); await expect(page.getByRole("button", { name: "Expand sidebar" })).toHaveCount(0); - // If the expanded panel exists in the DOM (e.g. a remembered expanded - // preference), it stays display:none below lg; tablet must still only - // present the icon rail. await expect(page.locator("#clinical-tools-sidebar")).toBeHidden(); await expect(page.getByLabel("Clinical Guide collapsed sidebar")).toBeVisible(); @@ -1339,8 +1341,8 @@ test.describe("Clinical KB UI smoke coverage", () => { }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockDemoApi(page); - // Exercises both collapsed and expanded account affordances; seed the - // remembered-collapsed preference now that new users default to labelled. + // Exercises both collapsed and expanded account affordances; seed collapsed + // explicitly (also the new-user default) so the journey starts on the rail. await page.addInitScript(() => window.localStorage.setItem("clinical-kb-sidebar-collapsed", "1")); await gotoApp(page, "/"); await waitForDemoDashboardReady(page); From 7fd7e989cfd3415661b87ceeb73fe44637758366 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 15:33:01 +0000 Subject: [PATCH 04/11] docs(sidebar): clarify null preference collapses for returning browsers The storage key is only written after an explicit toggle, so absent means collapsed for new users and never-toggled returning browsers alike. Document that so the rollout note stays honest about who is unaffected. Co-authored-by: BigSimmo --- src/components/clinical-dashboard/use-sidebar-collapsed.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/clinical-dashboard/use-sidebar-collapsed.ts b/src/components/clinical-dashboard/use-sidebar-collapsed.ts index ac9c7a6ba2..2a9125f314 100644 --- a/src/components/clinical-dashboard/use-sidebar-collapsed.ts +++ b/src/components/clinical-dashboard/use-sidebar-collapsed.ts @@ -18,8 +18,11 @@ function getSnapshot() { } try { const storedValue = window.localStorage.getItem(storageKey); - // New users get the collapsed sidebar by default; expanding stays a - // remembered choice. + // Collapsed is the default whenever the key is absent. The key is only + // written after an explicit toggle, so browsers that never touched the + // control (returning users included) also land on collapsed — matching + // the already-closed mobile drawer. Explicit "0"/"1" preferences still + // win; expanding remains a remembered choice. return storedValue === null ? true : storedValue === "1"; } catch { return true; From ea93d0bffe5de1ecc195a609c9626e18207695fb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 15:49:15 +0000 Subject: [PATCH 05/11] chore(ci): retrigger PR checks after Actions infra flake Previous Production UI / Semgrep ingestion gate failures were Set up job Service Unavailable / Bad Gateway while resolving action download info, not product assertions. Empty commit to get a clean required check run. Co-authored-by: BigSimmo From 493b4f3f18ca059aac877a6ea3dc2a52dea7a790 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 15:53:38 +0000 Subject: [PATCH 06/11] fix(sidebar): harden default-collapsed coverage after review (#1637) Scope reduced-motion New chat clicks to the collapsed rail, seed the hydration mismatch path with an expanded preference, and pin the readSidebarCollapsedPreference default with a focused unit test. Co-authored-by: BigSimmo --- .../use-sidebar-collapsed.ts | 22 ++++++++++--- tests/sidebar-collapsed.test.ts | 32 +++++++++++++++++++ tests/ui-accessibility.spec.ts | 10 ++++-- tests/ui-hydration.spec.ts | 5 ++- 4 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 tests/sidebar-collapsed.test.ts diff --git a/src/components/clinical-dashboard/use-sidebar-collapsed.ts b/src/components/clinical-dashboard/use-sidebar-collapsed.ts index 2a9125f314..bcc5834089 100644 --- a/src/components/clinical-dashboard/use-sidebar-collapsed.ts +++ b/src/components/clinical-dashboard/use-sidebar-collapsed.ts @@ -3,9 +3,23 @@ import { useCallback } from "react"; import { createBrowserStore } from "@/lib/client-store-factory"; -const storageKey = "clinical-kb-sidebar-collapsed"; +/** localStorage key for an explicit expanded/collapsed pin. */ +export const SIDEBAR_COLLAPSED_STORAGE_KEY = "clinical-kb-sidebar-collapsed"; + const changeEvent = "clinical-kb-sidebar-collapsed-change"; +/** + * Maps a raw stored value to the collapsed preference. Absent key (`null` / + * `undefined`) resolves to collapsed so first-run and never-toggled browsers + * match the closed-by-default product choice. When a value is present, + * collapsed is true only for the explicit `"1"` pin (same ternary the store + * used before this helper was extracted). Storage errors are handled by + * callers (they pass null / use their own catch path). + */ +export function readSidebarCollapsedPreference(storedValue: string | null | undefined): boolean { + return storedValue === null || storedValue === undefined ? true : storedValue === "1"; +} + // In-memory fallback when localStorage writes fail (e.g. private browsing mode). // Null means no fallback needed; storage is the source of truth. let inMemoryFallback: boolean | null = null; @@ -17,13 +31,13 @@ function getSnapshot() { return inMemoryFallback; } try { - const storedValue = window.localStorage.getItem(storageKey); + const storedValue = window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY); // Collapsed is the default whenever the key is absent. The key is only // written after an explicit toggle, so browsers that never touched the // control (returning users included) also land on collapsed — matching // the already-closed mobile drawer. Explicit "0"/"1" preferences still // win; expanding remains a remembered choice. - return storedValue === null ? true : storedValue === "1"; + return readSidebarCollapsedPreference(storedValue); } catch { return true; } @@ -49,7 +63,7 @@ export function useSidebarCollapsed() { const collapsed = useSidebarCollapsedStore(); const setCollapsed = useCallback((next: boolean) => { try { - window.localStorage.setItem(storageKey, next ? "1" : "0"); + window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, next ? "1" : "0"); // Storage write succeeded; clear the in-memory fallback so persisted // storage remains the source of truth. inMemoryFallback = null; diff --git a/tests/sidebar-collapsed.test.ts b/tests/sidebar-collapsed.test.ts new file mode 100644 index 0000000000..d144d4994a --- /dev/null +++ b/tests/sidebar-collapsed.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "vitest"; +import { + readSidebarCollapsedPreference, + SIDEBAR_COLLAPSED_STORAGE_KEY, +} from "../src/components/clinical-dashboard/use-sidebar-collapsed"; + +describe("readSidebarCollapsedPreference", () => { + it("defaults to collapsed when the preference key is absent", () => { + expect(readSidebarCollapsedPreference(null)).toBe(true); + expect(readSidebarCollapsedPreference(undefined)).toBe(true); + }); + + it("honours an explicit expanded pin", () => { + expect(readSidebarCollapsedPreference("0")).toBe(false); + }); + + it("honours an explicit collapsed pin", () => { + expect(readSidebarCollapsedPreference("1")).toBe(true); + }); + + it('only treats an explicit "1" as collapsed when a value is present', () => { + // Preserves the pre-existing ternary: absent → collapsed; otherwise + // collapsed iff the stored pin is exactly "1". Stale/garbage pins expand. + expect(readSidebarCollapsedPreference("")).toBe(false); + expect(readSidebarCollapsedPreference("true")).toBe(false); + expect(readSidebarCollapsedPreference("collapsed")).toBe(false); + }); + + it("pins the storage key used by Playwright seeds and the browser store", () => { + expect(SIDEBAR_COLLAPSED_STORAGE_KEY).toBe("clinical-kb-sidebar-collapsed"); + }); +}); diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts index 38479713d8..d915ff8891 100644 --- a/tests/ui-accessibility.spec.ts +++ b/tests/ui-accessibility.spec.ts @@ -233,12 +233,18 @@ test.describe("Clinical KB accessibility coverage", () => { await page.setViewportSize({ width: 1280, height: 800 }); await mockMinimalDashboardApi(page); + // New chat lives on both the collapsed rail and the expanded panel. With + // collapsed-by-default the expanded panel is unmounted, so scope to the + // rail rather than relying on .first() (same hazard the forced-colors + // journey below guards against). + const railNewChat = page.getByLabel("Clinical Guide collapsed sidebar").getByRole("button", { name: "New chat" }); + // Reduced motion → every scripted scroll must be an instant "auto" jump. await page.emulateMedia({ reducedMotion: "reduce" }); await gotoApp(page); await expectDashboardUsable(page); await resetBehaviours(); - await page.getByRole("button", { name: "New chat" }).first().click(); + await railNewChat.click(); await expect.poll(readBehaviours).not.toHaveLength(0); const reduced = await readBehaviours(); expect(reduced, "reduced motion must not animate scripted scrolls").not.toContain("smooth"); @@ -247,7 +253,7 @@ test.describe("Clinical KB accessibility coverage", () => { // No preference → the same action animates smoothly. await page.emulateMedia({ reducedMotion: "no-preference" }); await resetBehaviours(); - await page.getByRole("button", { name: "New chat" }).first().click(); + await railNewChat.click(); await expect.poll(readBehaviours).not.toHaveLength(0); expect(await readBehaviours(), "no-preference should animate scripted scrolls").toContain("smooth"); }); diff --git a/tests/ui-hydration.spec.ts b/tests/ui-hydration.spec.ts index c9c97b1754..66485701cc 100644 --- a/tests/ui-hydration.spec.ts +++ b/tests/ui-hydration.spec.ts @@ -4,9 +4,12 @@ test.describe("React Hydration Safety", () => { const scenarios = [ { name: "dashboard defaults", route: "/", storage: {} }, { + // Seed expanded ("0") so the client preference differs from the + // collapsed SSR/server snapshot after the closed-by-default flip — + // that is the mismatch/re-render path useSyncExternalStore must handle. name: "dashboard persisted theme and sidebar", route: "/", - storage: { "clinical-kb-theme": "dark", "clinical-kb-sidebar-collapsed": "1" }, + storage: { "clinical-kb-theme": "dark", "clinical-kb-sidebar-collapsed": "0" }, themeCookie: "dark", }, { From 8fd65e805a08c722411f78c471b724a4ae42a198 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 16:31:27 +0000 Subject: [PATCH 07/11] chore(ci): retrigger checks after stuck Actions run (#1637) GitHub Actions cancel returned 502 while Change scope stayed cancelled and PR required queued indefinitely; push a tip bump so concurrency replaces it. Co-authored-by: BigSimmo From d481c87ae0d29f492e77ae3c63f515e4fe502b26 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 19:34:21 +0000 Subject: [PATCH 08/11] chore(ci): retrigger required checks after Actions outage (#1637) Co-authored-by: BigSimmo From fa617c06b00485388384b3ab8d51f6dc67a3b1d1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 19:51:43 +0000 Subject: [PATCH 09/11] chore(ci): retrigger required checks (Actions gradual recovery) (#1637) Co-authored-by: BigSimmo From 74f58f5c9346c9de822a9daa5ede9f370c2d5472 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 6 Aug 2026 20:18:29 +0000 Subject: [PATCH 10/11] ci: retrigger after Actions queue timeout Prior CI run cancelled/timed out while queued during the GitHub Actions major outage. Empty commit to re-fire checks on current tip. Co-authored-by: BigSimmo From 6b7df2a7faf4db04ebaeb752690932a595a72396 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 7 Aug 2026 01:28:58 +0000 Subject: [PATCH 11/11] fix(ui-smoke): expand sidebar before desktop settings scroll test Sidebar now defaults to collapsed, so the Settings control inside #clinical-tools-sidebar is not reachable until Expand is clicked. Co-authored-by: BigSimmo --- tests/ui-smoke.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 4ff3ba4372..7e1cc43544 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1385,6 +1385,11 @@ test.describe("Clinical KB UI smoke coverage", () => { await gotoApp(page, "/"); await waitForDemoDashboardReady(page); + // Sidebar defaults to collapsed for new users; expand so the in-rail Settings + // control this journey asserts is reachable (same as the account-setup case). + await page.getByRole("button", { name: "Expand sidebar" }).click(); + await expect(page.locator("#clinical-tools-sidebar")).toBeVisible(); + const settings = accountSettingsDialog(page); await page.locator("#clinical-tools-sidebar").getByRole("button", { name: "Settings", exact: true }).click(); await expect(settings).toBeVisible();