ReferenceAPI

Core runtime

The core of the dd global: dd.ready and the init context, dd.request, dd.poll, dd.log, and how permission gating works on every call.

Every widget document gets one global, dd. The SDK script creates it:

<script src="/__sdk/widget-sdk.js"></script>

On load the SDK connects your document to the host, applies the design tokens to :root, and resolves dd.ready once the host has answered. Everything a widget can do (read settings, fetch data, stream system state) goes through dd; there is no other door to the system.

dd.ready

The init context, resolved once per document.

dd.ready: Promise<DdContext>

interface DdContext {
  instanceId: string;
  /** "main" = the widget document; "pane" = a popout pane document. */
  role: "main" | "pane";
  widget: { id: string; version: string };
  /** What the user granted: every permission key, plus http.hosts and
   *  links.prefixes. Mirrors the manifest after install-time consent. */
  grants: DdGrants;
  /** The manifest's interactive flag. Always false for panes. */
  interactive: boolean;
  /** Manifest defaults merged with the user's stored values. */
  settings: Record<string, unknown>;
  /** The current --dd-* token map. The SDK applies it for you. */
  tokens: Record<string, string>;
  /** Bridge API info: feature-detect against version; deprecated maps a
   *  method name to its deprecation note. */
  api?: { version: number; minSupported: number; deprecated: Record<string, string> };
  /** Static for the whole app run; absent on older hosts (treat as off). */
  perf?: { lowSpec: boolean };
  /** Pane documents only: the applied side and the data open() passed. */
  popout?: { side: "down" | "up" | "left" | "right"; data: unknown };
}

Notes:

  • perf.lowSpec is true when the user enabled low performance mode. It never changes mid-run, so read it once. Widgets that animate should cap their draw loop around 30 fps and their canvas backing store at 1x devicePixelRatio; the host already lowers its own side (spectrum frames arrive at 20 Hz instead of 30).
  • A pane document (see popouts) boots the SDK exactly like the main document and carries the same instance identity and grants.

dd.request()

The low-level escape hatch. Every namespaced method wraps it; call it directly only for something the typed surface does not cover.

dd.request<T>(method: string, params?: unknown, timeoutMs?: number): Promise<T>

Requests reject with an Error carrying a code:

interface DdError extends Error {
  code: string; // "PERMISSION_DENIED", "RATE_LIMITED", ...
}

The full code list is on Errors and versioning. The client rejects with TIMEOUT after 15 seconds by default; timeoutMs: 0 disables the client timer (the dialog pickers use this, since they wait on the user).

dd.poll()

The polling loop widgets keep re-implementing: an interval, error backoff, and staleness, so a settings-driven refresh() cancels the in-flight cycle.

dd.poll<T>(opts: PollOptions<T>): PollHandle

interface PollOptions<T> {
  /** Do the work; return the data (or throw to trigger onError + backoff). */
  fetch: () => Promise<T>;
  onData: (data: T) => void;
  onError?: (err: unknown) => void;
  /** Milliseconds until the next poll after a success: a number, or a getter
   *  read each cycle so it can follow a live setting. */
  every: number | (() => number);
  /** Exponential backoff from 1s on error, capped at every (default true). */
  backoff?: boolean;
}

interface PollHandle {
  /** Cancel any in-flight or scheduled cycle and poll now. */
  refresh: () => void;
  stop: () => void;
}

Polling starts immediately. The usual wiring:

const poll = dd.poll({
  fetch: () => fetchForecast(settings),
  onData: render,
  every: () => Number(settings.refreshMinutes) * 60_000,
});
dd.settings.onChange((next) => {
  settings = next;
  poll.refresh();
});

dd.log()

dd.log(level: "debug" | "info" | "warn" | "error", ...args: unknown[]): void

Writes to the host's log, tagged with your widget and instance id. Uncaught errors and unhandled rejections in your document are forwarded there automatically, so the host log is the first place to look when a widget misbehaves.

dd.links.open()

The one way out of the widget: open something with the OS default handler.

dd.links.open(url: string): Promise<void>

Needs the links permission, and url must start with one of the manifest's links.prefixes entries (each a full scheme://... string, like steam://run/ or https://github.com/). Dangerous schemes (file, javascript, data, and friends) are refused regardless of what a manifest declares. Honored only while the real cursor is inside the widget (call it from a click handler) and never in edit mode. 5 calls per rolling 10 seconds.

How permission gating works

The host checks permissions before a method dispatches. Most methods map to the permission named by their prefix:

PrefixNeeds
settings.*, theme.*, widget.*nothing, always available
time.*time
storage.*storage
files.*files
notify.*notifications
http.*http, with the exact host allowlisted
links.*links, with a matching prefix
desktop.*desktop
folders.*folders
apps.*systemApps
system.* readssystem
system.* writessystem + systemControls
media.* readsmedia
media.* controlsmedia + mediaControls
audio.*audio
widgets.*control
peers.*peers
claudeUsage.*claudeUsage
power.*power
dialogs.pickDesktopItemsdesktop
dialogs.pickFolderItemsfolders
popout.*a manifest popout block (a capability, not a permission)
input.*the manifest interactive flag (SDK-managed)

A call without its grant rejects PERMISSION_DENIED. See the permissions reference for what each grant means to the user.

Rules that apply everywhere:

  • Every on* subscription returns an unsubscribe function.
  • Rate limits are per instance over a rolling 10 second window unless a method says otherwise; exceeding one rejects RATE_LIMITED.
  • Serialized request params are capped at 256 KB.

How the connection works

Widget documents run in sandboxed iframes with an opaque origin. On load the SDK performs a small handshake with the host and receives a dedicated message port plus the init context; all further traffic flows over that port, and the host ties every message to the frame it physically arrived from. None of this is your concern day to day: the dd namespaces are the whole surface, and nothing a widget puts inside a message can change which widget the host believes it is talking to.