Tranquil API
What a script uses to work with Tranquil itself: tabs to find and open browser tabs, ui to tell
or ask the person running it, files to read and write files, and context for facts about the
run. The SDK form methods (fields, fill, click and the rest) have their own page, Forms API, and so does running your own code inside a page, Page Code API.
All of it comes from one import:
import { clipboard, config, context, FieldError, files, tabs, ui, workspace } from "tranquil/automation"; tabs
tabs resolves browser tabs; each method returns a Tab handle. A handle is bound to a stable
tab, so it survives navigation: tab.url is a live read, not a snapshot. These are the ways to get
one, which differ only in how the tab is located:
| Method | Returns | Description |
|---|---|---|
tabs.active() | Promise<Tab> | The tab you’re looking at (or most recently were). Opens one on the start page if no tab is open. |
tabs.find(urlOrRegExp) | Promise<Tab> | An open tab matching an exact URL string or a RegExp. |
tabs.all() | Promise<Tab[]> | Every open http(s) tab. |
tabs.open(url, { background?, location?, activate?, hideURLBar? }) | Promise<Tab> | Opens a tab. background: true opens it off-screen (for scraping or signed-in fetches), capped at about 4 concurrent, inheriting this window’s login session. The other options are under Where a tab opens. |
tabs.triggered() | Promise<Tab> | In a URL-triggered run, the tab whose load fired the trigger. |
All of them need the tabs grant (which forms, submit, page-js and browser include).
The Tab handle
What you can do with a handle, apart from the SDK form methods: wait on the page, navigate it, run page code, and change how the tab is presented.
| Member | Returns | Description |
|---|---|---|
tab.waitFor(target, { state?, timeout? }) | Promise<void> | Waits until a target is visible, or with state: "hidden" until it’s gone. Any visible match will do. On timeout, the error says what it found. |
tab.waitFor((page) => …) | Promise<void> | Polls page code until it returns something truthy. Needs page-js. |
tab.evaluate((page, ...args) => …, ...args) | Promise<T> | Runs a function in the page with JSON-serializable args; resolves its return value. Needs page-js. See Page Code API. |
tab.useCustomParts(...maps) | void | Tells fill, clear and fields() where a form that isn’t built to standards keeps a field’s value, open control and clear button. Optional. See Custom Parts. |
tab.goto(url, { waitUntil? }) | Promise<void> | Navigates the tab. waitUntil is "load" (default) or "domcontentloaded". |
tab.waitForNavigation(opts?) | Promise<void> | Awaits a navigation something else triggered (a click, a redirect). |
tab.frame(urlOrRegExp) | Promise<Frame \| null> | A form inside an iframe, with the same methods as a tab, or null when no frame matches. |
tab.title() | Promise<string> | The page title. |
tab.url | Promise<string> | The current URL (a live read). |
tab.screenshot() | Promise<Uint8Array> | A PNG of the tab. |
tab.toggleURLBar(hidden?) | Promise<boolean> | Hides or shows the tab’s address bar; flips it when called with no argument. Resolves to the new state. |
tab.close() | Promise<void> | Closes the tab. |
Tabs are AsyncDisposable, so await using tab = await tabs.open(url) closes the tab when the
block ends, even if it throws: no leaked tabs in loops.
Where a tab opens
A tab opens in the center, takes focus, and shows an address bar unless you say otherwise:
locationaccepts"center","right"or"bottom"; a dock location opens the dock as well as the tab;activate: falseopens without taking focus, which puts a panel beside your work instead of in front of it;hideURLBardrops the address bar, for a page presented as a panel rather than as a browser (remembered across a window reload).
// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions tabs
import { tabs } from "tranquil/automation";
await tabs.open("https://example.com/", { location: "right", activate: false, hideURLBar: true }); For a tab that’s already open, tab.toggleURLBar() does the same live, hiding the strip with the
URL field and the navigation buttons, not the tab itself.
Name the location on every open in a script that uses a dock, including the ones you want in the
center. Tranquil remembers where a URL was last opened and reuses that the next time the same URL
is opened, so a script that puts one page in a dock can find its later tabs going there too.
Passing location explicitly overrides what was remembered.
ui
Three ways to reach the person running the script: tell them something, ask them something, or put a file in front of them. None needs a grant.
| Method | Description |
|---|---|
ui.notify(message, { level? }) | Shows a notification and writes the message to the run output. level is "info" (default), "success", "warning", or "error". |
ui.confirm(message, { timeout? }) | Asks before something that can’t be undone, with Allow and Deny buttons. Resolves true only on Allow; false on Deny, after timeout (default 60 seconds) with no answer, and at once in a run nobody started by hand (a URL trigger). A submission needs a yes: the SDK refuses one unless the run’s most recent ui.confirm returned true. |
ui.open(path, { split? }) | Opens a file in the editor, or reloads and reveals it if it’s already open. split is "left" \| "right" \| "up" \| "down". |
files
Reading and writing files next to the script. Paths resolve against the script’s own folder
(context.scriptDir).
| Method | Returns | Description |
|---|---|---|
files.write(name, content) | string | Writes the file; returns its absolute path (feeds ui.open). |
files.read(name) | string | Reads a file as UTF-8. |
Four rules govern both:
- They work on disk, so
files.readsees the saved file, not an unsaved editor buffer. - Permissions apply.
files.read("input.txt")needsread=input.txt, andfiles.write("out.md")needswrite=out.md. The script’s own folder is not granted for free. - You name the file, not the folder.
- No missing folders are created. For a subfolder, call
Deno.mkdirSync(dir, { recursive: true })first; the SDK has nomkdirof its own.
// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions tabs write=title.txt
import { files, tabs, ui } from "tranquil/automation";
const tab = await tabs.active();
await ui.open(files.write("title.txt", await tab.title())); context
Facts about the current run (its identity, its folder, why it started, how it stops) and the one way to hand a result back:
| Member | Description |
|---|---|
context.runId | This run’s unique id. |
context.scriptDir | The script’s folder, which files resolves against. |
context.trigger | How the run started: { kind: "manual" }, { kind: "command", name }, or { kind: "url", url }. |
context.signal | An AbortSignal that fires when the run is cancelled: pass it to fetch, timers and anything else you wait on. |
context.result(json) | Hands back what the run found, as plain JSON (at most 64 KB). It appears in the Runs panel. |
Everything else
clipboard, workspace and config, then shell commands:
| Method | Description |
|---|---|
clipboard.read() / clipboard.write(text) | The system clipboard (distinct from the page’s clipboard inside evaluate). Needs clipboard. |
workspace.projectDir() | Absolute path of the first open project folder, or "". Needs tabs. |
config.get<T>(key) / config.set(key, value) | Per-script state that survives runs and restarts, kept under the script’s own section: a script can’t read or write app settings, or another script’s state. get resolves undefined for a key never set; name the type you stored: config.get<string[]>("seen"). |
To run a shell command, use Deno directly under a declared run= grant:
// Tranquil automation. Rules for writing and changing it: ~/.tranquil/llm/writing-automations.md
// @permissions run=git
const out = await new Deno.Command("git", { args: ["rev-parse", "HEAD"] }).output();
console.log(new TextDecoder().decode(out.stdout).trim()); Page code
For anything the SDK form methods don’t cover, tab.evaluate() runs a function of yours inside the
page, and tab.waitFor(fn) polls one; both need page-js. They have their own page: Page Code API, with the typed Page declaration, examples, and when to use a custom parts map instead.