Skip to content

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:

ImplPackageNotes
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

The single flag is the ANNOTATIONS_IMPL env var (wrangler var), resolved centrally by @alphacro/agent-annotations (packages/agent-annotations/src/impl.ts):

apps/editor-web/wrangler.jsonc
"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.

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.

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).

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.

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.

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).

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.

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.)

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.

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 emits thread.message
  • resolveAnnotation(id, "agent") → fades the pin, ”✓ resolved”, emits annotation.updated
  • focusAnnotation(id) → opens the thread panel
  • setCursor({ id, kind: "agent", x, y, label }) → labeled presence cursor (x is % of viewport width, y document px)

All of these validate their input at the JSON boundary and return null/false on malformed calls instead of corrupting overlay state.

  • 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.