Annotation overlay: flag entities, note them, copy for an agent (web)#12
Annotation overlay: flag entities, note them, copy for an agent (web)#12ragnorc wants to merge 3 commits into
Conversation
… agent
agentation's core "click → note → copy" flow, graph-native and web-only. An
"Annotate" mode makes every rendered graph entity clickable; a note popup
(anchored to the entity) captures a free-text note + optional intent; annotated
entities show a marker; a floating panel lists them with an overall-instruction
field and a "Copy for agent" button that writes structured markdown to the
clipboard. Persisted per-notebook in localStorage.
Annotations are ephemeral *feedback*, never written to the graph — so this is
host-shell-only: NO core/runtime/client/server changes, no schema, no mutations.
Where agentation reverse-engineers DOM identity, the notebook already has typed
identity (slug/id column + the row's data), so a tiny React-context hook makes
each lens annotation-aware without prop-threading.
- annotations-store.ts: model + localStorage persistence (mirrors
layout-overrides.ts) + serializeAnnotations markdown (+ tests)
- annotation-context.tsx: global + per-cell context, useAnnotation() leaf hook,
useAnnotations() host hook (session, mode, popup target, copy)
- components/Annotation{Popup,Panel,Marker}.tsx: the overlay UI
- App.tsx: annotate toggle, providers, popup/panel mount, per-cell provider
- lenses Table/ActionList/Subgraph/Card: annotate-on-click + marker (v1)
Verify: web typecheck + build green; 41 web tests pass (incl. serialize +
persistence round-trip). TUI skipped; Path/Timeline/Quote/Text are v1.1.
| const setInstruction = useCallback( | ||
| (instruction: string) => persist({ ...session, instruction }), | ||
| [session, persist], | ||
| ); |
There was a problem hiding this comment.
Stale session overwrites annotations
Medium Severity
save, remove, and setInstruction rebuild the session from the session value captured in useCallback instead of the latest state. Back-to-back saves, a save plus panel remove, or editing the overall instruction right after saving can persist an older items list and drop annotations that were just written.
Reviewed by Cursor Bugbot for commit 4010583. Configure here.
| <h3 className="mb-3 flex items-center gap-1.5 text-base font-semibold text-foreground"> | ||
| {annotated && <AnnotationMarker />} | ||
| {title} | ||
| </h3> |
There was a problem hiding this comment.
Card marker hidden without title
Low Severity
When a Card has no title_column value, the annotated marker is only rendered inside the title h3, which is omitted entirely. Annotated cards without a title still track in the session but show no in-lens marker unlike Table or ActionList.
Reviewed by Cursor Bugbot for commit 4010583. Configure here.
| typeof o.cellId === "string" && | ||
| typeof o.key === "string" && | ||
| typeof o.note === "string" | ||
| ); |
There was a problem hiding this comment.
Copy crashes on malformed items
Low Severity
isAnnotation does not require data, yet serializeAnnotations always calls contextLine(a.data). Items loaded from damaged or hand-edited localStorage can pass validation with a missing data field and throw when the user clicks Copy for agent.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 4010583. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4010583155
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const v = select_column | ||
| ? String(row[select_column] ?? "") | ||
| : firstCol | ||
| ? String(row[firstCol] ?? "") | ||
| : ""; | ||
| return v || `#${idx}`; |
There was a problem hiding this comment.
Use unique keys for table row annotations
When a Table does not set select_column, this falls back to the first displayed column value as the annotation key. For tables whose first column is not unique, such as a status/urgency column, multiple rows get the same ${cellId}::${key} annotation id, so annotating one row overwrites another and all matching rows are treated as the same annotation. Prefer a stable unique row field like id/slug, or include the row index only as a last-resort fallback.
Useful? React with 👍 / 👎.
…rlay Re-skin the popup, markers, and panel to agentation's own dark floating chrome (its values, lifted from its SCSS modules) rather than the notebook's light UI kit — the overlay reads identically over a light or dark notebook: - numbered circular markers (22px blue #3b82f6 pins, white 1-based order) instead of plain dots — adds numberOf() to the annotation context - popup: #1a1a1a, 16px radius, soft drop + 1px inner ring shadow, dark textarea, system font, blue accent; centered under the entity (translateX(-50%)) - panel: agentation dark chrome (#1a1a1a, layered shadow), numbered list items, blue count badge, dark control buttons Pure restyle — no behavior change. typecheck + build green; 41 web tests pass.
| : firstCol | ||
| ? String(row[firstCol] ?? "") | ||
| : ""; | ||
| return v || `#${idx}`; |
There was a problem hiding this comment.
Non-unique keys overwrite notes
Medium Severity
When a table defines select_column, annotation ids use that column’s cell value. Values like status or priority can repeat across rows, so two rows share one id and saving a note on one row replaces the other’s annotation without warning.
Reviewed by Cursor Bugbot for commit 2f5adb9. Configure here.
| const ctx = contextLine(a.data); | ||
| if (ctx) lines.push(` - ${ctx}`); | ||
| if (a.note.trim()) lines.push(` - note: ${a.note.trim()}`); | ||
| } |
There was a problem hiding this comment.
Unescaped markdown alters export
Medium Severity
Copied agent markdown embeds keys, headlines, notes, instructions, and context values without escaping. Backticks in a key break inline code spans, and newlines in notes or instructions can add extra list items or headings, changing what the agent reads.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 2f5adb9. Configure here.
| const storageKey = useMemo(() => annotationsKey(notebook), [notebook]); | ||
| const [session, setSession] = useState<AnnotationSession>(() => | ||
| loadAnnotations(storageKey), | ||
| ); |
There was a problem hiding this comment.
Notebook switch stale session
Medium Severity
session is loaded from localStorage only in the initial useState call while storageKey is derived from the current notebook. If the hook’s notebook input changes without remounting, the UI keeps the prior notebook’s annotations and the next persist writes them under the new notebook’s storage key.
Reviewed by Cursor Bugbot for commit 2f5adb9. Configure here.
…ct fidelity Cloned agentation and matched its actual components/SCSS: - popup: single-line entity header, rows=2 textarea with accent-focus (#3c82f7), delete pinned left (margin-right:auto), pill actions (1rem radius), Add/Save — mirrors annotation-popup-css - replace the header toggle + light panel with agentation's floating FAB toolbar: a collapsed 44px circle that expands to a pill of 34px circular control buttons (annotate / copy / list / clear) with a count badge top-right and a list panel that opens above it — mirrors page-toolbar-css (.toolbarContainer/.controlButton/ .badge/.settingsPanel) - numbered blue markers unchanged (already matched the marker SCSS) typecheck + build green; 41 web tests pass. Pure UI fidelity, no behavior change.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
There are 10 total unresolved issues (including 6 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d192174. Configure here.
| (id: string) => { | ||
| persist({ ...session, items: session.items.filter((a) => a.id !== id) }); | ||
| setPending(null); | ||
| }, |
There was a problem hiding this comment.
Remove closes unrelated popup
Medium Severity
The shared remove handler always clears pending, so deleting any annotation from the floating list closes the note popup even when the user is editing a different entity. In-progress notes can be lost without saving.
Reviewed by Cursor Bugbot for commit d192174. Configure here.
| const clear = useCallback(() => { | ||
| persist({ items: [], instruction: "" }); | ||
| clearAnnotations(storageKey); | ||
| }, [persist, storageKey]); |
There was a problem hiding this comment.
Clear all leaves popup open
Medium Severity
clear wipes the session and storage but never clears pending, so the annotate popup can stay open after “Clear all” with stale existing state. Saving from that popup re-adds an annotation the user believed they removed.
Reviewed by Cursor Bugbot for commit d192174. Configure here.
| return true; | ||
| } catch { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Copy reports success without clipboard
Medium Severity
copy returns true when navigator.clipboard is missing because optional-chained writeText is never invoked and the try block still completes. The toolbar shows a success checkmark although nothing was copied.
Reviewed by Cursor Bugbot for commit d192174. Configure here.
| e.stopPropagation(); | ||
| if (e.key === "Enter" && !e.shiftKey) { | ||
| e.preventDefault(); | ||
| save(); |
There was a problem hiding this comment.
Save allows empty new notes
Low Severity
Pressing Enter in the popup calls save even when the Add button is disabled for an empty note on a new entity. The session stores a note-less annotation that still increments the count and appears in exported markdown without a note line.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit d192174. Configure here.
| const akey = | ||
| title || (fields[0] ? fmt(row[fields[0].key]) : "") || "card"; |
There was a problem hiding this comment.
This persists the annotation under the rendered title, then the first displayed field, then the fixed key card. If the Card query refreshes and a different entity renders with the same title or an empty title/fields, the stored marker and note attach to that new entity. The note is then copied as if it belonged to the current card, even though it was created for a different row. Card annotations need a stable entity key from the row data rather than a display label or static fallback.
Context Used: CLAUDE.md (source)


Why
Brings agentation's core flow — click an entity → attach a note → copy a structured payload for a coding/analysis agent — to the notebook, graph-native. You flag graph entities across a notebook, annotate why, and copy agent-ready markdown to paste into Claude Code (etc.).
Crucially this takes only agentation's read-and-export half: annotations are ephemeral feedback, never written to the graph. So it's host-shell-only — no core/runtime/client/server changes, no schema, no mutations. Where agentation reverse-engineers DOM identity (Fiber/selectors), the notebook already has typed identity (slug / id column + the row's data), so a small React-context hook makes each lens annotation-aware without prop-threading.
What
fix/change/question/approve) + Save/Delete.localStorage(mirrorslayout-overrides.ts); survives reload.Files
annotations-store.ts(+ test) — model, localStorage persistence, markdown serializer.annotation-context.tsx— global + per-cell context,useAnnotation()(leaf) /useAnnotations()(host).components/Annotation{Popup,Panel,Marker}.tsx— overlay UI.App.tsx— annotate toggle, providers, popup/panel mount, per-cell provider inCellBody.Table/ActionList/Subgraph/Card— annotate-on-click + marker (uniform context hook).Scope
Verification
pnpm --filter @modernrelay/notebook-web typecheck+buildgreen.annotations-store.test.ts(markdown serialize; save→load round-trip; empty/unparseable fallback)./ogproxy confirmed serving against the local cluster. The interactive overlay (click → note → copy) wants a manual browser check — the Chrome extension isn't connected in this environment to drive it headlessly.Independent of PR #10 (web-only); based on current
main.Note
Low Risk
Changes are confined to the web package with browser-local storage and no server or graph mutations; main risk is UX overlap between annotate mode and existing row selection or ActionList mutations.
Overview
Adds a web-only annotate flow: click graph entities across notebook cells, attach notes (optional intent), and copy agent-ready markdown—persisted in localStorage per notebook, never written to the graph.
New plumbing:
annotations-store(session model, persistence, serializer + tests),annotation-context(useAnnotationsat the shell,useAnnotationin lenses via global + per-cell providers). App wraps the tree withAnnotationGlobalProvider, mounts popup and floating toolbar, and wraps each cellRendererinAnnotationCellProvider.Lens changes: Table, ActionList, Card, and Subgraph gain annotate-on-click, numbered markers, and crosshair styling when mode is on; Table row clicks annotate instead of selection while active; ActionList action buttons are disabled during annotate mode.
Reviewed by Cursor Bugbot for commit d192174. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
This PR adds a web annotation flow for notebook entities. The main changes are:
Confidence Score: 4/5
This is close, but the Card annotation key should be fixed before merging.
packages/web/src/components/Card.tsx
Important Files Changed
Reviews (3): Last reviewed commit: "style(web): port agentation's toolbar + ..." | Re-trigger Greptile
Context used: