ADR-0024: Script progress feedback via ui.status()
Status: Active (amended 2026-08-17) · Date: 2026-08-13
Amendment (2026-08-17): folded into ui.notify()
ui.status() is removed. Its behaviour is now what ui.notify() does: every notification also
writes its message to the run output.
The decision below stands — one call, one message, two surfaces — but it did not need its own
method. In practice the SDK offered three ways to say something, and the names did not distinguish
them: console.log (output only), ui.notify (popup only), ui.status (both). ui.status was
never a capability, only the other two called together, and “which of these do I want?” is not a
question an author should have to answer for every line.
What settled it is the argument ADR-0025’s permissions work already
made for refusals: a toast is gone in a few seconds, so a message worth interrupting for is worth
keeping. Notify-without-a-record is the combination nobody wants; making it the default for ui.notify costs nothing and removes a method.
The surface is now two things with a rule you can state in one line: console.log for the record, ui.notify to interrupt (which also records).
Migration is mechanical — ui.status(m) → ui.notify(m). level semantics are unchanged, and error/warning still route to console.error.
References
- ADR-0022: Automations run as Deno subprocesses — the runner and the
uicapability this extends;consoleoutput lands in its Runs panel andui.notifyis its notification bridge - Automations — ui.status & Search Example — Delivery Notes — the as-built record and a worked example that uses it
- Writing automations (guide) — the
uiAPI reference, kept in lockstep
Context
Automation scripts (ADR-0022) have two separate ways to report
what is happening: console.log, whose output is captured into the Automation Runs panel (the
run’s “terminal”), and ui.notify, which surfaces a transient popup. Neither alone narrates a
multi-step run well — the Runs panel is a persistent log the user may not be watching, and
notifications are glanceable but ephemeral and unlogged.
So scripts that wanted a progress feed hand-rolled the same helper —
const status = (m: string) => { console.log(m); return ui.notify(m); }; — re-implementing it per script. Trivial, but it is boilerplate everyone rewrites, and the “log and notify together” pattern is common enough (search, scrape, multi-tab crawls, long file jobs) to deserve a name in the SDK.
Decision
Add ui.status(message, { level? }) to the ui namespace in the automation SDK
(tranquil-automations/deno/mod.ts):
async status(message, options = {}) {
const level = options.level ?? "info";
if (level === "error" || level === "warning") console.error(message);
else console.log(message);
await ui.notify(message, { level });
}, - It writes to both channels: the run output (via
console, so it lands in the Runs panel) and a notification (via the existingui.notify). One call, one message, two surfaces. levelmirrorsui.notify(info|success|warning|error);error/warningroute toconsole.error(stderr) so the run log distinguishes them.- It composes
ui.notifyrather than adding a host capability — no change to the RPC surface or the trust model; it is pure guest-side sugar. It referencesui.notify(notthis) so it stays correct if destructured out of the namespace.
Consequences
- Scripts narrate a run with one idiomatic call (
await ui.status("▶ Searching…")), and the narration is both glanceable (popup) and durable (Runs panel) — a terminal-style feed. - One more method on the
uisurface to document and keep in lockstep (the guide + this ADR). - Deliberately minimal. A structured progress API (named steps, percentages, a live-updating
status region) is out of scope and deferred —
ui.statusis a flat status line, not a progress model. A structured surface, if needed later, would extend or supersede this. - Alternatives rejected: (a) leave it to scripts — it is ubiquitous boilerplate; (b) a heavier progress/step primitive now — premature; (c) log-only or notify-only — the value is doing both.