Annotations — editor-web integration
This page covers how editor-web wires the annotation overlay library — see the live capability demos for the library itself. For the embed-facing surfaces, see Collect visual annotations and Receive annotations (webhook).
Two implementations exist behind one seam:
| Impl | Package | Notes |
|---|---|---|
coframe (default in deployed/preview envs) | @coframe/annotations (vendored, packages/coframe-annotations) | AFS v1.1 events, threads, actions, screenshots, scoped hydration |
agentation (legacy fallback) | agentation (npm) | legacy {event:"submit"} payloads |
1. Basics
Section titled “1. Basics”Choosing the implementation
Section titled “Choosing the implementation”The single flag is the ANNOTATIONS_IMPL env var (wrangler var),
resolved centrally by @alphacro/agent-annotations
(packages/agent-annotations/src/impl.ts):
"vars": { "ANNOTATIONS_IMPL": "coframe" }coframe→ new library;agentation→ legacy npm package.- Missing/blank/invalid values fall back to
agentation. - There is no query-string override — env var only.
Nothing in editor-web imports either implementation directly; all
injection goes through @alphacro/agent-annotations
(buildAgentationInstallScript /
buildCoframeAnnotationsInstallScript), which builds the right IIFE +
__init call for the resolved impl.
How the overlay reaches the page
Section titled “How the overlay reaches the page”The worker injects a prebuilt IIFE into the preview page over CDP
(apps/editor-web/worker/agentation/agentation-cdp.ts). The page
watcher (agentation-page-watcher.ts) re-injects on reload, remount,
new documents, and idle — each time asking the host for fresh
install options so the overlay always hydrates from canonical state.
Receiving feedback
Section titled “Receiving feedback”The overlay POSTs AFS v1.1 envelopes
({type, timestamp, sessionId, sequence, payload}) to the session
webhook. packages/agent-annotations/src/webhook.ts validates both AFS
events and legacy agentation payloads at the boundary and maps them to
editor-web submissions (worker/session/session-annotations.ts).
2. Intermediate
Section titled “2. Intermediate”Parent-owned hydration
Section titled “Parent-owned hydration”Canonical pending-annotation state lives in the session layer (the
parent), not in the page. On every (re)injection the watcher seeds the
overlay via __init({ annotations }), so reloading the live view never
loses queued annotations. Hydration is ID-based and emits no duplicate
annotation.created events.
Scoped hydration
Section titled “Scoped hydration”Seeds may carry scope: { url?, sessionId?, variantId? } so
variant-A annotations never render on variant B. URL scope compares
origin + pathname (query/hash ignored); ids are exact-match; unscoped
records always hydrate.
Severity model
Section titled “Severity model”The default composer always shows severity — blocking selected by
default, downgradable to suggestion. Intent is not shown in the
default UI but legacy intent/important metadata is still accepted.
Hosts can reconfigure the whole field set via
__init({ composer: { fields } }) (see the package README).
Re-anchoring
Section titled “Re-anchoring”Pins survive SPA remounts through a layered resolver (unique stable
selector → structural path → context-scored candidates with a
nearby-text contradiction guard → stored bounding box), with an
explicit orphaned state when nothing reliable matches.
3. Advanced
Section titled “3. Advanced”Action slots
Section titled “Action slots”Host-supplied buttons on the toolbar / composer / pin / thread.
Declarative actions are JSON-safe (no callbacks) and can be seeded via
__init over CDP; clicking emits an action.requested AFS event that
the worker reacts to like any other webhook event:
window.__annotations.__init({ actions: [{ id: "ask-agent", label: "Ask agent to fix", placement: "thread" }],});(The worker’s install-script helper currently threads
webhookUrl/sessionId/annotations; extend
CoframeAnnotationsInstallScriptOptions to seed actions/screenshots/
composer config at injection time.)
Element screenshots
Section titled “Element screenshots”Opt-in via screenshots: { onCreate: ["viewport", "element"] }. The
overlay emits a screenshot.capture action request (element requests
include a bounding-box clip); the worker captures via CDP
Page.captureScreenshot and attaches the result with
captureCoframeAnnotationScreenshot
(packages/agent-annotations/src/install-script/coframe.ts, re-exported
through the worker’s CDP layer). The library validates
attachments (kind, MIME/format, size) and rejects mismatches.
Agent round-trip
Section titled “Agent round-trip”The agent can answer feedback in-page through JSON-arg commands sent
with sendCoframeAnnotationsCommand:
replyToAnnotation(id, { role: "agent", content })→ renders in the pin’s thread panel and emitsthread.messageresolveAnnotation(id, "agent")→ fades the pin, ”✓ resolved”, emitsannotation.updatedfocusAnnotation(id)→ opens the thread panelsetCursor({ id, kind: "agent", x, y, label })→ labeled presence cursor (xis % of viewport width,ydocument px)
All of these validate their input at the JSON boundary and return
null/false on malformed calls instead of corrupting overlay state.
Testing
Section titled “Testing”- Package unit tests:
corepack pnpm --filter @coframe/annotations test - Seam/webhook tests:
corepack pnpm --filter @alphacro/agent-annotations test - Worker CDP tests:
apps/editor-web/tests/agentation-cdp.test.ts - Browser-harness QA workflow:
.agents/skills/testing-annotations-overlays/SKILL.md
Full library API reference: packages/coframe-annotations/README.md.