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 {
  /** Per instance; a taskbar document (and its pane) carries the
   *  sentinel "__bar" on every display. */
  instanceId: string;
  /** "main" = the widget document; "pane" = a popout pane document;
   *  "bar" = the taskbar surface. Same grants either way. */
  role: "main" | "pane" | "bar";
  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 };
  /** Bar documents (and a bar's pane): the screen edge the bar window
   *  docks to. Absent on hosts older than this field: treat as "bottom". */
  bar?: { edge: "bottom" | "left" | "right" };
}

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.
  • A bar document (see bar widgets and building a taskbar) has role: "bar", real mouse input until it reports segments, no grid-style popouts (its pane opens beside the strip) and bar.edge set; interactive is always false for it.

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, staleness so a settings-driven refresh cancels the in-flight cycle, and the settings trigger itself.

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;
  /** Setting keys whose VALUE changing should re-poll, coalesced over
   *  refreshDebounceMs. Keys left out never cost a request. */
  refreshOn?: string[];
  /** How long the watched keys must rest before the re-poll (default 400ms). */
  refreshDebounceMs?: number;
}

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

Polling starts immediately. The usual wiring, with refreshOn naming the settings the fetch actually reads:

dd.settings.bind((next) => {
  settings = next;          // repaint on every change
});

dd.poll({
  fetch: () => fetchForecast(settings),
  onData: render,
  every: () => Number(settings.refreshMinutes) * 60_000,
  refreshOn: ["city", "units", "refreshMinutes"],
});

Do not call refresh() from a settings callback. The dialog live-previews your settings as the user types, one change per keystroke, so that version re-fetches once per CHARACTER: a nine-letter city name is nine requests, and dd.http.fetch allows 30 a minute. refreshOn coalesces the burst into one poll once the values rest, and a setting you leave out of the list, a panel restyle for instance, costs nothing at all.

refresh() stays for the cases it suits: a manual reload button, or a retry after the user fixes something.

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.

The host sanitizes forwarded text before writing it: URL query strings are replaced with ?..., home-directory paths collapse to ~, and each line is capped at 8 KB. Users share their log file for support, so nothing a widget logs should be able to leak a token or a username. Log identifiers and state, not raw URLs or file paths.

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; for a bar, the bar window or its pane, app 0.3.7 or newer) 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
games.*games
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.