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.
| Scope | Access |
|---|---|
screen-time:read | Usage, focus, goals, categories, sessions, and related queries |
todo:read / todo:write | Read or mutate todos |
browser:read | Browser activity query |
settings:write | Focus-mode writes |
local-api:call | Scoped 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