@llui/devmode-annotate

Current package version: 0.4.2

Browser-side HUD that connects the running LLui app to a shared on-disk notebook the LLM can also read and write. Floating button → click to draft a text note, or drag to draw a rectangle around the thing you want the LLM to look at → submit. The note lands as a .md file on disk under <your-app>/.llui/notes/session-<id>/ with a screenshot, the URL, the route, the component path under the cursor, scope state, recent messages, dirty trace, and (opt-in) verbose runtime telemetry.

The LLM consumes the same directory via @llui/mcp's llui_list_notes / llui_read_note / llui_capture tools and can request its own captures back through the HUD via the dev-server's SSE channel.

pnpm add -D @llui/devmode-annotate
// app entry — dev-only mount
import { mountAnnotateHud } from '@llui/devmode-annotate'

if (import.meta.env.DEV) {
  mountAnnotateHud()
}

That's the entire setup. The HUD only mounts when the dev-server has the devmodeAnnotate middleware registered (on by default in dev mode for the LLui Vite plugin). Production builds tree-shake the import.

How the pieces fit together

running app           dev-server               LLM
─────────────         ──────────────────       ───────────────

@llui/devmode-       @llui/vite-plugin        @llui/mcp
annotate (HUD)  ←──→ notes middleware   ←──→  notes tools
                     (/_llui/*)               (llui_capture,
                          │                    llui_list_notes,
                          ▼                    llui_read_note,
                     .llui/notes/              llui_note_*)
                     session-<id>/
                       001-human-…md
                       001-human-…png
                       002-llm-reply-…md
                       …
  • @llui/devmode-annotate — this package. The HUD: floating button, draft modal, rect overlay, screenshot capture, programmatic submit() API.
  • @llui/vite-plugin — owns the notes middleware mounted at /_llui/*. Same dev-server already running your app; nothing extra to boot. See devmodeAnnotate config.
  • @llui/mcp — exposes the notebook to the LLM as MCP resources and tools. The LLM can request a capture (HUD draws it), or read notes the human dropped. See notesRoot.

The notebook itself outlives any of them — the on-disk format is the contract, the three clients all read and write the same files. Full design + on-disk format spec: docs/proposals/devmode-annotate/.

When to use it

Use this when you'd otherwise screenshot + circle + paste into a chat. The HUD captures all the things that flatten away in that workflow — URL, route, component path under the cursor, scope state, message log, in-flight effects, dirty trace, source-position map. The LLM reads a rich artifact, not a flattened image.

Skip it for production telemetry, error reporting, or anything end users would see — this is a dev-mode developer surface, not a user-facing feedback widget.

Note intents: note vs task

Each submission carries an intent:

  • task (default for HUD button) — an actionable ask. Lands in the LLM's queue and shows up as a "Solve" affordance in subsequent notes. The optional attention router (see 05-task-mode.md in the proposal) auto-dispatches these to a headless Claude Code process and streams status (openworkingproposedaccepted) back into the HUD.
  • note — an FYI / observation. Doesn't enter the task queue; the LLM consumes it as ambient context. Pass intent: 'note' to submit() for these.

Use setIntent() to flip the floating-button default.

Capture levels

Every note carries a screenshot + the standard telemetry. Pass captureLevel: 'verbose' to additionally include the full binding array, scope tree, and recent message ring buffer. Verbose captures grow notes by 10–100× — useful for "I don't know what's wrong" investigations, overkill for "this button is the wrong color."

hud.submit('this list re-renders on every keystroke — why?', {
  captureLevel: 'verbose',
  intent: 'task',
})

LLM-initiated captures

When the LLM (via @llui/mcp's llui_capture tool) asks for a fresh snapshot, the dev-server fans the request out via SSE to every connected HUD. The HUD that owns the active page handles it, captures, posts the note back, and the LLM's tool call resolves with the note's metadata. No human in the loop required — the LLM can poke at the running app the same way the developer can.

When no HUD is connected (e.g., the app is closed in the browser), @llui/mcp falls back to a headless Playwright capture against the dev-server URL. The LLM gets a screenshot either way.

API

Functions

bundleFilename()

Default bundle filename: llui-notes-<contentHash prefix>.zip.

function bundleFilename(manifest: BundleManifest): string

defaultSecretRedactor()

An opt-in convenience redactor for the state channel: deep-walks the captured stateSnapshot / message+console logs and masks common secret shapes (Bearer tokens, sk-/ghp_ keys, JWTs, emails) in string values. A defense-in-depth default a host can plug in (redact: { state: defaultSecretRedactor() }); it does NOT replace authoring-time care — the host still owns what's sensitive. State is JSON-serializable (no cycles) by the framework contract; a depth cap guards pathological inputs.

function defaultSecretRedactor(options: SecretRedactorOptions = {}): (body: NoteBody) => NoteBody

devServerStore()

Build the dev-server-backed store rooted at origin (e.g. location.origin). When a taskCapabilityToken is supplied (injected by @llui/vite-plugin when the attention router is enabled), it's sent as the x-llui-task-capability header so the middleware can trust an in-HUD task submission.

function devServerStore(origin: string, taskCapabilityToken?: string): NotesStore

exportBundle()

Build an export bundle from any store that can produce raw sessions. Returns the zip as a Blob (for download), the parsed manifest, and the raw bytes.

function exportBundle(store: ExportableStore, opts: ExportBundleOptions = {}): Promise<ExportBundleResult>

httpStore()

A NotesStore that talks to a host-provided HTTP backend. Use in production when a team wants centralized capture instead of manual export/import. The backend must speak the notebook wire protocol (the same shapes the dev server serves under /_llui).

function httpStore(opts: HttpStoreOptions): NotesStore

indexedDbStore()

Build a browser-local NotesStore backed by IndexedDB. No dev server required; the HUD captures, persists, and browses entirely client-side.

function indexedDbStore(opts: IndexedDbStoreOptions = {}): NotesStore & ExportableStore

mountAnnotateHud()

function mountAnnotateHud(opts: MountAnnotateOptions = {}): AnnotateHudHandle

registerAnnotateEditor()

Register an optional editor implementation. The returned disposer restores the previous registration, which makes temporary host overrides safe.

function registerAnnotateEditor(editor: AnnotateEditorRegistration): () => void

Types

BakeFn

export type BakeFn = (
  screenshotBase64: string,
  annotations: Annotation[],
  geometry?: ScreenshotGeometry,
) => Promise<string>

HeadersInput

Static headers, or a (sync/async) function called per request so tokens can refresh.

export type HeadersInput =
  | Record<string, string>
  | (() => Record<string, string> | Promise<Record<string, string>>)

Interfaces

AnnotateEditorInstance

export interface AnnotateEditorInstance {
  getValue(): string
  setValue(value: string): void
  focus(): void
  dispose(): void
}

AnnotateEditorMountOptions

Optional note-editor seam for the HUD.

Core owns the note value and keyboard behavior. An editor package owns only the live editing surface, so the core package never needs to import its implementation (or any of that implementation's dependencies).

export interface AnnotateEditorMountOptions {
  host: HTMLElement
  initialValue: string
  placeholder: string
  onChange(value: string): void
}

AnnotateEditorRegistration

export interface AnnotateEditorRegistration {
  /** Human-readable help rendered below the editor surface. */
  hint: string
  /** Styles adopted into the HUD shadow root when isolation is enabled. */
  shadowCss?: string
  mount(options: AnnotateEditorMountOptions): AnnotateEditorInstance
}

AnnotateHudHandle

export interface AnnotateHudHandle {
  open(): void
  close(): void
  destroy(): void
  /** Programmatically set the compose draft (Markdown). Flows into the embedded
   * editor like a restored draft. */
  setProse(text: string): void
  submit(
    prose: string,
    opts?: {
      captureLevel?: CaptureLevel
      annotations?: Annotation[]
      screenshot?: string
      intent?: NoteIntent
      resume?: boolean
      chainName?: string
    },
  ): Promise<CreateNoteResponse>
  drawRect(): Promise<NoteRect | null>
  handleCaptureRequest(
    requestId: string,
    payload: CaptureRequestPayload,
  ): Promise<CreateNoteResponse>
  setIntent(intent: NoteIntent): void
  replayRepro(
    events: ReproEvent[],
    options?: { speed?: number; maxStepMs?: number; abortOnMissing?: boolean },
  ): Promise<{ applied: number; skipped: Array<{ event: ReproEvent; reason: string }> }>
  /** Export the notebook as a downloadable `.zip` bundle and trigger a
   *  browser download. Resolves to the bundle manifest, or `null` when the
   *  active store can't export (e.g. the dev-server store). */
  exportBundle(): Promise<BundleManifest | null>
}

BundleAppProvenance

Capture-environment provenance (host-populated; omitted when unknown).

export interface BundleAppProvenance {
  version?: string
  buildId?: string
  releaseChannel?: string
  url?: string
}

BundleIdentity

Who captured the notes (host-populated; omitted when unknown).

export interface BundleIdentity {
  id?: string
  label?: string
  kind: 'human' | 'llm' | 'agent'
}

BundleManifest

export interface BundleManifest {
  /** On-disk note-format schema version (see NOTE_SCHEMA_VERSION). */
  schemaVersion: number
  /** Host-stamped export time (ISO). */
  exportedAt: string
  /** The sessions included, sorted. */
  sessions: string[]
  /** Total `.md` notes across all sessions. */
  noteCount: number
  /** SHA-256 hex over every file entry (sorted by path), excluding the
   *  manifest itself. Drives idempotent import + integrity checks. */
  contentHash: string
  exportedBy?: BundleIdentity
  app?: BundleAppProvenance
}

CaptureDefaults

export interface CaptureDefaults {
  /** Collect the verbose debug-telemetry body (state/message/effect dump). */
  debug: boolean
  /** Record user interactions (repro trace). */
  repro: boolean
}

EventSubscription

Live-event subscription parameters.

export interface EventSubscription {
  role: SseRole
  onEvent: (event: ServerEvent) => void
  onError?: (err: unknown) => void
}

ExportableStore

A store that can produce its notebook as raw on-disk-format entries, for export into a zip bundle. Browser stores implement this; the dev-server store doesn't need to (its files already live on disk).

export interface ExportableStore {
  exportSessions(sessionIds?: string[]): Promise<RawSession[]>
}

ExportBundleOptions

export interface ExportBundleOptions {
  /** Limit to these sessions. Default: every session in the store. */
  sessionIds?: string[]
  /** Capture identity recorded in the manifest. */
  exportedBy?: BundleIdentity
  /** App/environment provenance recorded in the manifest. */
  app?: BundleAppProvenance
  /** Clock override (tests / deterministic runs). */
  now?: () => Date
}

ExportBundleResult

export interface ExportBundleResult {
  blob: Blob
  manifest: BundleManifest
  /** The raw zip bytes (same content as `blob`), handy for tests/Node. */
  bytes: Uint8Array
}

FullNote

A note fetched in full (the format=json shape). Frontmatter/body are intentionally loose — consumers narrow what they read. Concrete NoteFrontmatter/NoteBody values assign here (they carry these fields and more); the dev-server adapter also fills it from raw server JSON.

export interface FullNote {
  frontmatter: {
    kind: string
    author: string
    intent?: string
    screenshot?: string | null
  }
  prose: string
  body?: { repro?: unknown[] }
}

HttpStoreOptions

export interface HttpStoreOptions {
  /** Base URL the host's notebook backend lives under, no trailing slash. */
  baseUrl: string
  /** Headers injected on every request (e.g. an auth token). Never bake
   *  credentials into the bundle — supply them here at mount time. */
  headers?: HeadersInput
  /** Override fetch (tests / custom transport). */
  fetch?: typeof fetch
}

IndexedDbStoreOptions

export interface IndexedDbStoreOptions {
  /** IndexedDB database name. Default `llui-devmode-annotate`. */
  dbName?: string
  /** Clock override (tests / deterministic runs). Default `() => new Date()`. */
  now?: () => Date
}

MountAnnotateOptions

export interface MountAnnotateOptions {
  origin?: string
  /** The notes transport. Defaults to `devServerStore(origin)` — the Vite
   *  dev-server endpoints. Inject a different adapter (IndexedDB, HTTP,
   *  export bundle) to run the HUD without a dev server.
   *
   *  `destroy()` calls `store.dispose()` on whatever it is given, so the HUD
   *  takes over the instance's out-of-heap resources: give it its OWN store
   *  rather than one the host also drives (see `NotesStore.dispose`). */
  store?: NotesStore
  /** Mount in a production build. By default the HUD only mounts under the
   *  dev server (`import.meta.env.DEV`); set this when a live app deliberately
   *  ships it (typically via `installAnnotateHud`, behind the host's own
   *  authorization). */
  allowProduction?: boolean
  llui?: { runtime: string; compiler: string }
  hidden?: boolean
  capture?: CaptureFn
  bake?: BakeFn
  subscribeEvents?: boolean
  rehydrate?: boolean
  solveEnabled?: boolean
  /** Out-of-band capability token injected by `@llui/vite-plugin` (only when the
   *  attention router is enabled). The HUD echoes it as the `x-llui-task-capability`
   *  header on note POSTs so the dev-server middleware can distinguish a real in-HUD
   *  task submission (which may spawn a CLI agent) from a forged same-origin page
   *  POST. Without it, task submissions are created but never marked trusted, so
   *  they never spawn. Ignored when a custom `store` is supplied (the caller owns
   *  its transport/auth). */
  taskCapabilityToken?: string
  autoCaptureOnError?: boolean
  repro?: boolean
  elementPick?: boolean
  /** Per-channel redaction hooks (state / repro / screenshot), run before a
   *  capture is persisted. The host owns the privacy policy; these are the
   *  seams to enforce it. */
  redact?: RedactHooks
  /** Collect the verbose debug-telemetry body (state/message/effect dump).
   *  Defaults: on under the dev server, OFF in production. */
  captureDebug?: boolean
  /** Mount the HUD chrome inside an open shadow root with isolated styles
   *  (constructable `adoptedStyleSheets`, falling back to a shadow `<style>`).
   *  Gives bidirectional style isolation from the host app and avoids the
   *  `style-src 'unsafe-inline'` CSP rule. Default false (light DOM, the dev
   *  default); `installAnnotateHud` turns it on for production. */
  isolate?: boolean
  /** Registered editor stylesheet text adopted into the shadow root in isolate
   *  mode. Defaults to the active editor registration's `shadowCss`. Override
   *  to supply the CSS in environments where raw CSS imports can't resolve. */
  editorCss?: string
}

NotesStore

The transport the HUD reads and writes through. Methods reject on failure; callers keep their own try/catch and best-effort semantics.

export interface NotesStore {
  /** Create a note (text/rect/capture/reply, or a task). */
  createNote(req: CreateNoteRequest): Promise<CreateNoteResponse>

  /** List all sessions, newest first. */
  listSessions(): Promise<SessionSummary[]>

  /** The session the store is currently writing into. */
  currentSession(): Promise<CurrentSessionResponse>

  /** Summaries of the notes in a session. */
  listNotes(query: ListNotesQuery): Promise<ListNotesResponse>

  /** A single note in full, or null if it can't be read. */
  readNote(id: string, sessionId: string): Promise<FullNote | null>

  /** A note's current status + transition history. */
  getStatus(id: string, sessionId: string): Promise<NoteStatusResponse>

  /** The task queue for a session. */
  getQueue(sessionId: string): Promise<QueueResponse>

  /** Delete a note. */
  deleteNote(id: string, sessionId: string): Promise<void>

  /** Patch a note (currently prose only). */
  updateNote(id: string, sessionId: string, update: NoteUpdate): Promise<void>

  /** Request a status transition for a note. */
  postStatus(id: string, sessionId: string, update: StatusUpdate): Promise<void>

  /** A URL usable directly as an `<img src>` for a note's screenshot.
   *  `screenshotRef` is the frontmatter `screenshot` value. Synchronous so
   *  it can be read inside a reactive view binding. */
  screenshotUrl(id: string, screenshotRef: string): string

  /** Subscribe to live notebook events. Returns an unsubscribe function.
   *  A noop subscription (returning a noop unsubscribe) is valid when the
   *  store has no live channel. */
  subscribeEvents(sub: EventSubscription): () => void

  /** Release everything the store holds outside the JS heap — object URLs,
   *  open connections — so a HUD mount/destroy cycle reclaims it. Idempotent.
   *
   *  The store stays USABLE (a later call lazily re-creates what it needs),
   *  but this is not a no-op for anything already handed out: it is the
   *  OWNER's teardown, and what it releases can belong to someone else.
   *  `indexedDbStore` revokes every object URL `screenshotUrl` returned, so an
   *  `<img>` the host is still displaying goes blank; `httpStore` closes every
   *  live `EventSource`, so ANOTHER subscriber's `onEvent` goes silently dead
   *  and nothing re-opens it.
   *
   *  This matters because `mountAnnotateHud` calls it from `destroy()` on the
   *  store it was GIVEN — the HUD cannot leave an injected store undisposed
   *  without re-opening the leak this exists to close (an inline
   *  `installAnnotateHud({ store: indexedDbStore() })` keeps no reference the
   *  host could dispose, and object URLs are not garbage-collected). So a host
   *  that also uses the store from its own code should construct a SECOND
   *  instance for the HUD; they are cheap and share the backing store.
   *
   *  Required on the port rather than optional so the compiler names it for
   *  every adapter (a store with nothing to release implements a no-op) —
   *  though that is a type-level guard only: a `as unknown as NotesStore` test
   *  fake still reaches destroy() and throws there. */
  dispose(): void
}

NoteStatusResponse

Status sidecar for a single note: its current status + transition log.

export interface NoteStatusResponse {
  current: NoteStatus | null
  history: StatusTransition[]
}

NoteUpdate

A mutable patch to an existing note (PATCH).

export interface NoteUpdate {
  prose?: string
}

QueueEntry

A note's place in the task queue.

export interface QueueEntry {
  noteId: string
  status: NoteStatus
}

QueueResponse

export interface QueueResponse {
  queue: QueueEntry[]
}

RawNote

One note in raw export form: its serialized .md plus optional screenshot.

export interface RawNote {
  /** The `.md` filename (canonical `{id}-{author}-{kind}-{slug}.md`). */
  filename: string
  /** Serialized note markdown (YAML frontmatter + prose). */
  markdown: string
  /** Screenshot bytes (PNG), or null when the note has none. */
  screenshot: Uint8Array | null
}

RawSession

One session in raw export form.

export interface RawSession {
  id: string
  notes: RawNote[]
  /** `status.jsonl` content (one JSON transition per line; '' when empty). */
  statusJsonl: string
}

RedactHooks

Per-channel sanitize hooks, each run just before a capture is persisted. Separate channels so a host can drop only the risky one rather than all-or-nothing.

export interface RedactHooks {
  /** Transform the debug-telemetry body (per-component state snapshot,
   *  message/effect logs, dirty trace, …). Return a replacement, e.g.
   *  `{}` to drop it entirely or a copy with `stateSnapshot` removed. */
  state?: (body: NoteBody) => NoteBody
  /** Transform recorded interactions (e.g. mask typed input values). Return
   *  `[]` to drop the repro trace. */
  repro?: (events: ReproEvent[]) => ReproEvent[]
  /** Transform the screenshot (base64 PNG, no `data:` prefix) — e.g. mask
   *  regions. Return `null` to drop the screenshot entirely. */
  screenshot?: (pngBase64: string) => string | null
}

ScreenshotGeometry

Geometry threaded from the capture into the annotation baker so viewport (CSS-px, scroll-relative) annotation coordinates land correctly on the full-document, DPR-scaled screenshot raster.

export interface ScreenshotGeometry {
  /** Device pixel ratio the screenshot was captured at (frontmatter viewport.dpr). */
  dpr: number
  /** Viewport scroll offset (CSS px) at capture time. */
  scrollX: number
  scrollY: number
}

SecretRedactorOptions

export interface SecretRedactorOptions {
  /** Extra regexes whose matches are masked (added to the built-ins). */
  patterns?: readonly RegExp[]
  /** Replacement token. Default `'[redacted]'`. */
  mask?: string
  /** Max recursion depth for the state walk. Default 12. */
  maxDepth?: number
}

SessionSummary

One session as returned by the session list.

export interface SessionSummary {
  id: string
  noteCount: number
  startedAt?: string
}

StatusUpdate

A status transition the HUD requests (POST status).

export interface StatusUpdate {
  to: NoteStatus
  by: Author | 'system'
  reason?: string
}

Constants

NOTE_SCHEMA_VERSION

On-disk note-format schema version. Stamped into export bundles and checked on dev import so a producer and consumer never silently disagree. v2 = the current "body under a body: frontmatter key" format (v1 was the legacy trailing-```json fence, still readable by parseNote).

const NOTE_SCHEMA_VERSION

Public Entry Points

@llui/devmode-annotate/note-types

Types

Annotation from @llui/devmode-annotate/note-types
export type Annotation = ({
    type: 'rect';
} & NoteRect & {
    label?: string;
}) | {
    type: 'element';
    selector: string;
    bbox: NoteRect;
    label?: string;
};
Author from @llui/devmode-annotate/note-types
export type Author = 'human' | 'llm';
CaptureLevel from @llui/devmode-annotate/note-types
export type CaptureLevel = 'standard' | 'verbose';
LogLevel from @llui/devmode-annotate/note-types
export type LogLevel = 'log' | 'warn' | 'error' | 'info' | 'debug';
NoteIntent from @llui/devmode-annotate/note-types
export type NoteIntent = 'task' | 'note';
NoteKind from @llui/devmode-annotate/note-types
export type NoteKind = 'rect' | 'element' | 'text' | 'capture' | 'reply';
NoteStatus from @llui/devmode-annotate/note-types
export type NoteStatus = 'open' | 'claimed' | 'in-progress' | 'proposed' | 'accepted' | 'applied' | 'rejected' | 'wontfix' | 'failed';
ReproEvent from @llui/devmode-annotate/note-types
export type ReproEvent = {
    type: 'click';
    t: number;
    selector: string;
} | {
    type: 'input';
    t: number;
    selector: string;
    value?: string;
    redacted?: boolean;
} | {
    type: 'keydown';
    t: number;
    key: string;
    mods?: string;
} | {
    type: 'route';
    t: number;
    pathname: string;
};
ServerEvent from @llui/devmode-annotate/note-types
export type ServerEvent = {
    type: 'note-created';
    id: string;
    filename: string;
    author: Author;
} | {
    type: 'note-updated';
    id: string;
    sessionId: string;
} | {
    type: 'note-deleted';
    id: string;
    sessionId: string;
} | {
    type: 'task-progress';
    noteId: string;
    elapsedMs: number;
    /** Token counters from the LLM stream.
     *   - `in`: latest cumulative input_tokens (context size). Grows
     *           monotonically across the conversation.
     *   - `out`: sum of all output_tokens generated so far.
     *   - `cacheRead`: prompt-cache hits, if the model reports them
     *           (claude's `cache_read_input_tokens`). Shows how much
     *           of the context was served from cache vs. reprocessed. */
    tokens?: {
        in: number;
        out: number;
        cacheRead?: number;
    };
    toolSummary?: string;
} | {
    type: 'capture-request';
    requestId: string;
    payload: CaptureRequestPayload;
} | {
    type: 'capture-request-cancelled';
    requestId: string;
} | {
    type: 'session-rotated';
    sessionId: string;
} | {
    type: 'status-changed';
    noteId: string;
    from: NoteStatus | null;
    to: NoteStatus;
    /** Optional human-readable context — e.g. the LLM's proposed-fix
     *  summary, a failure message, or a git-apply conflict. The HUD
     *  surfaces this verbatim in its status line. */
    reason?: string;
};
SseRole from @llui/devmode-annotate/note-types
export type SseRole = 'hud' | 'mcp' | 'viewer';

Interfaces

AgentSchemaSummary from @llui/devmode-annotate/note-types
export interface AgentSchemaSummary {
    msg: string;
    fields: Record<string, string>;
}
CaptureRequestPayload from @llui/devmode-annotate/note-types
export interface CaptureRequestPayload {
    route?: string;
    url?: string;
    selector?: string;
    annotate?: Annotation[];
    prose?: string;
    waitForMessage?: string;
    captureLevel?: CaptureLevel;
    timeoutMs?: number;
}
CaptureRequestResponse from @llui/devmode-annotate/note-types
export interface CaptureRequestResponse {
    requestId: string;
    status: 'fulfilled' | 'timeout' | 'no-client';
    note?: CreateNoteResponse;
}
ComponentMetaRef from @llui/devmode-annotate/note-types
export interface ComponentMetaRef {
    file: string;
    line: number;
    name: string;
}
ConsoleLogEntry from @llui/devmode-annotate/note-types
export interface ConsoleLogEntry {
    ts: string;
    level: LogLevel;
    text: string;
}
CreateNoteRequest from @llui/devmode-annotate/note-types
export interface CreateNoteRequest {
    body: string;
    frontmatter: Omit<NoteFrontmatter, 'id' | 'ts'>;
    noteBody: NoteBody;
    screenshot?: string;
}
CreateNoteResponse from @llui/devmode-annotate/note-types
export interface CreateNoteResponse {
    id: string;
    filename: string;
    path: string;
    sessionId: string;
}
CurrentSessionResponse from @llui/devmode-annotate/note-types
export interface CurrentSessionResponse {
    sessionId: string;
    startedAt: string;
    notesDir: string;
}
DirtyTraceEntry from @llui/devmode-annotate/note-types
export interface DirtyTraceEntry {
    component: string;
    pathsTracked: string[];
    mask: number;
    maskHi?: number;
    lastFlippedBits: string[];
}
ListNotesError from @llui/devmode-annotate/note-types

A note file that matched the canonical filename but could not be parsed (corrupt frontmatter, torn write, hand-edited). Surfaced rather than silently dropped so a broken note is visible instead of vanishing.

export interface ListNotesError {
    filename: string;
    message: string;
}
ListNotesQuery from @llui/devmode-annotate/note-types
export interface ListNotesQuery {
    sessionId?: string;
    author?: Author;
    kind?: NoteKind | NoteKind[];
    since?: string;
    limit?: number;
}
ListNotesResponse from @llui/devmode-annotate/note-types
export interface ListNotesResponse {
    sessionId: string;
    notes: NoteSummary[];
    total: number;
    /** Present (and non-empty) only when one or more note files failed to
     *  parse. Absent on the clean path so existing consumers are unaffected. */
    errors?: ListNotesError[];
}
MessageLogEntry from @llui/devmode-annotate/note-types
export interface MessageLogEntry {
    ts: string;
    component: string;
    msg: unknown;
}
NoteBody from @llui/devmode-annotate/note-types
export interface NoteBody {
    stateSnapshot?: unknown;
    messageLog?: MessageLogEntry[];
    consoleLog?: ConsoleLogEntry[];
    pendingMessages?: PendingMessage[];
    effects?: {
        pending: PendingEffectEntry[];
        recent: RecentEffectEntry[];
    };
    dirtyTrace?: DirtyTraceEntry[];
    structuralAt?: StructuralSnapshot;
    sourceMap?: SourceMapEntry[];
    errors?: RuntimeErrorEntry[];
    /** Captured user interactions from the HUD's repro recorder. The
     *  LLM uses this to understand what the developer did before the
     *  bug appeared. Times are milliseconds from the start of the
     *  recording, not absolute. */
    repro?: ReproEvent[];
    verbose?: VerboseNoteBody;
}
NoteFrontmatter from @llui/devmode-annotate/note-types
export interface NoteFrontmatter {
    id: string;
    ts: string;
    author: Author;
    kind: NoteKind;
    captureLevel: CaptureLevel;
    url: string;
    route: string | null;
    routeParams: Record<string, string>;
    viewport: {
        w: number;
        h: number;
        dpr: number;
    };
    componentPath: string[] | null;
    componentMeta: ComponentMetaRef | null;
    annotations: Annotation[];
    screenshot: string | null;
    agentSchemas: AgentSchemaSummary[];
    llui: {
        runtime: string;
        compiler: string;
    };
    fulfillsRequestId?: string;
    intent?: NoteIntent;
    replyTo?: string;
    proposedDiff?: ProposedDiff;
    /** When true (default true for HUD-originated tasks), the router
     *  spawns the LLM with its resume-previous-conversation flag (e.g.
     *  `claude --continue`) so the LLM keeps prior context. Presets
     *  without a resume flag treat this as a no-op. */
    resume?: boolean;
    /** Name of the resume chain this task participates in. The router
     *  keeps a map of chain name → last session id and passes the
     *  corresponding id via `--resume` when `resume: true`. Lets the
     *  user maintain independent conversation threads (e.g. "refactor",
     *  "ui-polish") without them stomping on each other. Default
     *  `'default'`. */
    chainName?: string;
}
NoteRect from @llui/devmode-annotate/note-types
export interface NoteRect {
    x: number;
    y: number;
    w: number;
    h: number;
}
NoteSummary from @llui/devmode-annotate/note-types
export interface NoteSummary {
    id: string;
    sessionId: string;
    filename: string;
    ts: string;
    author: Author;
    kind: NoteKind;
    url: string;
    componentPath: string[] | null;
    preview: string;
    hasScreenshot: boolean;
    /** Frontmatter shortcuts surfaced in the list so the HUD can
     *  rehydrate trackedTasks + chainHistories on reload without
     *  fetching each note individually. Optional for back-compat with
     *  servers that don't populate them. */
    intent?: NoteIntent;
    chainName?: string;
    /** For reply notes only — the original task this reply addresses. */
    replyTo?: string;
    /** For reply notes only — the LLM's one-line summary of the
     *  proposed change (extracted from proposedDiff). */
    proposedSummary?: string;
}
PendingEffectEntry from @llui/devmode-annotate/note-types
export interface PendingEffectEntry {
    id: string;
    component: string;
    effect: unknown;
    sinceMs: number;
}
PendingMessage from @llui/devmode-annotate/note-types
export interface PendingMessage {
    component: string;
    msg: unknown;
}
ProposedDiff from @llui/devmode-annotate/note-types
export interface ProposedDiff {
    files: Array<{
        path: string;
        patch: string;
    }>;
    summary: string;
    confidence: 'high' | 'medium' | 'low';
}
RecentEffectEntry from @llui/devmode-annotate/note-types
export interface RecentEffectEntry {
    ts: string;
    component: string;
    effect: unknown;
    outcome: 'ok' | 'error' | 'cancelled';
    error?: string;
}
RuntimeErrorEntry from @llui/devmode-annotate/note-types
export interface RuntimeErrorEntry {
    ts: string;
    kind: 'runtime' | 'compiler';
    file?: string;
    line?: number;
    message: string;
    stack?: string;
}
SourceMapEntry from @llui/devmode-annotate/note-types
export interface SourceMapEntry {
    selector: string;
    file: string;
    line: number;
    componentPath: string[];
}
StatusTransition from @llui/devmode-annotate/note-types
export interface StatusTransition {
    ts: string;
    noteId: string;
    from: NoteStatus | null;
    to: NoteStatus;
    by: Author | 'system';
    reason?: string;
}
StructuralSnapshot from @llui/devmode-annotate/note-types
export interface StructuralSnapshot {
    branches: Array<{
        at: string;
        activeArm: string;
    }>;
    shows: Array<{
        at: string;
        visible: boolean;
    }>;
    eachKeys: Array<{
        at: string;
        keys: string[];
    }>;
}
VerboseNoteBody from @llui/devmode-annotate/note-types
export interface VerboseNoteBody {
    scopeTree?: Array<{
        id: string;
        parent: string | null;
        component: string;
        key?: string;
    }>;
    bindings?: {
        total: number;
        hottest: Array<{
            component: string;
            path: string;
            firesPerSec: number;
        }>;
        lastCycleMs: number;
    };
    agentBridge?: {
        connectedAgents: string[];
        pendingToolCalls: number;
        recentMsgs: Array<{
            ts: string;
            direction: 'in' | 'out';
            payload: unknown;
        }>;
    };
    transitionsInFlight?: Array<{
        component: string;
        name: string;
        progress: number;
    }>;
    foreignInstances?: Array<{
        component: string;
        library: string;
    }>;
}

@llui/devmode-annotate/note-format

Functions

buildQueue() from @llui/devmode-annotate/note-format

Materialize per-note current status from a flat transition log, newest touched first. One entry per note id that has ever transitioned; optionally filtered by status.

export declare function buildQueue(transitions: readonly StatusTransition[], filter?: {
    status?: NoteStatus | NoteStatus[];
}): QueueEntry[];
currentStatusFromHistory() from @llui/devmode-annotate/note-format

Current status for a note: last to, or null when it has no transitions.

export declare function currentStatusFromHistory(history: readonly StatusTransition[]): NoteStatus | null;
defaultSessionName() from @llui/devmode-annotate/note-format

Default UTC session folder name: session-YYYY-MM-DD-HHMM.

export declare function defaultSessionName(d: Date): string;
deriveFilename() from @llui/devmode-annotate/note-format
export declare function deriveFilename(id: string, author: Author, kind: NoteKind, slug: string): string;
deriveSlug() from @llui/devmode-annotate/note-format
export declare function deriveSlug(prose: string): string;
nextId() from @llui/devmode-annotate/note-format

The next id given the ids already present (handles gaps): padId(max+1).

export declare function nextId(existingIds: readonly number[]): string;
padId() from @llui/devmode-annotate/note-format

3-digit zero-padded session-local sequence id (001, 002, … then 1000+).

export declare function padId(n: number): string;
parseFilename() from @llui/devmode-annotate/note-format
export declare function parseFilename(filename: string): ParsedFilename | null;
preview() from @llui/devmode-annotate/note-format

One-line preview of prose for note summaries.

export declare function preview(prose: string, max?: number): string;

Interfaces

ParsedFilename from @llui/devmode-annotate/note-format
export interface ParsedFilename {
    id: string;
    idNum: number;
    author: Author;
    kind: NoteKind;
    slug: string;
}
QueueEntry from @llui/devmode-annotate/note-format
export interface QueueEntry {
    noteId: string;
    status: NoteStatus;
    transitions: StatusTransition[];
}

Constants

NOTE_FILENAME_RE from @llui/devmode-annotate/note-format
const NOTE_FILENAME_RE: RegExp
NOTE_SCHEMA_VERSION from @llui/devmode-annotate/note-format

On-disk note-format schema version. Stamped into export bundles and checked on dev import so a producer and consumer never silently disagree. v2 = the current "body under a body: frontmatter key" format (v1 was the legacy trailing-```json fence, still readable by parseNote).

const NOTE_SCHEMA_VERSION

@llui/devmode-annotate/note-serialize

Functions

parseNote() from @llui/devmode-annotate/note-serialize
export declare function parseNote(markdown: string): SerializedNote;
serializeNote() from @llui/devmode-annotate/note-serialize
export declare function serializeNote(note: SerializedNote): string;

Interfaces

SerializedNote from @llui/devmode-annotate/note-serialize
export interface SerializedNote {
    frontmatter: NoteFrontmatter;
    prose: string;
    body: NoteBody;
}

@llui/devmode-annotate/install

Functions

installAnnotateHud() from @llui/devmode-annotate/install

Install the HUD lazily behind an activation trigger. Intended for live apps: the host calls this (behind its own authorization), and the HUD code only loads when a user activates it. Defaults to allowProduction: true since the host is opting in deliberately.

function installAnnotateHud(opts: InstallAnnotateOptions = {}): AnnotateHudInstaller

Interfaces

AnnotateHudInstaller from @llui/devmode-annotate/install
export interface AnnotateHudInstaller {
  /** Lazily import + mount the HUD (idempotent — repeat calls return the same
   *  handle). Resolves to the live handle. */
  activate(): Promise<AnnotateHudHandle>
  /** Remove the bootstrap trigger listener. Does not unmount a HUD that has
   *  already been activated. */
  dispose(): void
}
InstallAnnotateOptions from @llui/devmode-annotate/install
export interface InstallAnnotateOptions extends MountAnnotateOptions {
  /** Register the Cmd/Ctrl+Shift+A keyboard trigger that lazily loads + opens
   *  the HUD. Default true. Set false to drive activation yourself via
   *  `activate()`. */
  trigger?: boolean
}

@llui/devmode-annotate/editor

Functions

registerAnnotateEditor() from @llui/devmode-annotate/editor

Register an optional editor implementation. The returned disposer restores the previous registration, which makes temporary host overrides safe.

function registerAnnotateEditor(editor: AnnotateEditorRegistration): () => void
registeredAnnotateEditor() from @llui/devmode-annotate/editor

The editor registration captured by the next HUD mount, if any.

function registeredAnnotateEditor(): AnnotateEditorRegistration | null

Interfaces

AnnotateEditorInstance from @llui/devmode-annotate/editor
export interface AnnotateEditorInstance {
  getValue(): string
  setValue(value: string): void
  focus(): void
  dispose(): void
}
AnnotateEditorMountOptions from @llui/devmode-annotate/editor

Optional note-editor seam for the HUD.

Core owns the note value and keyboard behavior. An editor package owns only the live editing surface, so the core package never needs to import its implementation (or any of that implementation's dependencies).

export interface AnnotateEditorMountOptions {
  host: HTMLElement
  initialValue: string
  placeholder: string
  onChange(value: string): void
}
AnnotateEditorRegistration from @llui/devmode-annotate/editor
export interface AnnotateEditorRegistration {
  /** Human-readable help rendered below the editor surface. */
  hint: string
  /** Styles adopted into the HUD shadow root when isolation is enabled. */
  shadowCss?: string
  mount(options: AnnotateEditorMountOptions): AnnotateEditorInstance
}

@llui/devmode-annotate/stores

Functions

devServerStore() from @llui/devmode-annotate/stores

Build the dev-server-backed store rooted at origin (e.g. location.origin). When a taskCapabilityToken is supplied (injected by @llui/vite-plugin when the attention router is enabled), it's sent as the x-llui-task-capability header so the middleware can trust an in-HUD task submission.

function devServerStore(origin: string, taskCapabilityToken?: string): NotesStore
httpStore() from @llui/devmode-annotate/stores

A NotesStore that talks to a host-provided HTTP backend. Use in production when a team wants centralized capture instead of manual export/import. The backend must speak the notebook wire protocol (the same shapes the dev server serves under /_llui).

function httpStore(opts: HttpStoreOptions): NotesStore
indexedDbStore() from @llui/devmode-annotate/stores

Build a browser-local NotesStore backed by IndexedDB. No dev server required; the HUD captures, persists, and browses entirely client-side.

function indexedDbStore(opts: IndexedDbStoreOptions = {}): NotesStore & ExportableStore

Types

HeadersInput from @llui/devmode-annotate/stores

Static headers, or a (sync/async) function called per request so tokens can refresh.

export type HeadersInput =
  | Record<string, string>
  | (() => Record<string, string> | Promise<Record<string, string>>)

Interfaces

EventSubscription from @llui/devmode-annotate/stores

Live-event subscription parameters.

export interface EventSubscription {
  role: SseRole
  onEvent: (event: ServerEvent) => void
  onError?: (err: unknown) => void
}
ExportableStore from @llui/devmode-annotate/stores

A store that can produce its notebook as raw on-disk-format entries, for export into a zip bundle. Browser stores implement this; the dev-server store doesn't need to (its files already live on disk).

export interface ExportableStore {
  exportSessions(sessionIds?: string[]): Promise<RawSession[]>
}
FullNote from @llui/devmode-annotate/stores

A note fetched in full (the format=json shape). Frontmatter/body are intentionally loose — consumers narrow what they read. Concrete NoteFrontmatter/NoteBody values assign here (they carry these fields and more); the dev-server adapter also fills it from raw server JSON.

export interface FullNote {
  frontmatter: {
    kind: string
    author: string
    intent?: string
    screenshot?: string | null
  }
  prose: string
  body?: { repro?: unknown[] }
}
HttpStoreOptions from @llui/devmode-annotate/stores
export interface HttpStoreOptions {
  /** Base URL the host's notebook backend lives under, no trailing slash. */
  baseUrl: string
  /** Headers injected on every request (e.g. an auth token). Never bake
   *  credentials into the bundle — supply them here at mount time. */
  headers?: HeadersInput
  /** Override fetch (tests / custom transport). */
  fetch?: typeof fetch
}
IndexedDbStoreOptions from @llui/devmode-annotate/stores
export interface IndexedDbStoreOptions {
  /** IndexedDB database name. Default `llui-devmode-annotate`. */
  dbName?: string
  /** Clock override (tests / deterministic runs). Default `() => new Date()`. */
  now?: () => Date
}
NotesStore from @llui/devmode-annotate/stores

The transport the HUD reads and writes through. Methods reject on failure; callers keep their own try/catch and best-effort semantics.

export interface NotesStore {
  /** Create a note (text/rect/capture/reply, or a task). */
  createNote(req: CreateNoteRequest): Promise<CreateNoteResponse>

  /** List all sessions, newest first. */
  listSessions(): Promise<SessionSummary[]>

  /** The session the store is currently writing into. */
  currentSession(): Promise<CurrentSessionResponse>

  /** Summaries of the notes in a session. */
  listNotes(query: ListNotesQuery): Promise<ListNotesResponse>

  /** A single note in full, or null if it can't be read. */
  readNote(id: string, sessionId: string): Promise<FullNote | null>

  /** A note's current status + transition history. */
  getStatus(id: string, sessionId: string): Promise<NoteStatusResponse>

  /** The task queue for a session. */
  getQueue(sessionId: string): Promise<QueueResponse>

  /** Delete a note. */
  deleteNote(id: string, sessionId: string): Promise<void>

  /** Patch a note (currently prose only). */
  updateNote(id: string, sessionId: string, update: NoteUpdate): Promise<void>

  /** Request a status transition for a note. */
  postStatus(id: string, sessionId: string, update: StatusUpdate): Promise<void>

  /** A URL usable directly as an `<img src>` for a note's screenshot.
   *  `screenshotRef` is the frontmatter `screenshot` value. Synchronous so
   *  it can be read inside a reactive view binding. */
  screenshotUrl(id: string, screenshotRef: string): string

  /** Subscribe to live notebook events. Returns an unsubscribe function.
   *  A noop subscription (returning a noop unsubscribe) is valid when the
   *  store has no live channel. */
  subscribeEvents(sub: EventSubscription): () => void

  /** Release everything the store holds outside the JS heap — object URLs,
   *  open connections — so a HUD mount/destroy cycle reclaims it. Idempotent.
   *
   *  The store stays USABLE (a later call lazily re-creates what it needs),
   *  but this is not a no-op for anything already handed out: it is the
   *  OWNER's teardown, and what it releases can belong to someone else.
   *  `indexedDbStore` revokes every object URL `screenshotUrl` returned, so an
   *  `<img>` the host is still displaying goes blank; `httpStore` closes every
   *  live `EventSource`, so ANOTHER subscriber's `onEvent` goes silently dead
   *  and nothing re-opens it.
   *
   *  This matters because `mountAnnotateHud` calls it from `destroy()` on the
   *  store it was GIVEN — the HUD cannot leave an injected store undisposed
   *  without re-opening the leak this exists to close (an inline
   *  `installAnnotateHud({ store: indexedDbStore() })` keeps no reference the
   *  host could dispose, and object URLs are not garbage-collected). So a host
   *  that also uses the store from its own code should construct a SECOND
   *  instance for the HUD; they are cheap and share the backing store.
   *
   *  Required on the port rather than optional so the compiler names it for
   *  every adapter (a store with nothing to release implements a no-op) —
   *  though that is a type-level guard only: a `as unknown as NotesStore` test
   *  fake still reaches destroy() and throws there. */
  dispose(): void
}
NoteStatusResponse from @llui/devmode-annotate/stores

Status sidecar for a single note: its current status + transition log.

export interface NoteStatusResponse {
  current: NoteStatus | null
  history: StatusTransition[]
}
NoteUpdate from @llui/devmode-annotate/stores

A mutable patch to an existing note (PATCH).

export interface NoteUpdate {
  prose?: string
}
QueueEntry from @llui/devmode-annotate/stores

A note's place in the task queue.

export interface QueueEntry {
  noteId: string
  status: NoteStatus
}
QueueResponse from @llui/devmode-annotate/stores
export interface QueueResponse {
  queue: QueueEntry[]
}
RawNote from @llui/devmode-annotate/stores

One note in raw export form: its serialized .md plus optional screenshot.

export interface RawNote {
  /** The `.md` filename (canonical `{id}-{author}-{kind}-{slug}.md`). */
  filename: string
  /** Serialized note markdown (YAML frontmatter + prose). */
  markdown: string
  /** Screenshot bytes (PNG), or null when the note has none. */
  screenshot: Uint8Array | null
}
RawSession from @llui/devmode-annotate/stores

One session in raw export form.

export interface RawSession {
  id: string
  notes: RawNote[]
  /** `status.jsonl` content (one JSON transition per line; '' when empty). */
  statusJsonl: string
}
SessionSummary from @llui/devmode-annotate/stores

One session as returned by the session list.

export interface SessionSummary {
  id: string
  noteCount: number
  startedAt?: string
}
StatusUpdate from @llui/devmode-annotate/stores

A status transition the HUD requests (POST status).

export interface StatusUpdate {
  to: NoteStatus
  by: Author | 'system'
  reason?: string
}

Constants

SCREENSHOT_URL_CACHE_LIMIT from @llui/devmode-annotate/stores

How many screenshot object URLs the store keeps alive at once. Each one pins a decoded PNG Blob, and only the open detail view displays a screenshot at a time — the cache exists so revisiting a recently browsed note is instant, not so the whole notebook stays decoded in memory.

const SCREENSHOT_URL_CACHE_LIMIT
  • @llui/vite-plugin — the dev-server middleware that backs every HUD HTTP call. See devmodeAnnotate config.
  • @llui/mcp — the LLM-facing side of the same notebook.
  • Proposal: docs/proposals/devmode-annotate/ — full on-disk format spec, middleware contract, MCP surface, runtime-hook plan, and task-mode design.