Skip to content

feat(CodeLookup): memory picklist — frequently-used codes with device-trust storage - #351

Open
horner wants to merge 14 commits into
mainfrom
code-memory
Open

feat(CodeLookup): memory picklist — frequently-used codes with device-trust storage#351
horner wants to merge 14 commits into
mainfrom
code-memory

Conversation

@horner

@horner horner commented Aug 1, 2026

Copy link
Copy Markdown
Member

CodeLookup memory — frequently-used codes, safe on shared machines

Adds a "Frequently used" picklist to CodeLookup: focus the empty search box and
the codes you actually pick are right there, ranked by how often you use them.
Memory is off unless the deployment turns it on, and it stays off on any
machine that isn't trusted.

Focus on the field shows a dropdown:

focus shows list of memories

Searching

search shows memory first

What's in it

  • Memory store (memoryStore.ts) — usage counts per { userId, context }
    bucket, so a prescriber's med list and a rooming nurse's are separate.

  • Picklist — empty-query focus shows the top N; while typing, remembered
    codes rank ahead of index hits.

  • Server is the source of truth — the local store is a cache. Counts
    revalidate on a TTL, the server's number is authoritative (it can move a count
    down), and unflushed local picks are added on top so the list never goes
    backwards while you work. Picks flush debounced with credentials: 'include'.

  • Two gates, both fail closed — a real signed-in userId (anonymous,
    guest, kiosk, empty… never remember) and a storage mode:

    storage Where picks live For
    'session' (default) RAM only, gone when the tab closes shared clinical workstation
    'local' IndexedDB a workstation under a user profile
    'none' nowhere belt and braces

    memory={false} on the provider is the one-line kiosk switch and beats any
    per-instance opt-in.

  • YAML import/export — buckets round-trip as YAML for seeding, backup and
    moving a clinician's list between systems.

Storybook changes

Storybook toolbar globals — "Signed in as" (not signed in / two doctors /
nurse / reception / patient) and "Device" (public kiosk vs trusted
workstation) drive the gates live, so the behaviour is demonstrable rather
than described.

Screenshot 2026-08-01 at 7 24 12 AM Screenshot 2026-08-01 at 7 24 35 AM

PHI note

Nothing is written to disk unless the deployment declares the machine trusted
and a real user is signed in. The server must take identity from the session —
the ?user= in the sync URL is a hint for the cache key, never an authorization
(documented in the README).

Verified

  • 51 unit tests (store, YAML, engine); typecheck and lint clean.
  • Live in Storybook: not-signed-in records nothing; a kiosk remembers in-tab but
    writes no IndexedDB row and forgets on reload; a trusted workstation survives
    reload; each identity sees its own bucket.

Not in this PR

Forwarding memory={{ context }} from the Healthcare wrappers (MedicationList,
OrderEditor, ProblemList, AllergyList, …) — the plumbing is ready, the
wiring is a follow-up.

horner added 12 commits August 1, 2026 07:32
…mory

Memory now needs two gates open: a real signed-in userId (placeholders like
'anonymous' no longer pool every kiosk visitor into one bucket) and a storage
mode declared by the app. Storage defaults to 'session' (RAM, dies with the
tab), so a public kiosk that forgets to configure leaks nothing; 'local' opts
into the IndexedDB cache and asserts a login-secured machine.

The server becomes the source of truth: syncFromServer revalidates on a TTL
instead of seeding once, and its counts are authoritative on top of unflushed
local picks, so a count can move down without losing a pick.
'Signed in as' and 'Device' drive the memory picklist's two gates through the
ambient CodeLookup provider, so every Healthcare story demonstrates them and
the decorator doubles as the reference wiring. Both default to the safe end
(not signed in, public kiosk), leaving existing story baselines unchanged.
dynamicTitle defaults to true, so every global widened the bar with its
selected label; the labels still show in each dropdown and the button's
aria-label keeps carrying the current value.
Emoji move into `title` so the button is one glyph wide while still reporting
its setting, and the wording moves to the dropdown's `right` column. Theme,
density and device use per-item icons instead (sun/moon, grow/collapse,
unlock/lock), so the lock icon alone says whether picks are cached.
Each role gets its own memory bucket, so the picklist can be shown filling
differently for a prescriber, a rooming nurse and a patient portal.
Copilot AI review requested due to automatic review settings August 1, 2026 11:33
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploying ui with  Cloudflare Pages  Cloudflare Pages

Latest commit: b1d4b9c
Status: ✅  Deploy successful!
Preview URL: https://923b1782.ui-6d0.pages.dev
Branch Preview URL: https://code-memory.ui-6d0.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in “Frequently used” memory picklist to CodeLookup, backed by a device-trust-aware local cache (session/IndexedDB) with optional server revalidation + debounced delta flush, plus YAML import/export tooling and Storybook globals to demo the gating behavior.

Changes:

  • Introduces a new memory storage backend + store (usage counts per {userId, context}) with optional server sync and flush.
  • Wires memory picklist behavior into CodeLookup (empty-query focus shows remembered codes; remembered matches rank ahead while typing).
  • Adds YAML import/export utilities + tests and updates Storybook/README to document and demonstrate the feature.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/components/CodeLookup/README.md Documents the new memory prop, gating rules, server protocol, and YAML import/export.
src/components/CodeLookup/memoryYaml.ts Implements YAML export/import (lazy js-yaml loading) for memory buckets.
src/components/CodeLookup/memoryYaml.test.ts Adds unit tests for YAML round-tripping, merging, and validation behavior.
src/components/CodeLookup/memoryStore.ts Implements the memory store: record/getTop/sync/flush/match/clear APIs.
src/components/CodeLookup/memoryStore.test.ts Adds unit tests covering isolation, sync authority, flushing, gates, and modes.
src/components/CodeLookup/memoryBackend.ts Adds storage backends (IndexedDB/session/none) and dispatch logic.
src/components/CodeLookup/index.ts Re-exports memory APIs/types and YAML APIs from the CodeLookup barrel.
src/components/CodeLookup/engine.ts Adds viaMemory marker on result rows.
src/components/CodeLookup/context.tsx Adds provider-level memory defaults and a provider-wide disable switch (memory={false}).
src/components/CodeLookup/CodeLookup.tsx Integrates memory picklist UI/behavior and recording/flush wiring.
src/components/CodeLookup/CodeLookup.stories.tsx Adds a Storybook demo with YAML import/export controls.
.storybook/preview.tsx Adds Storybook toolbar globals for “Signed in as” and “Device” and wires provider memory defaults.
eslint.config.js Adds IDB globals needed by the new backend code/tests.
pnpm-lock.yaml Updates lockfile entries (unrelated to CodeLookup functionality).
code-memory-plan.md Adds an implementation plan/notes document for the feature.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread src/components/CodeLookup/memoryYaml.ts Outdated
Comment thread src/components/CodeLookup/memoryYaml.ts
Comment thread src/components/CodeLookup/memoryStore.ts
Comment thread src/components/CodeLookup/CodeLookup.tsx
- export ties break on lastUsed like the picklist, not label
- a missing js-yaml now says so, and the cached import resets so a later
  install works without a reload
- clearMemory drops the scope's revalidation TTL, otherwise a cleared bucket
  stayed empty until it expired
- safelist text-primary-600 for Tailwind 3 consumers
Copilot AI review requested due to automatic review settings August 1, 2026 11:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (2)

src/components/CodeLookup/memoryStore.ts:249

  • syncFromServer builds the GET URL by string-concatenating ?user=…&context=…. If serverUrl already contains a query string (or a trailing ?), this will produce an invalid URL (double ?) and the sync will fail.

Build the URL with new URL() + searchParams so existing query params are preserved and encoding is handled correctly.

    const url = `${serverUrl}?user=${encodeURIComponent(scope.userId)}&context=${encodeURIComponent(scope.context)}`;
    const res = await fetch(url, { credentials: 'include' });

src/components/CodeLookup/CodeLookup.tsx:304

  • setMemoryStorage(memStorage) runs unconditionally (and memStorage currently defaults to 'session'). Because the storage mode in memoryBackend is a module-level singleton, rendering a CodeLookup without a provider (or without memory enabled) can still force global storage back to 'session', disabling persistence for other CodeLookup instances that expect 'local'.

While touching this block, also prefer ?? for memCfg.limit so a caller-provided 0 isn’t silently replaced by the default.

    // Declared first so the store knows the device's trust level before the
    // effect below reads or writes anything.
    React.useEffect(() => {
      setMemoryStorage(memStorage);
    }, [memStorage]);

The picklist needed two things wired per instance (a memory config and a
context), so most lookups silently had no memory. Now a provider that names
a signed-in memory.userId turns it on everywhere below it, and the bucket
defaults to the box's own domains so a med picker and a problem picker still
never share a list.

memory={false} remains the opt-out, on the provider (kiosk) or one box.
Copilot AI review requested due to automatic review settings August 1, 2026 12:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (1)

src/components/CodeLookup/memoryStore.ts:153

  • recordUse() does a read-modify-write (memGet → memSet). If recordUse is called concurrently for the same (userId, context, fullid) key, both calls can read the same previous value and one increment can be lost. Serializing updates per entry key avoids undercounting and keeps ordering stable under rapid selections.
    const key = entryKey(scope, result.fullid);
    const prev = await memGet(key);
    const entry: MemoryEntry = {
      userId: scope.userId,
      context: scope.context,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants