Data services
Reference for dd.http fetch against host allowlists, dd.feeds for user-chosen news feeds, the dd.weather helpers, and dd.claudeUsage.
dd.http
Needs the http permission with the exact hostname allowlisted in the
manifest (permissions.http.hosts). All traffic goes through the host's
proxy; widgets have no direct network access at all.
dd.http.fetch()
dd.http.fetch(req: {
url: string;
method?: string; // GET (default), POST, PUT, PATCH, DELETE, HEAD
headers?: Record<string, string | { $secret: string }>;
/** Extra query params merged into the URL host-side after allowlist
* validation. Values may be { $secret: name } like headers. */
query?: Record<string, string | { $secret: string }>;
body?: string;
/** "dataUrl" returns the raw response bytes as a base64 data: URL
* (bodyText/json omitted), for images. Default "text". */
as?: "text" | "dataUrl";
}): Promise<{
status: number;
headers: Record<string, string>;
bodyText?: string;
json?: unknown; // parsed when the response is JSON
dataUrl?: string;
}>The rules, all enforced host-side:
- Hosts. The URL's hostname must exactly match an entry in
permissions.http.hosts.httpsonly (httpallowed for localhost). Redirects are followed up to 4 hops, each hop re-validated against the allowlist. - Methods.
GET,POST,PUT,PATCH,DELETE,HEAD. Headers that would let a widget impersonate the browser session (host,cookie,origin,referer, connection-control headers) are refused; the response exposes a small safe set (content-type,content-length,date,cache-control,etag,last-modified). - Secrets. A header or query value of
{ "$secret": "settingKey" }names one of this widget'ssecretRefsettings; the host injects the user's stored value after validation. The resolved value (and the merged URL) never appears in widget JS, logs, or error messages, and secret query params are never re-appended across redirects. - Sizes and pacing. Request body up to 256 KB; response up to 1 MB;
query up to 32 pairs (keys 128 bytes, values 2048 bytes, final URL 8 KB);
30 second timeout; 2 concurrent requests per instance; 30 requests per
minute by default, raised by the manifest's
http.rateLimitup to 120. - Binary.
as: "dataUrl"returns the bytes as a base64data:URL (same 1 MB cap). The widget CSP allowsimg-src data:, so the result can feed animgordd-icondirectly.
const res = await dd.http.fetch({
url: "https://api.example.com/v1/status",
headers: { authorization: { $secret: "apiKey" } },
});
if (res.status === 200) render(res.json);dd.feeds
Needs the feeds permission (app 0.3.9+, and 0.3.11+ for
dd.feeds.open; the store sets minAppVersion). Reads a news feed from the web address the user typed into
one of the widget's url settings, and opens its items in the browser. The
host fetches and parses the feed and hands back only the parsed items, never
the response, so the grant cannot fetch anything else.
dd.feeds.fetch()
dd.feeds.fetch(opts: { settingKey: string }): Promise<FeedResult>
interface FeedResult {
/** The feed's own title (at most 120 characters), or null. */
title: string | null;
/** Up to 50 items, in the order the feed lists them. */
items: FeedItem[];
/** When the host last downloaded this feed (epoch ms). */
fetchedAtMs: number;
}
interface FeedItem {
/** Plain text on one line: tags removed, entities decoded, at most 300
* characters. Items without a title never arrive. */
title: string;
/** An absolute http(s) address of at most 1024 bytes, or null. Open it
* with dd.feeds.open. */
link: string | null;
/** The published time, else the updated time, in epoch ms; or null. */
publishedMs: number | null;
}settingKey names one of this widget's url settings. A url field never
inherits its manifest default, so the widget shows a setup state until the
user types an address; put an example in the field's placeholder. RSS 0.9,
1.0 and 2.0, Atom and JSON Feed all work.
The rules, all enforced host-side:
- Address.
httpsonly (httpallowed for localhost). Redirects are followed up to 4 hops, each held to the same rule, and a hop from a public address into loopback or a private network is refused. - Cache. One cache per address, shared by every widget that reads it. A call within 5 minutes of a download answers from memory; a later one revalidates with the ETag and Last-Modified the server sent.
- Output. Only titles, links and dates cross the bridge. Put titles on
screen with
textContent, and open links with dd.feeds.open, which needs nolinksgrant. - Errors.
NOT_FOUNDwhile the setting is blank or when the feed is gone (404 or 410);INVALID_PARAMSfor an address that is not https;PERMISSION_DENIEDwhensettingKeyis not aurlfield;INTERNALwhen the page is not a feed or the server fails. No error carries the address. - Sizes and pacing. Response up to 1 MB, 30 second timeout; 10 calls per rolling 10 seconds.
dd.poll({
every: 15 * 60_000,
refreshOn: ["feedUrl"],
fetch: () => dd.feeds.fetch({ settingKey: "feedUrl" }),
onData: (feed) => render(feed.items),
onError: (err) => (err.code === "NOT_FOUND" ? showSetup() : showStale()),
});dd.feeds.open()
dd.feeds.open(opts: { settingKey: string; link: string }): Promise<void>App 0.3.11+. Opens an item link from
dd.feeds.fetch in the default browser. The widget needs no links grant:
the host opens only a link it parsed out of the feed at the address the user
typed into that same url setting, so a widget can never pick another page.
- Which links. The items of the newest answer for that address, plus those of earlier answers, up to 200 links. A headline that is still on screen after a refresh still opens. When the host no longer holds the address in its cache, the call reads the feed once before it decides.
- Gesture. Honored only while the real cursor is inside the widget, and never from a desktop widget in edit mode. Call it from a click handler.
- Errors.
NOT_FOUNDwhen the link is in none of those answers (the headline left the feed);PERMISSION_DENIEDoutside a click; plus thedd.feeds.fetcherrors for the setting. - Pacing. 5 calls per rolling 10 seconds.
strip.addEventListener("click", (event) => {
const item = event.target.closest("[data-link]");
if (!item) return;
dd.feeds
.open({ settingKey: "feedUrl", link: item.dataset.link })
.catch((err) => dd.log("warn", "headline open failed", err.code));
});dd.weather
The shared weather stack, built on Open-Meteo. Convenience code, not a
capability: everything routes through this widget's own grants. geocode
needs geocoding-api.open-meteo.com in http.hosts, current and
forecast need api.open-meteo.com, and the geocode cache uses your
storage grant.
dd.weather.describe()
dd.weather.describe(code: number, isDay: number | boolean): {
label: string;
glyph: WeatherGlyph;
}
type WeatherGlyph =
| "clearDay" | "clearNight" | "partlyDay" | "partlyNight" | "cloudy"
| "fog" | "drizzle" | "rain" | "snow" | "thunder";Maps a WMO weather code to a human label plus a glyph name for icon().
dd.weather.geocode()
dd.weather.geocode(query: string, opts?: { cacheKey?: string }): Promise<{
query: string;
lat: number;
lon: number;
label: string;
}>Resolves a place name, cached in the widget's own storage (one entry per
cacheKey; a changed query re-fetches).
dd.weather.current()
dd.weather.current(opts: {
lat: number;
lon: number;
units?: "celsius" | "fahrenheit";
}): Promise<{
temperature: number; // in the requested unit
code: number; // WMO code; feed it to describe()
isDay: boolean;
}>dd.weather.forecast()
dd.weather.forecast(opts: {
lat: number;
lon: number;
units?: "celsius" | "fahrenheit";
scale: "hourly" | "daily";
count?: number; // slots to return, 1..24 (default 5)
stepHours?: number; // hourly only: hours between slots, 1..12 (default 1)
}): Promise<Array<{
time: string; // local ISO: "2026-08-28T15:00" hourly, "2026-08-29" daily
code: number; // WMO code; feed it to describe()
isDay: boolean; // daily slots are always true
temperature: number; // hourly: the temperature. daily: the high
low?: number; // daily only
}>>The next slots of the forecast, hour by hour or day by day. Slot 0 is always
the current period, this hour or today, so you can label it "Now" or "Today"
without a second call; stepHours spaces the hourly slots, which lets five of
them reach a day and a half ahead.
Unlike geocode, this is stateless: nothing is cached for you. Keep your own
last-good snapshot in dd.storage if you want the widget to paint something
after a remount or while the network is down.
The array is short rather than padded when the forecast window ends, and a malformed or empty response throws instead of returning partial slots.
dd.weather.icon()
dd.weather.icon(glyph: WeatherGlyph | string): stringA built-in line-art icon as a data: URI, ready for <dd-icon src=...>
(the widget CSP allows img-src data:). Unknown names return "".
dd.claudeUsage
Needs the claudeUsage permission.
dd.claudeUsage.status()
dd.claudeUsage.status(opts: { dirKey: string }): Promise<ClaudeUsage>Claude Code's own usage as one aggregated snapshot. dirKey names one of
this widget's filePath settings; the host resolves the .claude directory
the user configured there, reads the limit bars, today's tokens by model,
and recently-active sessions, and refreshes the bars from Anthropic's usage
endpoint in the background. The OAuth token, transcript contents, and raw
paths never reach the widget. Poll it. 20 calls per rolling 10 seconds.
A blank setting means the host finds the folder (app 0.5.1+; before that,
%USERPROFILE%\.claude): CLAUDE_CONFIG_DIR, the default install, and the
~/.claude folders of running WSL distros, the most recently used one
winning. The host never starts a stopped WSL distro. A folder in one, found
or typed, answers its last snapshot with wslStopped: true.
interface ClaudeUsage {
/** False when the configured directory is not a readable Claude home,
* or a blank setting found none: show a "set Claude home dir" hint. */
dirOk: boolean;
/** True when the folder is in a WSL distro that is not running: the
* rest is the last snapshot read, with no sessions. With dirOk false,
* nothing was found and a stopped distro went unsearched. App 0.5.1+. */
wslStopped?: boolean;
claudeVersion: string | null;
/** Where limits came from: "live" (Anthropic's usage endpoint), "cache"
* (Claude Code's own cache), or "none" (nothing available yet). */
source: "live" | "cache" | "none";
/** When limits was produced (epoch ms), for an "as of" readout. */
fetchedAtMs: number | null;
/** One bar per active limit window; percent is 0..100. A window whose
* reset time has passed reads 0 with resetsAtMs null (app 0.5.1+). */
limits: {
kind: string;
group: string;
label: string;
percent: number;
severity: string;
resetsAtMs: number | null;
isActive: boolean;
}[];
/** Today's usage summed from the transcripts (local day), by model. */
today: {
byModel: {
model: string;
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
messages: number;
}[];
totals: {
input: number;
output: number;
cacheRead: number;
cacheWrite: number;
messages: number;
};
};
/** Recently-active Claude Code sessions. */
sessions: {
name: string | null;
cwd: string;
status: string;
updatedAtMs: number;
}[];
}Bar widgets
Reference for the bar-only surface: the taskbar store and the strip, the open windows, the pinned apps, the preview flyout, the system tray mirror, the start-menu tap and the per-app context menu, gated by the taskbar permission.
Notifications and dialogs
Reference for dd.notify and the dd.dialogs pickers for desktop and folder items.