Docs

Examples Repo & Explicit Permissions — Delivery Notes

Two connected efforts landed 2026-08-17. tranquil-examples stopped being part of the app and became a repo you clone; separately, the permission model stopped granting a script its own folder for free. They are in one note because the second was found by writing examples for the first — every example needed a header, which is what exposed how much was still implicit.

1. The examples are no longer part of the app

The app used to seed tranquil-examples into ~/.tranquil/examples on first run, open it, and offer File → New Default Window to reopen it. That is all removed:

  • atom-application.jsseedExampleProject, firstRunWindowOptions, onlyOpensResourcePath and the first-run branch in launch() are gone, along with the tranquil:new-default-window IPC handler. The “has this run before?” gate was the existence of ~/.tranquil itself, not a marker file.
  • tranquil-config — the command registration and the File-menu insertion are gone. Everything application:new-window was left alone; the menu item located its insertion point by searching for that command, so the coupling was one-way.
  • tranquil-client/package.json — the link: dependency and the reseed-examples script removed.

It had never actually shipped. script/electron-builder.js excludes !**/examples, !**/example and !**/*.{markdown,md,…}, so a packaged build stripped the lesson files and the Examples/ subdirectory. The seeding path only ever worked in dev checkouts — which is a good argument for the change on its own.

release-manifest.json was deliberately not regenerated. It is the record of what v0.1.1 shipped; re-running the generator restamped it with today’s date and current SHAs, claiming that release contained code that did not exist then. The tranquil-examples pin drops out naturally at the next release, because the generator derives its set from link: dependencies.

Flattened

One folder per example, no hierarchy: <name>/<name>.ts plus a README. Moved with git mv so history follows. The five numbered lesson files were deleted — the docs site covers that ground better and two copies drift. Three examples were added: slow-count (cancellation), search-to-bookmarks (the fullest scrape-to-files run), and spice-catalogue (CRUD across runs).

Do not ship a deno.json in that repo. findDenoConfig() walks up from the script’s directory and falls back to ~/.tranquil/automations/deno.json, which is what makes a clone runnable from anywhere. A committed config would shadow it, and the mapping is an absolute file:// path to the installed SDK — machine-specific by construction.

2. File permissions are explicit

read/write used to mean “beyond your own folder”, with <scriptDir> added silently to both Deno flags. So // @permissions none was untrue: the script could still read and rewrite every file beside it. Now grants are exactly what the header lists, and --allow-write is omitted entirely when nothing is declared. Full reasoning in the ADR-0022 amendment.

Three findings worth keeping, all measured rather than assumed:

The machinery genuinely needs read. With no --allow-read at all, import(entry) fails — dynamic import is permission-checked. With only the entry file, resolving tranquil/automation fails. Both paths are required, and because the grant is file- and package-scoped rather than directory-scoped, a script still cannot read a sibling data file.

A bare . is rejected at parse time. Deno path grants are recursive, covering everything beneath a directory including folders that do not exist yet, so . reinstates the blanket. It is also the value an author reaches for by reflex, which is exactly why it had to be refused rather than discouraged.

ensureDirSync needs read, not just write. @std/fs’s helper calls Deno.statSync before creating, and stat is a read. Deno.mkdirSync(dir, { recursive: true }) needs write only and is already idempotent, so the stat bought nothing but a wider grant. This surfaced immediately once the implicit grant was gone — under the old model nobody would have noticed the script was reading its whole folder to create one directory.

3. Automation Runs panel

Copy and Cancel moved out of the panel body into the pane’s tab-bar strip, joining a new Clear Finished Runs and Revoke All Permissions. Clear keeps in-flight runs: their record is the only handle on a live child, so removing it would strand the process.

pane-controls had to learn about the bottom dock — it only ever observed centre, left and right, so no cluster was injected into the runs panel’s tab bar and nothing registered there could render. Any future bottom-dock control would have failed the same way.

Cancel now advertises itself: green with a slow pulse while a run is cancellable, solid green with a spinner while the kill escalates, plain when there is nothing to stop.

Traps found while testing

  • Two CSS traps hid the Cancel colour, and both leave the animation running, which reads as half-broken. First, the base rule .tab-bar .tranquil-pane-controls .tranquil-pane-controls-item is (0,3,0) and sets color: inherit, so a shorter .…-item.runs-can-cancel at (0,2,0) loses. Second, the glyph does not inherit: the business themes set * { color: @text-primary }, and a declaration matching an element directly beats an inherited value regardless of specificity. The icon is a ::before on that span, so the span itself must be coloured.
  • Codicon is a 16px pixel-designed font. An override to 14px put every glyph on fractional pixels — measured at 69% partial-intensity edge pixels versus 47% at 16px. Do not set a font-size on .codicon in the pane-control strip.
  • getURL() on a <webview> throws before attach, rather than returning falsy. Three call sites guarded with if (!uri) return;, a guard written for a falsy return. See upstream fix candidates entry 3 — this is what made DevTools open on every reload of a window with a browser tab.
  • A stray editor selection silently changes what runs. A double-click leaves one, and the run then executes two words instead of the file. The runs panel now labels selection runs, and a ReferenceError from one explains that a selection inherits imports but not the file’s variables.

Behaviour changes worth knowing

  • ui.status() is removed, folded into ui.notify() — see the ADR-0024 amendment.
  • tabs.active() opens a tab when none is open instead of throwing. Deliberately not via tabs.open(): that resolves the new tab by URL equality, and a blank tab reports the start-page document it loaded rather than tranquil-browser://blank.
  • The debugger now has a user-facing guide: Debugging Automations. It had existed only in ADR-0025 and the delivery note until now.
  • ⇧⌘U toggles the Automation Runs panel, wired on both ADR-0018 paths — the keymap and handleGlobalGuestKey, because a focused webview swallows keys before Pulsar’s keymap.
  • Toggling that panel now destroys the item rather than hiding the whole bottom dock, which used to take the Terminal with it.