@llui/vite-plugin

Current package version: 0.12.1

Vite adapter for LLui. Wires the @llui/compiler signal transform into Vite — lowering signal expressions in component views to runtime helpers, emitting introspection metadata, and surfacing the signal lint rules as non-bypassable build errors. There is no virtual DOM.

pnpm add -D @llui/vite-plugin

Setup

// vite.config.ts
import { defineConfig } from 'vite'
import llui from '@llui/vite-plugin'

export default defineConfig({
  plugins: [llui()],
})

Options

llui({
  mcpPort: 5200, // MCP debug server port (default: 5200, false to disable)
})

What It Does

The plugin runs a single signal transform (@llui/compiler) over every .ts/.tsx file using the TypeScript Compiler API:

Step What it does
View lowering Lowers signal expressions in a component's DIRECT view to runtime helpers (signalText / el / react / signalEach / signalShow / signalBranch / …). An optimization — anything it can't lower runs via the runtime authoring helpers, so both forms coexist.
Introspection metadata Emits component / msg / state metadata (and, via opt-in compiler modules, agent schemas and devtools __componentMeta).
Lint rules Runs the signal lint set as non-bypassable build errors (surfaced via this.error()): peek-in-slot, operator-on-signal, pure-derive-body, no-node-construction-in-body, empty-props, plus shared cross-file / agent / convention checks.

Diagnostics

The compiler emits warnings for common issues:

Diagnostic Description
Missing alt attribute Accessibility: img without alt
Non-exhaustive update update() switch missing msg type cases
Empty props Element helper called with empty props object
Namespace imports import * as prevents tree-shaking
Spread children Spread in children array defeats static analysis

Functions

resolveRouterInput()

Normalize the user's router setting into the public LlmRouterConfig shape (or null when disabled). Accepts false, a preset string, or a full config object. Used in configResolved so the rest of the plugin (router startup + HUD bootstrap) sees one canonical shape.

OPT-IN by default: an unset router resolves to null (disabled). The attention router auto-spawns an LLM CLI (with tool access) in the project root, so it must never turn on implicitly — a forgeable same-origin/loopback task note reaching a default-on router is a local-RCE path. Enabling it requires an explicit router: 'claude' (or a full config object).

function resolveRouterInput(router: false | LlmPreset | LlmRouterConfig | undefined, legacyTimeoutMs: number | undefined): LlmRouterConfig | null

Types

AgentPluginConfig

Reserved for future agent-server config. Empty today — opaque tokens (post-0.0.35) need no signing key, and the dev server hard-codes the identity resolver to 'dev-user'. The shape is kept so callers can pass agent: { ... } and we can grow options without churning the public type.

export type AgentPluginConfig = Record<string, never>

Annotation

export type Annotation = ({
    type: 'rect';
} & NoteRect & {
    label?: string;
}) | {
    type: 'element';
    selector: string;
    bbox: NoteRect;
    label?: string;
};

Author

export type Author = 'human' | 'llm';

CaptureLevel

export type CaptureLevel = 'standard' | 'verbose';

LogLevel

export type LogLevel = 'log' | 'warn' | 'error' | 'info' | 'debug';

NoteIntent

export type NoteIntent = 'task' | 'note';

NoteKind

export type NoteKind = 'rect' | 'element' | 'text' | 'capture' | 'reply';

NoteStatus

export type NoteStatus = 'open' | 'claimed' | 'in-progress' | 'proposed' | 'accepted' | 'applied' | 'rejected' | 'wontfix' | 'failed';

ServerEvent

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

export type SseRole = 'hud' | 'mcp' | 'viewer';

Interfaces

AgentSchemaSummary

export interface AgentSchemaSummary {
    msg: string;
    fields: Record<string, string>;
}

CaptureRequestPayload

export interface CaptureRequestPayload {
    route?: string;
    url?: string;
    selector?: string;
    annotate?: Annotation[];
    prose?: string;
    waitForMessage?: string;
    captureLevel?: CaptureLevel;
    timeoutMs?: number;
}

CaptureRequestResponse

export interface CaptureRequestResponse {
    requestId: string;
    status: 'fulfilled' | 'timeout' | 'no-client';
    note?: CreateNoteResponse;
}

ComponentMetaRef

export interface ComponentMetaRef {
    file: string;
    line: number;
    name: string;
}

ConsoleLogEntry

export interface ConsoleLogEntry {
    ts: string;
    level: LogLevel;
    text: string;
}

CreateNoteRequest

export interface CreateNoteRequest {
    body: string;
    frontmatter: Omit<NoteFrontmatter, 'id' | 'ts'>;
    noteBody: NoteBody;
    screenshot?: string;
}

CreateNoteResponse

export interface CreateNoteResponse {
    id: string;
    filename: string;
    path: string;
    sessionId: string;
}

CurrentSessionResponse

export interface CurrentSessionResponse {
    sessionId: string;
    startedAt: string;
    notesDir: string;
}

DevmodeAnnotateConfig

export interface DevmodeAnnotateConfig {
  /** Override the on-disk notes root. Relative paths resolve against
   *  the Vite project root. Default: `.llui/notes`. The
   *  `LLUI_NOTES_DIR` env var takes precedence if set. */
  notesDir?: string
  /**
   * Override session-folder naming and/or slug derivation. The
   * id+author+kind prefix of each filename stays fixed so id ordering
   * and filename parsing keep working — only the trailing slug and
   * the session folder name are customizable.
   *
   * ```ts
   * format: {
   *   formatSessionFolder: (d) => `session-${d.toISOString().slice(0, 10)}`,
   *   deriveSlug: (prose) =>
   *     prose.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 20).replace(/^-|-$/g, '') || 'capture',
   * }
   * ```
   *
   * Note: when the MCP server writes notes directly (out-of-process),
   * it uses defaults — only writes that go through the dev-server
   * middleware (the HUD path) honor these overrides.
   */
  format?: NoteFormatConfig
  /** Override the default capture-request long-poll timeout in
   *  milliseconds. The `LLUI_CAPTURE_TIMEOUT_MS` env var takes
   *  precedence if set. Default: 30000. */
  captureTimeoutMs?: number
  /**
   * The attention router auto-picks up task-mode notes (the developer
   * clicks "Solve" in the HUD) and spawns the configured LLM CLI to
   * propose a fix. Accepts:
   *
   *  - unset / `undefined` — DISABLED (the default). The router is
   *              OPT-IN: it spawns an LLM CLI with tool access in the
   *              project root, so it never turns on implicitly. Notes
   *              still save to disk; the HUD hides its "Solve" button.
   *  - `false` — explicitly disable (same effect as unset).
   *  - `'claude' | 'codex' | 'gemini'` — enable with a preset.
   *  - `LlmRouterConfig` — preset + overrides (model, timeoutMs,
   *              concurrency, env, extraArgs), or a fully custom
   *              invocation `{ command, args, promptVia }` (omit
   *              `preset` to opt out of preset defaults entirely). Set
   *              `dangerouslySkipPermissions: true` to run the agent
   *              fully unattended (no approval prompts) — dangerous.
   *
   * When the chosen CLI isn't on PATH the router degrades silently
   * to save-only and the HUD hides the Solve button — the user gets
   * a one-line install hint in the console.
   *
   * Default: disabled (opt-in).
   */
  router?: false | LlmPreset | LlmRouterConfig
  /** Override the per-task timeout for the router's spawn. Default
   *  5 minutes. Deprecated alias for `router.timeoutMs`. */
  routerTimeoutMs?: number
  /**
   * Controls the in-app HUD (`@llui/devmode-annotate`) auto-injection.
   *
   *  - `true` / omitted — inject in dev mode (default).
   *  - `false`          — skip injection. The notes API stays live so
   *                       MCP can still consume the notebook; only the
   *                       floating button + modal are skipped.
   *  - `HudInjectionConfig` — inject with forwarded options. Currently
   *                       supports `{ hidden: true }` to mount the HUD
   *                       programmatically (no floating button).
   *
   * Injection silently no-ops when `@llui/devmode-annotate` isn't
   * resolvable from the project root.
   */
  hud?: boolean | HudInjectionConfig
}

DirtyTraceEntry

export interface DirtyTraceEntry {
    component: string;
    pathsTracked: string[];
    mask: number;
    maskHi?: number;
    lastFlippedBits: string[];
}

HudInjectionConfig

export interface HudInjectionConfig {
  /** Mount the HUD without rendering the floating button. The
   *  keyboard shortcut + programmatic API still work. */
  hidden?: boolean
  /** When `true` (default), the HUD installs `window.onerror` +
   *  `unhandledrejection` listeners. On an uncaught error it opens
   *  the modal pre-populated with the stack + a screenshot — turns
   *  "I saw something weird but can't reproduce it" into a
   *  one-click solve. Set `false` to opt out of the listeners
   *  entirely. */
  autoCaptureOnError?: boolean
  /** When `true` (default), the HUD shows a "● Record" toggle that
   *  captures clicks/inputs/route-changes/messages between toggle-on
   *  and submit, attaching them to the note for the LLM to replay.
   *  Set `false` to hide the toggle and skip the listener setup. */
  repro?: boolean
  /** When `true` (default), the HUD exposes the "⌖ Pick element"
   *  annotation mode alongside "⌖ Add region". Set `false` to hide
   *  the picker affordance. */
  elementPick?: boolean
}

ListNotesQuery

export interface ListNotesQuery {
    sessionId?: string;
    author?: Author;
    kind?: NoteKind | NoteKind[];
    since?: string;
    limit?: number;
}

ListNotesResponse

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[];
}

LluiPluginOptions

export interface LluiPluginOptions {
  /**
   * Port for the MCP debug bridge. In dev mode, the runtime relay connects
   * to `ws://127.0.0.1:<port>` so an external `llui-mcp` server can forward
   * tool calls into the running app.
   *
   * When omitted, the plugin checks whether `@llui/mcp` is resolvable from
   * the Vite project root. If yes → defaults to `5200`. If no → stays
   * disabled. This means installing `@llui/mcp` (+ starting its server)
   * Just Works without an explicit config entry. Pass an explicit `false`
   * to opt out even when `@llui/mcp` is installed; pass a number to use
   * a non-default port. When enabled but the MCP server isn't running,
   * the plugin returns 404 from its discovery endpoint and the browser
   * silently skips the connection — no retry noise.
   */
  mcpPort?: number | false

  /**
   * Enables two things together when set:
   *
   *   1. Emits schemas + binding descriptors in prod builds so the
   *      @llui/agent runtime has metadata to advertise over its WS hello
   *      frame.
   *   2. Auto-mounts `@llui/agent/server`'s router at `/agent/*` and its
   *      WS upgrade handler at `/agent/ws` on the Vite dev server — so
   *      plain `vite dev` has working agent endpoints with no extra
   *      server.ts wiring. Requires `@llui/agent` installed; if it isn't,
   *      the plugin warns and skips dev mounting (prod emission still
   *      works from Plan 3b).
   *
   * Pass `true` for defaults (random signing key per dev session;
   * `identityResolver` returns `'dev-user'`). Pass an object to customize.
   * Default `false` — metadata is dev-only, no agent endpoints.
   */
  agent?: boolean | AgentPluginConfig

  /**
   * Whether any component in the app uses `each()`'s `enter` / `leave`
   * / `onTransition` options. When `false` (the default), the
   * vite-plugin substitutes `__LLUI_TRANSITIONS__ = false` into the
   * runtime bundle; Vite's dead-code eliminator then drops the
   * per-entry enter/leave helpers, the `leaving` queue plumbing, and
   * the `report` allocation in `each()`'s reconcile path. Saves
   * ~0.3 kB gz on jfb-shape bundles that don't animate.
   *
   * Apps using `@llui/transitions` or any custom `each({ enter, leave,
   * onTransition })` MUST pass `transitions: true` — otherwise the
   * options will be silently ignored at runtime.
   */
  transitions?: boolean

  /**
   * Surface compiler `perf` diagnostics as Vite warnings. Currently one
   * diagnostic exists: `llui/each-verbatim` — an `each` whose rows did not
   * compile to the cloneNode RowFactory (nor the render-callback lowering)
   * and render via the runtime authoring path instead, paying per-row
   * construction overhead. The message names the bail reason(s) with an
   * actionable hint (e.g. a row delegating to an imported helper, spread
   * connect-part props, an imperative render body).
   *
   * Advisory only — never blocks the build (a verbatim `each` is fully
   * correct, just slower per row). **Default: on in dev mode, off in
   * build.** Pass `false` to silence, `true` to also warn during builds.
   */
  perfDiagnostics?: boolean

  /**
   * Controls the devmode-annotate notebook surface — a single Connect
   * middleware mounted at `/_llui/*` that lets the HUD
   * (`@llui/devmode-annotate`) and the MCP server (`@llui/mcp`) read
   * and write a shared on-disk notebook under `.llui/notes/`. The HUD
   * developer drops notes from the running app; the LLM consumes them
   * via MCP subscriptions; both can initiate captures.
   *
   * **Default: on in dev mode.** Omitting the option (or passing `true`)
   * registers the middleware automatically — there's nothing to do.
   * Pass `false` to opt out (no routes registered, middleware tree-
   * shakes). Pass an object to keep it on while customizing the notes
   * directory or default timeout.
   *
   * The HUD is **auto-injected** in dev mode: the plugin emits a
   * `<script type="module">` into the served HTML that imports
   * `@llui/devmode-annotate` and mounts the floating button. Production
   * builds never run `configureServer` or `transformIndexHtml(dev)`, so
   * this is dev-only by construction. Disable just the HUD (keeping the
   * notes API on) with `devmodeAnnotate: { hud: false }`; disable
   * everything with `devmodeAnnotate: false`. The HUD package must be
   * resolvable from the project root — install
   * `@llui/devmode-annotate` alongside `@llui/vite-plugin`.
   *
   * Environment overrides (honored when not opted out):
   *   - `LLUI_NOTES_DIR` — override the notes root path
   *   - `LLUI_CAPTURE_TIMEOUT_MS` — override the default capture-request timeout
   *
   * The proposal (`docs/proposals/devmode-annotate/`) details what
   * lands on disk and what the LLM gets.
   */
  devmodeAnnotate?: boolean | DevmodeAnnotateConfig
}

MessageLogEntry

export interface MessageLogEntry {
    ts: string;
    component: string;
    msg: unknown;
}

NoteBody

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

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

export interface NoteRect {
    x: number;
    y: number;
    w: number;
    h: number;
}

NoteSummary

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

export interface PendingEffectEntry {
    id: string;
    component: string;
    effect: unknown;
    sinceMs: number;
}

PendingMessage

export interface PendingMessage {
    component: string;
    msg: unknown;
}

ProposedDiff

export interface ProposedDiff {
    files: Array<{
        path: string;
        patch: string;
    }>;
    summary: string;
    confidence: 'high' | 'medium' | 'low';
}

RecentEffectEntry

export interface RecentEffectEntry {
    ts: string;
    component: string;
    effect: unknown;
    outcome: 'ok' | 'error' | 'cancelled';
    error?: string;
}

RuntimeErrorEntry

export interface RuntimeErrorEntry {
    ts: string;
    kind: 'runtime' | 'compiler';
    file?: string;
    line?: number;
    message: string;
    stack?: string;
}

SourceMapEntry

export interface SourceMapEntry {
    selector: string;
    file: string;
    line: number;
    componentPath: string[];
}

StatusTransition

export interface StatusTransition {
    ts: string;
    noteId: string;
    from: NoteStatus | null;
    to: NoteStatus;
    by: Author | 'system';
    reason?: string;
}

StructuralSnapshot

export interface StructuralSnapshot {
    branches: Array<{
        at: string;
        activeArm: string;
    }>;
    shows: Array<{
        at: string;
        visible: boolean;
    }>;
    eachKeys: Array<{
        at: string;
        keys: string[];
    }>;
}

VerboseNoteBody

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;
    }>;
}

Public Entry Points

@llui/vite-plugin/notes

Functions

acquireClaimLock() from @llui/vite-plugin/notes

Acquire an exclusive claim on a note via an O_CREAT | O_EXCL lock file (<noteId>.claim) — the arbiter for cross-process claiming. Exactly ONE caller wins the exclusive create; every other caller gets EEXIST and reads the winner's workerId back out of the file. This closes the read-then-append TOCTOU where two workers both observed open status and both appended a claimed transition.

The lock file is a permanent record (it is NOT released) so a later claim of the same note is reported as already-claimed by the recorded holder.

function acquireClaimLock(sessionDir: string, noteId: string, workerId: string): ClaimLockOutcome
appendStatus() from @llui/vite-plugin/notes

Append a transition to status.jsonl. The file is created on first append. from is the current status (or null on the first transition for this note) so the line is self-describing.

function appendStatus(sessionDir: string, transition: StatusTransition): void
checkSameOriginLoopback() from @llui/vite-plugin/notes

Reject any mutating request that isn't a same-origin call to a loopback host. Returns null when the request is allowed, or an error message describing the rejection (the caller answers with 403).

function checkSameOriginLoopback(req: IncomingMessage): string | null
cleanupResolvedTask() from @llui/vite-plugin/notes

Delete a resolved task note's files: the task .md + .png plus every reply note (replyTo === taskNoteId) and their screenshots. The status.jsonl audit log is preserved — those transitions stay as a trail of what happened.

Used by the middleware when a task transitions to applied (the success path). Idempotent: missing files are skipped silently.

Returns the list of deleted filenames (sans the session dir prefix) so the caller can log/broadcast.

function cleanupResolvedTask(notesRoot: string, sessionId: string, taskNoteId: string): string[]
createCaptureRegistry() from @llui/vite-plugin/notes
function createCaptureRegistry(): CaptureRegistry
createEventBus() from @llui/vite-plugin/notes
function createEventBus(): EventBus
createNote() from @llui/vite-plugin/notes
function createNote(notesRoot: string, req: CreateNoteRequest, format: NoteFormatConfig = {}): CreateNoteResponse
createNotesMiddleware() from @llui/vite-plugin/notes
function createNotesMiddleware(config: NotesMiddlewareConfig): MiddlewareHandler
createTrustedTaskRegistry() from @llui/vite-plugin/notes
function createTrustedTaskRegistry(): TrustedTaskRegistry
currentStatus() from @llui/vite-plugin/notes

Current status for a note: last to value, or null when no transitions exist (= the note hasn't entered the status machine).

function currentStatus(sessionDir: string, noteId: string): NoteStatus | null
defaultSessionName() from @llui/vite-plugin/notes

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

export declare function defaultSessionName(d: Date): string;
deleteNote() from @llui/vite-plugin/notes

Delete a note: the .md file + its sibling .png screenshot (if any). Returns the list of paths actually removed. Idempotent — missing files are skipped. The session-wide status.jsonl is intentionally left alone; orphan transitions for the deleted note are harmless since downstream readers filter by id.

function deleteNote(notesRoot: string, sessionId: string, id: string): string[]
deriveFilename() from @llui/vite-plugin/notes
export declare function deriveFilename(id: string, author: Author, kind: NoteKind, slug: string): string;
deriveSlug() from @llui/vite-plugin/notes
export declare function deriveSlug(prose: string): string;
ensureNotesRoot() from @llui/vite-plugin/notes

Ensures a directory exists, no-op if already present. Used by callers that want to materialize the notes root before any note is written (e.g. middleware startup).

function ensureNotesRoot(notesRoot: string): void
ensureSession() from @llui/vite-plugin/notes

Ensure a session subdirectory exists. Does NOT touch the marker file — use rotateSession or resolveCurrentSession for that.

function ensureSession(notesRoot: string, sessionId: string): string
importBundle() from @llui/vite-plugin/notes

Import an export bundle (zip bytes) into notesRoot. Returns a summary; throws on a malformed bundle, schema mismatch, or unsafe entry paths.

function importBundle(notesRoot: string, zip: Uint8Array, options: ImportBundleOptions = {}): ImportBundleResult
isClaudeAvailable() from @llui/vite-plugin/notes

Back-compat — prefer isCliAvailable('claude').

function isClaudeAvailable(): boolean
isJsonContentType() from @llui/vite-plugin/notes

Whether the request declares a JSON body.

function isJsonContentType(req: IncomingMessage): boolean
listNotes() from @llui/vite-plugin/notes
function listNotes(notesRoot: string, query: ListNotesQuery): ListNotesResponse
listQueue() from @llui/vite-plugin/notes

Materialize per-note status by replaying every transition. Returns one entry per noteId that has ever been touched; filter by status via filter.

function listQueue(sessionDir: string, filter?: { status?: NoteStatus | NoteStatus[] }): QueueEntry[]
listSessions() from @llui/vite-plugin/notes
function listSessions(notesRoot: string): SessionListEntry[]
padId() from @llui/vite-plugin/notes

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

export declare function padId(n: number): string;
parseNote() from @llui/vite-plugin/notes
export declare function parseNote(markdown: string): SerializedNote;
readAllTransitions() from @llui/vite-plugin/notes

Read every transition in the session log, regardless of note id. Used by listQueue() to materialize the current status of all notes.

function readAllTransitions(sessionDir: string): StatusTransition[]
readCurrentSessionFile() from @llui/vite-plugin/notes
function readCurrentSessionFile(notesRoot: string): string | null
readNote() from @llui/vite-plugin/notes
function readNote(notesRoot: string, sessionId: string, id: string): SerializedNote
readScreenshot() from @llui/vite-plugin/notes
function readScreenshot(notesRoot: string, sessionId: string, id: string): Buffer | null
readStatusHistory() from @llui/vite-plugin/notes

Read the full status history for a single note id, in chronological order. Empty array when the file doesn't exist or the note has no transitions.

function readStatusHistory(sessionDir: string, noteId: string): StatusTransition[]
resolveCurrentSession() from @llui/vite-plugin/notes

Resolve the current session: reuse the one named by current-session if present, otherwise mint a new one from defaultSessionName(now()) (or the env override).

function resolveCurrentSession(notesRoot: string, opts: ResolveSessionOptions = {}): SessionInfo
rotateSession() from @llui/vite-plugin/notes

Start a fresh session. The previous session is left on disk; only the marker moves.

function rotateSession(notesRoot: string, opts: ResolveSessionOptions = {}): RotatedSession
serializeNote() from @llui/vite-plugin/notes
export declare function serializeNote(note: SerializedNote): string;
startRouter() from @llui/vite-plugin/notes

Start the attention router. Subscribes to the bus and processes note-created events for task-intent notes. Returns a handle that can stop the router and probe state for tests.

function startRouter(config: RouterConfig): RouterHandle
updateNoteProse() from @llui/vite-plugin/notes

Replace a note's prose, keeping its frontmatter intact. Returns the updated SerializedNote. Throws when the note doesn't exist. The status-history JSONL sidecar is untouched — edits don't reset task state.

function updateNoteProse(notesRoot: string, sessionId: string, id: string, newProse: string): SerializedNote

Types

Annotation from @llui/vite-plugin/notes
export type Annotation = ({
    type: 'rect';
} & NoteRect & {
    label?: string;
}) | {
    type: 'element';
    selector: string;
    bbox: NoteRect;
    label?: string;
};
Author from @llui/vite-plugin/notes
export type Author = 'human' | 'llm';
CaptureLevel from @llui/vite-plugin/notes
export type CaptureLevel = 'standard' | 'verbose';
ClaudeSpawner from @llui/vite-plugin/notes
export type ClaudeSpawner = LlmSpawner
ClaudeSpawnResult from @llui/vite-plugin/notes
export type ClaudeSpawnResult = LlmSpawnResult
LogLevel from @llui/vite-plugin/notes
export type LogLevel = 'log' | 'warn' | 'error' | 'info' | 'debug';
MiddlewareHandler from @llui/vite-plugin/notes
export type MiddlewareHandler = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: unknown) => void,
) => void
NoteIntent from @llui/vite-plugin/notes
export type NoteIntent = 'task' | 'note';
NoteKind from @llui/vite-plugin/notes
export type NoteKind = 'rect' | 'element' | 'text' | 'capture' | 'reply';
NoteStatus from @llui/vite-plugin/notes
export type NoteStatus = 'open' | 'claimed' | 'in-progress' | 'proposed' | 'accepted' | 'applied' | 'rejected' | 'wontfix' | 'failed';
ServerEvent from @llui/vite-plugin/notes
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;
};
SseEventListener from @llui/vite-plugin/notes
export type SseEventListener = (event: ServerEvent) => void
SseRole from @llui/vite-plugin/notes
export type SseRole = 'hud' | 'mcp' | 'viewer';

Interfaces

AgentSchemaSummary from @llui/vite-plugin/notes
export interface AgentSchemaSummary {
    msg: string;
    fields: Record<string, string>;
}
CaptureRegistry from @llui/vite-plugin/notes
export interface CaptureRegistry {
  submit(payload: CaptureRequestPayload, opts: SubmitOptions): SubmitResult
  fulfill(requestId: string, note: CreateNoteResponse): boolean
  cancel(requestId: string, status: 'timeout' | 'no-client'): boolean
  listPending(): string[]
}
CaptureRequestPayload from @llui/vite-plugin/notes
export interface CaptureRequestPayload {
    route?: string;
    url?: string;
    selector?: string;
    annotate?: Annotation[];
    prose?: string;
    waitForMessage?: string;
    captureLevel?: CaptureLevel;
    timeoutMs?: number;
}
CaptureRequestResponse from @llui/vite-plugin/notes
export interface CaptureRequestResponse {
    requestId: string;
    status: 'fulfilled' | 'timeout' | 'no-client';
    note?: CreateNoteResponse;
}
CaptureSubmitOptions from @llui/vite-plugin/notes
export interface SubmitOptions {
  /** Whether a HUD is currently subscribed. If false, the promise
   *  resolves immediately with status:no-client so the MCP server can
   *  fall through to the Playwright fallback. */
  hudConnected: boolean
  /** ms before the promise resolves with status:timeout. */
  timeoutMs: number
}
CaptureSubmitResult from @llui/vite-plugin/notes
export interface SubmitResult {
  requestId: string
  promise: Promise<CaptureRequestResponse>
  payload: CaptureRequestPayload
}
ClaimLockOutcome from @llui/vite-plugin/notes
export interface ClaimLockOutcome {
  /** True only for the single caller that created the lock file. */
  acquired: boolean
  /** The workerId recorded in the lock file — our own id when we won, the
   *  prior winner's id when we lost. `null` when the lock is unreadable. */
  holder: string | null
}
ComponentMetaRef from @llui/vite-plugin/notes
export interface ComponentMetaRef {
    file: string;
    line: number;
    name: string;
}
ConsoleLogEntry from @llui/vite-plugin/notes
export interface ConsoleLogEntry {
    ts: string;
    level: LogLevel;
    text: string;
}
CreateNoteRequest from @llui/vite-plugin/notes
export interface CreateNoteRequest {
    body: string;
    frontmatter: Omit<NoteFrontmatter, 'id' | 'ts'>;
    noteBody: NoteBody;
    screenshot?: string;
}
CreateNoteResponse from @llui/vite-plugin/notes
export interface CreateNoteResponse {
    id: string;
    filename: string;
    path: string;
    sessionId: string;
}
CurrentSessionResponse from @llui/vite-plugin/notes
export interface CurrentSessionResponse {
    sessionId: string;
    startedAt: string;
    notesDir: string;
}
DirtyTraceEntry from @llui/vite-plugin/notes
export interface DirtyTraceEntry {
    component: string;
    pathsTracked: string[];
    mask: number;
    maskHi?: number;
    lastFlippedBits: string[];
}
EventBus from @llui/vite-plugin/notes
export interface EventBus {
  subscribe(role: SseRole, listener: SseEventListener): () => void
  broadcast(event: ServerEvent): void
  countByRole(role: SseRole): number
}
ImportBundleResult from @llui/vite-plugin/notes
export interface ImportBundleResult {
  /** Short stable key derived from the bundle content hash. */
  bundleKey: string
  /** Target session folder names that received notes. */
  importedSessions: string[]
  /** `.md` notes written this run. */
  notesImported: number
  /** `.md` notes already present (idempotent re-import). */
  notesSkipped: number
}
ListNotesQuery from @llui/vite-plugin/notes
export interface ListNotesQuery {
    sessionId?: string;
    author?: Author;
    kind?: NoteKind | NoteKind[];
    since?: string;
    limit?: number;
}
ListNotesResponse from @llui/vite-plugin/notes
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/vite-plugin/notes
export interface MessageLogEntry {
    ts: string;
    component: string;
    msg: unknown;
}
NoteBody from @llui/vite-plugin/notes
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/vite-plugin/notes
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/vite-plugin/notes
export interface NoteRect {
    x: number;
    y: number;
    w: number;
    h: number;
}
NotesMiddlewareConfig from @llui/vite-plugin/notes
export interface NotesMiddlewareConfig {
  notesRoot: string
  bus: EventBus
  registry: CaptureRegistry
  defaultCaptureTimeoutMs?: number
  /** Heartbeat interval for SSE keepalive in ms. Default 15000. */
  sseHeartbeatMs?: number
  /** Override session-folder naming and/or slug derivation. */
  format?: NoteFormatConfig
  /**
   * Provenance registry for task-intent notes. When provided, a task note
   * accepted through this (same-origin, authenticated) middleware is marked
   * here — but ONLY when the request also presents the capability token (see
   * {@link taskCapabilityToken}) — so the attention router only spawns agents
   * for tasks the trusted in-page HUD actually created. Omit to skip
   * provenance recording (the router then falls back to on-disk intent —
   * dev/test only).
   */
  trustedTasks?: TrustedTaskRegistry
  /**
   * Per-launch, unforgeable capability token that authorizes marking a task
   * note trusted (which lets the router spawn a local CLI agent, possibly
   * with `--dangerously-skip-permissions` → local RCE). The plugin generates
   * it and injects it into the HUD bundle out-of-band; the HUD echoes it on
   * the task-create POST via the `x-llui-task-capability` header.
   *
   * SECURITY: same-origin passes the CSRF/loopback guard, so a malicious page
   * script CAN reach this endpoint. It cannot, however, read this token (it
   * lives in the HUD module's closure, never on `window` or in the DOM), so it
   * cannot forge a *trusted* task. Without a matching token the note is still
   * created and enters the status machine, but is NOT marked trusted, so the
   * router will not spawn for it. When unset, NO task is ever marked trusted
   * (secure default) — the plugin always sets it in real dev servers.
   */
  taskCapabilityToken?: string
}
NoteSummary from @llui/vite-plugin/notes
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/vite-plugin/notes
export interface PendingEffectEntry {
    id: string;
    component: string;
    effect: unknown;
    sinceMs: number;
}
PendingMessage from @llui/vite-plugin/notes
export interface PendingMessage {
    component: string;
    msg: unknown;
}
ProposedDiff from @llui/vite-plugin/notes
export interface ProposedDiff {
    files: Array<{
        path: string;
        patch: string;
    }>;
    summary: string;
    confidence: 'high' | 'medium' | 'low';
}
QueueEntry from @llui/vite-plugin/notes
export interface QueueEntry {
    noteId: string;
    status: NoteStatus;
    transitions: StatusTransition[];
}
RecentEffectEntry from @llui/vite-plugin/notes
export interface RecentEffectEntry {
    ts: string;
    component: string;
    effect: unknown;
    outcome: 'ok' | 'error' | 'cancelled';
    error?: string;
}
ResolveSessionOptions from @llui/vite-plugin/notes
export interface ResolveSessionOptions {
  /** Override for tests / fixed-seed runs. Defaults to `new Date()`. */
  now?: () => Date
  /** Override for env-based session names (LLUI_SESSION_NAME). */
  sessionName?: string
  /** Format the session folder name from the start date. Overrides
   *  the default UTC `session-YYYY-MM-DD-HHMM` scheme. Ignored when
   *  `sessionName` is explicitly set. */
  formatSessionFolder?: (date: Date) => string
}
RotatedSession from @llui/vite-plugin/notes
export interface RotatedSession extends SessionInfo {
  previousSessionId: string
}
RouterConfig from @llui/vite-plugin/notes

Resolved router config — what startRouter ultimately consumes. Either spawner is injected (tests / dependency inversion) OR the preset/custom fields drive a real child_process.

export interface RouterConfig {
  /** Path of the .llui/notes/ root. */
  notesRoot: string
  /** Working directory passed to the spawned CLI — the project root.
   *  The CLI inherits the project's `CLAUDE.md`, `.mcp.json`, etc. */
  projectRoot: string
  /** Event bus to subscribe to. */
  bus: EventBus
  /** Spawner override. When omitted, a default spawner is built from
   *  `preset` / `command` etc. */
  spawner?: LlmSpawner
  /** CLI preset. Default `'claude'`. Ignored when `spawner` is set. */
  preset?: LlmPreset
  /** Override the binary name (mostly useful for `'custom'` setups
   *  where there's no matching preset). Ignored when `spawner` is set. */
  command?: string
  /** Static args prepended before model + extraArgs + prompt. When
   *  unset and `preset` is given, the preset's args are used. */
  args?: string[]
  /** Model identifier (e.g. `'opus'`, `'gpt-5'`, `'gemini-2.5-pro'`).
   *  Mapped to the preset's modelFlag. */
  model?: string
  /** Extra args appended after preset args + model, before the prompt.
   *  Escape hatch for per-tool flags we haven't promoted. */
  extraArgs?: string[]
  /**
   * DANGEROUS, opt-in only. When `true`, append the active preset's
   * skip-permissions flag (e.g. claude's `--dangerously-skip-permissions`)
   * so the spawned agent runs fully unattended — no interactive approval
   * for file writes or shell commands in the project root. Off by default;
   * only enable when you understand that a task note can then drive
   * arbitrary local tool use without a human in the loop. Ignored for
   * presets/commands that expose no such flag.
   */
  dangerouslySkipPermissions?: boolean
  /** Extra env vars merged with `process.env`. */
  env?: Record<string, string>
  /** How the prompt reaches the CLI. Defaults per preset. */
  promptVia?: 'arg' | 'stdin'
  /** Per-task timeout in ms. Default 5 minutes. */
  timeoutMs?: number
  /** Number of tasks that may run concurrently. Default 1
   *  (serialized — avoids competing patches against the same files). */
  concurrency?: number
  /**
   * Project-relative paths to additional context files that get
   * inlined into every prompt the router sends, between the task body
   * and the reply-format instructions. Use this to surface project-
   * specific conventions, design notes, or scratch files the LLM
   * wouldn't otherwise see.
   *
   * Note: `claude --print` already auto-loads `CLAUDE.md` from the
   * project root (and nested CLAUDE.md per claude code's normal
   * resolution rules), so don't add it here. This config is for the
   * "ALSO show the model these files" case — e.g. a design doc, a
   * dependency-policy file, an API surface manifest. Files that don't
   * exist are skipped with a one-line warning.
   */
  contextFiles?: string[]
  /**
   * Live progress events during solve. When `true` (default), the
   * router parses claude's `--output-format stream-json` output as
   * lines arrive and broadcasts `task-progress` SSE events with
   * elapsed time, running token counts, and the last tool used. The
   * HUD surfaces this as a live status line so the user knows the
   * solve is working instead of stuck.
   *
   * Set `false` to fall back to the single-envelope `json` format —
   * less chatty on the wire but no in-flight feedback. Other presets
   * (codex, gemini) fall back to an elapsed-time-only heartbeat
   * regardless of this setting.
   */
  streaming?: boolean
  /**
   * Transform the prompt right before it's sent to the LLM. Runs
   * after `buildPrompt()` (which assembles the note + contextFiles)
   * and after any preset-specific layering. Use this to:
   *   - prepend a project-specific persona / policy block,
   *   - sanitize PII / secrets out of the prompt,
   *   - inject computed context (recent commits, open PRs, …) that
   *     contextFiles can't express because it's dynamic.
   * Receives the assembled prompt + the note being solved; returns
   * the prompt to actually send. May return a Promise.
   */
  beforePrompt?: (input: { prompt: string; note: NoteContext }) => string | Promise<string>
  /** Logger; defaults to stderr. */
  log?: (msg: string) => void
  /**
   * Provenance registry gating which task notes may spawn an agent. When
   * set, a `note-created` event only triggers a spawn if the note was
   * marked trusted (i.e. created through the authenticated same-origin
   * middleware). This prevents a note that reached disk by some other
   * path — a forged/dropped file, a stale mutation — from auto-spawning a
   * CLI agent in the project root. When omitted, the router falls back to
   * the on-disk `intent` field alone (dev/test convenience only).
   */
  trustedTasks?: TrustedTaskRegistry
}
RouterHandle from @llui/vite-plugin/notes
export interface RouterHandle {
  /** Stop the router. Currently-running task continues but no new
   *  tasks will be claimed. */
  stop(): void
  /** Number of tasks pending in the internal queue. Test affordance. */
  queueLength(): number
  /** Whether a task is currently being processed. Test affordance. */
  isBusy(): boolean
}
RuntimeErrorEntry from @llui/vite-plugin/notes
export interface RuntimeErrorEntry {
    ts: string;
    kind: 'runtime' | 'compiler';
    file?: string;
    line?: number;
    message: string;
    stack?: string;
}
SerializedNote from @llui/vite-plugin/notes
export interface SerializedNote {
    frontmatter: NoteFrontmatter;
    prose: string;
    body: NoteBody;
}
SessionInfo from @llui/vite-plugin/notes
export interface SessionInfo {
  sessionId: string
  /** ISO timestamp at session start (the resolution moment, not the dir mtime). */
  startedAt: string
  /** Absolute path to the session subdirectory. */
  notesDir: string
}
SourceMapEntry from @llui/vite-plugin/notes
export interface SourceMapEntry {
    selector: string;
    file: string;
    line: number;
    componentPath: string[];
}
StatusTransition from @llui/vite-plugin/notes
export interface StatusTransition {
    ts: string;
    noteId: string;
    from: NoteStatus | null;
    to: NoteStatus;
    by: Author | 'system';
    reason?: string;
}
StructuralSnapshot from @llui/vite-plugin/notes
export interface StructuralSnapshot {
    branches: Array<{
        at: string;
        activeArm: string;
    }>;
    shows: Array<{
        at: string;
        visible: boolean;
    }>;
    eachKeys: Array<{
        at: string;
        keys: string[];
    }>;
}
TrustedTaskRegistry from @llui/vite-plugin/notes
export interface TrustedTaskRegistry {
  /** Mark a task note as originating from an authenticated in-page write. */
  mark(sessionId: string, noteId: string): void
  /** Whether this task note was marked trusted. */
  isTrusted(sessionId: string, noteId: string): boolean
}
VerboseNoteBody from @llui/vite-plugin/notes
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;
    }>;
}

Constants

defaultClaudeSpawner from @llui/vite-plugin/notes

Back-compat: a spawner pre-bound to the 'claude' preset. Kept for existing call sites; new code should prefer createCliSpawner so preset, model, env etc. propagate consistently.

const defaultClaudeSpawner: LlmSpawner