Widget Development Guide

This guide documents the current third-party runtime after the Kernel and Gateway rewrite.


Current scope

The first supported host is sandboxed JavaScript or TypeScript. Local packages, ESM entries, createWidget()/mount(), Gateway queries, todo writes, widget state, subscriptions, local API calls, consent prompts, revocation, throttling, lifecycle events, and audit records are supported. v1/v2 manifests remain available through the compatibility adapter.

Remote distribution, Java hosting, and complete network/media proxies are not available yet. client.fetch() and client.loadMedia() are reserved APIs and currently report unimplemented.

Package and manifest v4

widgets/
  my-widget/
    manifest.json
    index.js
    assets/
{
  "manifest_version": "v4",
  "widget_type": "sample_hello",
  "name": "Sample Hello Widget",
  "publisher": "Example Publisher",
  "entry": "index.js",
  "runtime": { "language": "javascript", "version": "ES2022", "entry": "index.js" },
  "ui": { "model": "web-sandbox" },
  "capabilities": ["metrics.summary.read"]
}

v4 requires the version, unique type, name, publisher, runtime language/version/entry, UI model, and capabilities. The current JavaScript loader also requires the top-level entry, which should match runtime.entry. Optional fields include size, icon, signature, CSP, budgets, capability justifications, requested domains, and media sources.

Capabilities and consent

Privileged calls go through the Widget Gateway. A missing grant returns a recoverable denial; the host shows a consent prompt and retries once only after the user accepts. Users can revoke one scope or all scopes in Widget Center.

ScopeAccess
screen-time:readUsage, focus, goals, categories, sessions, and related queries
todo:read / todo:writeRead or mutate todos
browser:readBrowser activity query
settings:writeFocus-mode writes
local-api:callScoped local TimeLens API calls

Entry and context

export function createWidget() {
  let root;
  return {
    async mount(container, context) {
      root = document.createElement("div");
      root.textContent = `Hello from ${context.widgetType}`;
      container.appendChild(root);
      console.log(await context.client.query("metrics"));
    },
    unmount() { root?.remove(); root = null; },
  };
}

Direct mount(container, context) plus optional unmount() is also supported. Context contains widgetId, widgetType, the new client, the legacy channel, and lifecycle callbacks. New widgets should use client; channel is for migration.

WidgetClient

const metrics = await context.client.query("metrics", { start, end });
const value = await context.client.getState("selected-range");
await context.client.setState("selected-range", "today");
const todo = await context.client.addTodo("Review screen time");
const handle = await context.client.subscribe("focus-started", console.log);
await context.client.unsubscribe(handle);

Namespaces are metrics, sessions, categories, projects, tags, goals, rules, focus, todos, and browser. Local API calls remain available through context.channel.localApiCall({ method, path, scopes }).

Errors, lifecycle, and testing

Gateway statuses include success, denied, revoked, throttled, timed_out, degraded, and error. WidgetGatewayError exposes code, scope, and recoverable; use isConsentRequired() for consent failures. Release subscriptions during unmount. The current limit is 60 channel calls per minute per instance.

Copy the package into the app data widgets directory, start development mode, add it from Widget Center, and exercise the consent and permission matrix flows. The development-only Widget Dev Harness can mock Gateway responses and reload local entries.

阅读中文指南 · See v1/v2 migration notes and the src-tauri/widget-contract/manifest-v4.schema.json schema.


Last updated: 2026-08-27 · TimeLens widget runtime rewrite