Docs

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

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 existing ui.notify). One call, one message, two surfaces.
  • level mirrors ui.notify (info | success | warning | error); error/warning route to console.error (stderr) so the run log distinguishes them.
  • It composes ui.notify rather than adding a host capability — no change to the RPC surface or the trust model; it is pure guest-side sugar. It references ui.notify (not this) 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 ui surface 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 deferredui.status is 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.