Skip to content

Annotation overlay: flag entities, note them, copy for an agent (web)#12

Open
ragnorc wants to merge 3 commits into
mainfrom
ragnorc/annotation-overlay
Open

Annotation overlay: flag entities, note them, copy for an agent (web)#12
ragnorc wants to merge 3 commits into
mainfrom
ragnorc/annotation-overlay

Conversation

@ragnorc

@ragnorc ragnorc commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

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

  • Annotate mode toggle in the header (parallels "Edit layout"). In annotate mode every rendered entity becomes clickable.
  • Anchored note popup (entity-positioned): textarea + optional intent chip (fix/change/question/approve) + Save/Delete.
  • Marker on annotated entities; a floating panel lists them grouped by cell, with an overall-instruction field, Copy for agent, and Clear.
  • Persisted per-notebook in localStorage (mirrors layout-overrides.ts); survives reload.
  • Clipboard export — grouped markdown carrying type+key, source query, displayed columns, intent, and note:
    # Notebook annotations — Company context
    > server: … · graph: company
    
    ## policy-clause-review (ActionList · query: policy_clauses_for_review)
    - [change] `pdr-c3` — EU territoriality
      - note: conflicts with the zero-retention clause — reconcile

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 in CellBody.
  • lenses Table/ActionList/Subgraph/Card — annotate-on-click + marker (uniform context hook).

Scope

  • v1: Table, ActionList, Subgraph, Card (clean per-entity identity). TUI skipped (web-only feature). v1.1: Path/Timeline/Quote/Text (index-keyed). Later: multi-select → one note over a set; agent-loop.

Verification

  • pnpm --filter @modernrelay/notebook-web typecheck + build green.
  • 41 web tests pass, incl. new annotations-store.test.ts (markdown serialize; save→load round-trip; empty/unparseable fallback).
  • Dev server + /og proxy 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 (useAnnotations at the shell, useAnnotation in lenses via global + per-cell providers). App wraps the tree with AnnotationGlobalProvider, mounts popup and floating toolbar, and wraps each cell Renderer in AnnotationCellProvider.

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:

  • Annotate mode in the web shell with a floating toolbar.
  • Entity note popups with optional intent tags.
  • Local per-notebook annotation persistence.
  • Markdown export for agent handoff.
  • Annotation support in Table, ActionList, Subgraph, and Card lenses.

Confidence Score: 4/5

This is close, but the Card annotation key should be fixed before merging.

  • Card annotations can attach to the wrong entity after a query refresh.
  • The key comes from display text or a static fallback instead of stable row identity.
  • The rest of the reviewed annotation wiring is contained to the web shell.

packages/web/src/components/Card.tsx

Important Files Changed

Filename Overview
packages/web/src/App.tsx Wires the annotation provider, popup, toolbar, and per-cell context into the web app.
packages/web/src/annotation-context.tsx Adds annotation session state, persistence, popup handling, and clipboard copy support.
packages/web/src/annotations-store.ts Adds localStorage helpers and markdown serialization for annotation sessions.
packages/web/src/components/Table.tsx Adds row annotation clicks and markers to the table lens.
packages/web/src/components/ActionList.tsx Adds action annotation clicks and markers, and disables actions while annotating.
packages/web/src/components/Card.tsx Adds Card annotation support, but uses display text as the persisted annotation key.
packages/web/src/components/Subgraph.tsx Adds annotation support for subgraph center nodes.

Fix All in Claude Code

Reviews (3): Last reviewed commit: "style(web): port agentation's toolbar + ..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

  • Context used - CLAUDE.md (source)

… 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],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4010583. Configure here.

typeof o.cellId === "string" &&
typeof o.key === "string" &&
typeof o.note === "string"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4010583. Configure here.

Comment thread packages/web/src/components/Table.tsx
Comment thread packages/web/src/components/Table.tsx
Comment thread packages/web/src/components/ActionList.tsx Outdated
Comment thread packages/web/src/annotations-store.ts
Comment thread packages/web/src/annotation-context.tsx

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +77 to +82
const v = select_column
? String(row[select_column] ?? "")
: firstCol
? String(row[firstCol] ?? "")
: "";
return v || `#${idx}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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()}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2f5adb9. Configure here.

const storageKey = useMemo(() => annotationsKey(notebook), [notebook]);
const [session, setSession] = useState<AnnotationSession>(() =>
loadAnnotations(storageKey),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 4 potential issues.

There are 10 total unresolved issues (including 6 from previous reviews).

Fix All in Cursor

❌ 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);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d192174. Configure here.

const clear = useCallback(() => {
persist({ items: [], instruction: "" });
clearAnnotations(storageKey);
}, [persist, storageKey]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d192174. Configure here.

return true;
} catch {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d192174. Configure here.

e.stopPropagation();
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
save();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d192174. Configure here.

Comment on lines +41 to +42
const akey =
title || (fields[0] ? fmt(row[fields[0].key]) : "") || "card";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Card Key Can Drift

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)

Fix in Claude Code

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.

1 participant