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.js—seedExampleProject,firstRunWindowOptions,onlyOpensResourcePathand the first-run branch inlaunch()are gone, along with thetranquil:new-default-windowIPC handler. The “has this run before?” gate was the existence of~/.tranquilitself, not a marker file.tranquil-config— the command registration and the File-menu insertion are gone. Everythingapplication:new-windowwas left alone; the menu item located its insertion point by searching for that command, so the coupling was one-way.tranquil-client/package.json— thelink:dependency and thereseed-examplesscript 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-itemis (0,3,0) and setscolor: inherit, so a shorter.…-item.runs-can-cancelat (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::beforeon 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-sizeon.codiconin the pane-control strip. getURL()on a<webview>throws before attach, rather than returning falsy. Three call sites guarded withif (!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
ReferenceErrorfrom one explains that a selection inherits imports but not the file’s variables.
Behaviour changes worth knowing
ui.status()is removed, folded intoui.notify()— see the ADR-0024 amendment.tabs.active()opens a tab when none is open instead of throwing. Deliberately not viatabs.open(): that resolves the new tab by URL equality, and a blank tab reports the start-page document it loaded rather thantranquil-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.
⇧⌘Utoggles the Automation Runs panel, wired on both ADR-0018 paths — the keymap andhandleGlobalGuestKey, 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.