Prep Notes
Shows every persisted "Strategy Sync Notes" PrepNote across every flow,
grouped by status (notes needing follow-up first), with a "cycle status"
action per note and an "assign to" control for handing a note off as a task
to a teammate.
- Route:
/prep-notes - Nav: the Tools page's Prep & Practice group; the Reason Editor's
Workspace menu (
t prep notesin Ctrl/Cmd-Shift-Space's command palette) - Package:
debate-round
⚠️ Known regression, discovered 2026-09-05: PR #498 ("Remove flow spreadsheet grid, show round flows in round editor", merged 2026-09-03) deleted the AG Grid-based
FlowSpreadsheetview along withflow/PrepNoteBadge.tsxandflow/PrepNotePopover.tsx. This standalone/prep-notespanel is unaffected — it never depended on the grid — but the two references below to aFlowSpreadsheet-gridPrepNoteBadge(a per-box note badge in the live flow grid, distinct from this cross-flow list view) describe deleted code with no current replacement. Seeflow-annotations.md's matching notice for the same regression.
What it shows
Notes are grouped into three sections, in this order:
| Group | Meaning |
|---|---|
| Needs follow-up | The argument this note is about still needs work |
| Open | The note hasn't been marked covered or flagged for follow-up |
| Covered | The argument this note is about has been addressed |
Each note shows its text, author, current assignee (if any), a "Mark <next status>" button, a "Flag high priority"/"Unflag" toggle, and an "Assign to" input + button (plus an "Unassign" button once a note is assigned).
Priority flag
Closes the "🔄 Strategy Sync Notes" bullet's "a priority flag" follow-up: a
note can be flagged high priority from PrepNotesPanel, independent of its
status. A flagged note shows a "High priority" badge and sorts ahead of its
status-mates within its group; the "Flag high priority"/"Unflag" button
toggles it back and forth.
flow/strategy-sync-notes.ts
PrepNotePriority = "normal" | "high" — omitted from a PrepNote entirely
when "normal" (mirrors assignedToId's
omit-when-unset convention)
setNotePriority(note, priority, now) — pure transform, clears the field
on "normal" rather than storing it
getHighPriorityNotes(notes) — flagged notes, oldest first
sortNotesByPriorityThenCreatedAt(notes) — high-priority tier first, each
tier oldest first
state/prepNotes.ts
updatePersistedPrepNotePriority(id, priority, now)
→ applies setNotePriority to the persisted note and saves it
buildPrepNotesPanelView()
→ now sorts each status group with sortNotesByPriorityThenCreatedAt
instead of plain createdAt order
panels/PrepNotesPanel.tsx
"Flag high priority" / "Unflag" button per note
→ updatePersistedPrepNotePriority(id, next, Date.now())
→ panel re-reads buildPrepNotesPanelView() to refreshbuildPrepNoteSummaryText's counts line also reports how many notes are
currently flagged (e.g. "4 notes: 1 open, 1 covered, 2 need follow-up, 1 high priority").
Data flow
state/prepNotes.ts (localStorage: prepNotes)
→ buildPrepNotesPanelView() — groups every persisted PrepNote
by status, needs-follow-up
first, each group oldest first
→ panels/PrepNotesPanel.tsx — renders it, grouped by status
→ apps/debate-ai.com/app/prep-notes/page.tsx — mounts the panel as a route
Cycling a note's status:
panels/PrepNotesPanel.tsx
→ nextPrepNoteStatus(status) — state/prepNotes.ts
(open → covered → needs-follow-up → open)
→ updatePersistedPrepNoteStatus(id, next, now) — state/prepNotes.ts
(applies flow/strategy-sync-notes.ts's updateNoteStatus and saves it)
→ panel re-reads buildPrepNotesPanelView() to refresh
Assigning/unassigning a note:
panels/PrepNotesPanel.tsx
→ assignPersistedPrepNote(id, assignedToId | null, now) — state/prepNotes.ts
(applies flow/strategy-sync-notes.ts's assignNote and saves it)
(on a real assignment, also calls recordPrepNoteAssignedNotification —
state/prepNoteNotifications.ts — to notify the new assignee)
→ panel re-reads buildPrepNotesPanelView() to refresh
Notifying an assignee (closes follow-up (b), see "Notifications" below):
state/prepNotes.ts's assignPersistedPrepNote(id, assignedToId, now)
→ recordPrepNoteAssignedNotification(id, note, assignedToId, now) — state/prepNoteNotifications.ts
(builds flow/prep-note-notifications.ts's PrepNoteNotification via
createPrepNoteAssignedNotification and saves it — skipped on an
unassignment, i.e. assignedToId === null)Every note create/status-update/assign rule already existed and was
Vitest-covered; this feature closes follow-up (a), "a prep-notes panel UI,"
named under the "🔄 Strategy Sync Notes" bullet in TODO.md, adding two
small helpers to state/prepNotes.ts — buildPrepNotesPanelView (groups
the persisted notes by status for the panel) and nextPrepNoteStatus (the
panel's status-cycle order) — rather than introducing new mutation logic.
Vitest-covered in packages/debate-round/test/prepNotes.test.ts.
Cross-tab live update
PrepNotesPanel now also live-updates across browser tabs: a note
created, cycled to a new status, or (re)assigned in another tab refreshes
this panel's grouped list here too, closing this panel's share of the
"every other localStorage-backed panel in this repo still has no cross-tab
live-update mechanism" Known gap tracked in
shared-flow-sync.md. It subscribes to the
browser's storage event — fired only in other same-origin tabs, never
the one that made the write — and, via flow/live-update.ts's
isPrepNotesPanelLiveUpdateStorageEvent, re-reads buildPrepNotesPanelView()
whenever the backing prepNotes key changes (or localStorage.clear()
fires a null-key event). Distinct from FLOW_LIVE_UPDATE_STORAGE_KEYS in
the same module, which drives the FlowSpreadsheet grid's per-box
PrepNoteBadge instead of this standalone cross-flow list view. Vitest-covered
in packages/debate-round/test/live-update.test.ts (the backing-store key,
the null-key clear-all case, and unrelated/substring-matching keys
staying ignored).
Notifications
Closes follow-up (b), "an assignee notification once a notification system
exists." This is the first notification system in this repo, scoped
narrowly to the one event that follow-up named — a PrepNote being
assigned to a teammate — rather than a general-purpose system for events
that don't exist yet.
- Route:
/notifications - Nav: the Tools page's Prep & Practice group; the Reason Editor's
Workspace menu (
t notificationsin Ctrl/Cmd-Shift-Space's command palette) - Model:
flow/prep-note-notifications.ts'sPrepNoteNotification(recipient id, the note's text/author at assignment time, read status). There's no auth/identity system in this repo, so a recipient is a free-form id — the sameassignedToIdaPrepNoteis assigned to. - Persistence:
state/prepNoteNotifications.ts(localStorage:prepNoteNotifications), wired directly intoassignPersistedPrepNoteso a real assignment always records one — no caller has to remember to. - UI:
panels/PrepNoteNotificationsPanel.tsxasks for a recipient id (remembered in localStorage across visits) and renders that recipient's notifications grouped into a digest per day (see "Digest grouping" below), each expandable to its individual "Mark read" actions.
Digest grouping
Closes the "🔄 Strategy Sync Notes" bullet's "a digest notification instead
of one per assignment" follow-up. Rather than showing every assignment as
its own permanent row — unwieldy once a teammate has been assigned several
notes — PrepNoteNotificationsPanel groups a recipient's notifications
into one digest card per UTC calendar day, most recent day first.
- Grouping:
flow/prep-note-notifications.ts#groupNotificationsIntoDigestsis pure: it buckets a recipient's notifications by UTC day and returns aNotificationDigestGroupper day (dayKey, that day's notifications newest first, and a precomputedunreadCount).state/prepNoteNotifications.ts#buildNotificationDigestViewis the read-side wrapper the panel calls, composing that grouping with the existinggetNotificationsForRecipient. - Heading:
buildDigestGroupHeadingrenders each card's title, e.g."3 notifications on 2026-09-04 (2 unread)". - Bulk mark-read: each digest card has a "Mark all read" action (hidden
once nothing in it is unread) backed by
markManyPersistedNotificationsRead, a single localStorage write instead of one per notification. - Expand: an "Expand (N)"/"Collapse" toggle reveals the day's
individual notifications underneath the card, each still carrying its own
"Mark read" action — the per-notification
markPersistedNotificationReadpath is unchanged, so a teammate can still clear one assignment at a time within a day instead of only all-or-nothing. - The unread badge next to the recipient lookup now sums every digest
group's
unreadCountinstead of counting a flat list.
Vitest-covered: groupNotificationsIntoDigests (no notifications, grouping
same-day entries together with the most recent day first, and scoping to
one recipient) and buildDigestGroupHeading (plural/singular phrasing) in
test/prep-note-notifications.test.ts; buildNotificationDigestView and
markManyPersistedNotificationsRead (a bulk write, an empty id list as a
no-op, and unknown ids being ignored) in test/prepNoteNotifications.test.ts.
Cross-tab live update
PrepNoteNotificationsPanel now also live-updates across browser tabs: a
new assignment or a "Mark read" made in another tab refreshes the currently
looked-up recipient's notification list here too, closing this panel's
share of the "every other localStorage-backed panel in this repo still has
no cross-tab live-update mechanism" Known gap tracked in
shared-flow-sync.md. It subscribes to the
browser's storage event — fired only in other same-origin tabs, never
the one that made the write — and, via flow/live-update.ts's
isPrepNoteNotificationsLiveUpdateStorageEvent, re-reads the panel's
notification list whenever the backing prepNoteNotifications key changes
(or localStorage.clear() fires a null-key event). The listener
deliberately ignores state/prepNoteNotifications.ts's separate
prepNoteNotifications:lastRecipientId key — which recipient id another
tab last looked up isn't this tab's business. The effect depends on
recipientId so a lookup for a different recipient re-registers the
listener with a fresh closure instead of refreshing against a stale one.
Vitest-covered in packages/debate-round/test/live-update.test.ts (every
backing-store key, the null-key clear-all case, the excluded
recipient-id key, and unrelated/substring-matching keys staying ignored).
Threaded replies
Closes the "🔄 Strategy Sync Notes" bullet's "threaded replies on a note instead of flat status" follow-up: each note has a "Replies (N)" toggle that opens a comment thread underneath it — a teammate can discuss the note itself without changing its status, and without every discussion having to be crammed into the note's own text.
- Store:
state/prepNoteReplies.ts(localStorage:prepNoteReplies), local-first, mirroringdebate-research-evidence'sstate/dailyBestCardComments.tsconvention — aPrepNoteReplyis keyed by its own generated id and carries thenoteIdit's attached to, anauthorId(trimmed, falling back to"Anonymous"when blank),text(trimmed and capped atMAX_PREP_NOTE_REPLY_TEXT_LENGTH), and apostedAttimestamp.listRepliesForNote(noteId)returns a note's thread oldest first. - Like
dailyBestCardComments.ts, this store syncs to the signed-in user's account as theprepNoteRepliescollection, the same generic syncprepNotesitself rides on (see Known gaps below and Tool Data Sync). - UI:
panels/PrepNotesPanel.tsx'sPrepNoteReplyThreadrenders the thread (author + text, each with a "Delete" action) and an inline "Your name" / "Reply" form, mirroringDailyBestCardPanel'sCommentThreadcomponent. The "Replies (N)" toggle sits alongside the note's other actions; the thread itself is collapsed until toggled open.PrepNotesPanelalso takes an optionalsignedInContributorIdprop (mirroringDailyBestCardPanel's identical convention) that prefills a reply draft's "Your name" field —apps/debate-ai.com/components/research/PrepNotesWithIdentity.tsxresolves it from the real signed-in session viaderiveContributorIdFromSessionIdentity. A starting value only; typing over it is always respected. - Cascade delete:
state/prepNotes.ts'sdeletePrepNotenow also callsdeleteRepliesForNote(id), so a deleted note doesn't leave its thread behind as orphaned replies no UI can reach. - Cross-tab live update: the
prepNoteReplieskey was added toflow/live-update.ts'sPREP_NOTES_PANEL_LIVE_UPDATE_STORAGE_KEYS, so a reply posted or deleted in another browser tab refreshes this panel here too, the same way a status/priority/assignment change already did.
Vitest-covered in packages/debate-round/test/prepNoteReplies.test.ts
(posting, trimming, the "Anonymous" fallback, the text-length cap,
per-note listing/counting, and deletion) and
packages/debate-round/test/prepNotes.test.ts's deletePrepNote suite
(the cascade delete).
Jump to argument
Each note in PrepNotesPanel has a Jump to argument link that takes
you to /debate and scrolls the note's flow into view, flashing its cell.
flow/strategy-sync-notes.ts
buildPrepNoteJumpHref(note) — /debate?flowId=<id>&boxPath=<path>
parsePrepNoteJumpParams(searchParams) — the inverse, tolerant of a
missing/malformed flowId or
boxPath (returns null rather
than throwing)
hooks/useJumpToPrepNoteBox.ts (mounted by DebateFlowPage)
reads the URL's flowId/boxPath via parsePrepNoteJumpParams
→ selects the matching flow tab by id (flows.findIndex, not the
store's array-index `selected`)
→ once that flow's AG Grid has the target row rendered — either
immediately (grid already mounted) or once its onGridReady fires
(fresh mount) — calls edit-cells.ts's jumpToBoxInGrid(api, boxPath)
to scroll to and flash the box's cell, retrying every 200ms until it
succeeds or edit-cells.ts's MAX_BOX_JUMP_ATTEMPTS (5) is reachedThe panel itself still doesn't mount a live Flow (it stays cross-flow, so
resolvePrepNoteBox isn't used here); the link instead hands off to
/debate, which already owns a live flow and its AG Grid. This closes the
"No 'jump to argument' link" gap below.
If the row never resolves — e.g. the note's boxPath no longer matches a
real row because the flow was edited or the row removed since the note was
made — useJumpToPrepNoteBox gives up after MAX_BOX_JUMP_ATTEMPTS retries
and returns jumpFailed: true. DebateFlowPage renders
edit-cells.ts's buildBoxJumpFailedMessage() as a dismissible banner
above the flow grid when that happens, closing the "silently returns
false... no error is shown" Known gap below.
Create a note
Closes the "no note-creation UI" gap below: a PrepNote can now be created
directly against the flow box it's about, from the live /debate grid
itself, rather than needing a not-yet-built flow-view affordance.
-
Where: every
FlowSpreadsheetcell gets a small note badge (PrepNoteBadge, next to the existing annotation/edit badges) — a filled pill with the note count when the box already has one or more notes, or a faint always-present affordance when it doesn't (mirrorsEditBadge's "always visible" pattern, since a box with zero notes is exactly when a contributor most wants to add one). -
Flow:
flow/PrepNoteBadge.tsx — the cell badge; click opens the popover → FlowSpreadsheet.tsx's handleOpenPrepNote(params) sets popover state (x, y, boxPath) → flow/PrepNotePopover.tsx — lists the box's existing notes (if any) and a small "author id" + "text" form submit → strategy-sync-notes.ts's createPrepNote(...) → state/prepNotes.ts's savePrepNote(note) → onCreated() bumps FlowSpreadsheet's refresh token and force- refreshes the cell (mirrors handleEditLogged for EditBadge), so the badge's count updates immediately -
Data:
state/prepNotes.ts's newlistPrepNotesForBox(flowId, boxPath)(wrappingstrategy-sync-notes.ts'sgetNotesForBox) feeds both the badge's count and the popover's existing-notes list. -
A note created this way immediately shows up in the cross-flow
PrepNotesPanelabove (same persistedPrepNotestore) and can be cycled/assigned from either place.
Round-anchored notes
Closes idea #6's ("Speech Transcript Summaries and Answers") "a one-click
'send to Prep Notes' action for a summary" follow-up in TODO.md: a note can
now attach to a round as a whole instead of one specific flow argument, for
a source with no flowId/boxPath of its own (the Speech Transcript
Summaries panel's FlowSummaryRecord, keyed only by roundId).
PrepNote is now a discriminated union of the original BoxAnchoredPrepNote
(flowId/boxPath) and a new RoundAnchoredPrepNote (roundId instead) —
isBoxAnchoredPrepNote(note)/isRoundAnchoredPrepNote(note) type-guard
between them, createRoundPrepNote(...)/getNotesForRound(notes, roundId)
are the round-anchored counterparts to createPrepNote/getNotesForBox, and
state/prepNotes.ts's addRoundPrepNote({ roundId, authorId, text })/
listPrepNotesForRound(roundId) are the persisted-store wrappers.
getNotesForBox/getNotesForFlow now only ever match box-anchored notes,
resolvePrepNoteBox returns null for a round-anchored note (no box to
resolve), and buildPrepNoteJumpHref's signature is narrowed to
BoxAnchoredPrepNote — a caller must check isBoxAnchoredPrepNote(note)
first. PrepNotesPanel renders a "Round <roundId>" badge in place of the
"Jump to argument" link for a round-anchored note.
See docs/features/flow-summaries.md's "Sending a summary to Prep Notes"
section for the sending side (FlowSummariesPanel's "Send to Prep Notes"
action and its apps/debate-ai.com app-layer wiring).
Known gaps
- Notes sync to a signed-in user's account as the
prepNotescollection (see Tool Data Sync), so they follow that user across devices — but they are still per-user, not a shared team resource two people write to at once. If a note'sClosed:boxPathno longer resolves to a real grid row (e.g. the flow was edited since the note was made),jumpToBoxInGridsilently returnsfalse— the flow tab still gets selected, but nothing scrolls or flashes, and no error is shown.useJumpToPrepNoteBoxnow retries the jump every 200ms up toMAX_BOX_JUMP_ATTEMPTS(5) times before giving up, andDebateFlowPageshows a dismissiblebuildBoxJumpFailedMessage()banner above the flow grid once it does (see "Jump to argument" above).The new note-creation popover's "Author ID" is a free-form typed field, not an authenticated identity — there's no auth system in this repo, so anyone can type any author id (same gap asClosed:review-queue.md's reviewer id).PrepNotesPanelandFlowSummariesPanel(the "Send to Prep Notes" form) both take an optionalsignedInContributorIdprop — the same conventionPrepRoomPanel/DailyBestCardPanelalready use — andPrepNotesWithIdentity.tsx/FlowSummariesPanelWithPrepNotes.tsxprefill it from the real signed-in session viaderiveContributorIdFromSessionIdentity. Still a prefill, not a login: the field stays free-form and typing over it is always respected.- Notifications are in-app only (no email/push/browser notification) and cover prep-note assignment only — no other event in this repo creates one yet.
