ADR-0011: A first-class Tranquil settings tab
Status: Active · Date: 2026-07-19
References
- Tranquil Settings — Delivery Notes — what shipped and where
- Settings UI & Form Widgets — reusable widget specs & platform gotchas
- Theme Development — the business-theme conventions this builds on
Context
Tranquil had no home for user-adjustable settings. The immediate trigger was a papercut: corner
notification toasts vanished too fast (~1s). The obvious suspect was a hard-coded TOAST_TIMEOUT = 1000 in Tranquil’s own notify() helper (duplicated in tranquil-automations/lib/notify.js and tranquil-browser/lib/notify.js). But the actual cap turned out to be a stray global handler in tranquil-browser that force-dismissed every info toast after a hard-coded 1000ms (registered once per browser tab) — it overrode any
per-toast timer. Both had to go; the constant needed to become a user setting, and the global
1s dismiss had to be removed.
Rather than just bump a constant, we want a durable place for this and future preferences — and it should live where users already look for settings: the Pulsar settings-view pane, as a proper top-level tab next to Core / Editor / Keybindings, not buried under Packages.
Two constraints shape the approach. First, settings-view exposes no service to register a
top-level panel — the built-in tabs are added by an internal addCorePanel(name, icon, factory) method on the live SettingsView instance. Second, the third-party-package rule: we do not modify
settings-view (or notifications) source. What we do have: settings-view is a workspace pane item (so atom.workspace.observePaneItems can reach each instance), its SettingsPanel class auto-renders
config-bound controls for a whole namespace, and atom.config.setSchema lets us register an arbitrary
namespace.
Decision
Tranquil owns a top-level “Tranquil” tab in settings-view, built entirely from owned code. On
activation, tranquil-config (a) registers a tranquil.* config namespace via atom.config.setSchema, and (b) observes workspace pane items; for each SettingsView instance it
calls the instance’s own addCorePanel('Tranquil', 'gear', factory). The factory wraps core’s SettingsPanel({ namespace: 'tranquil' }) in a minimal .panels-item shell satisfying the element/show/focus/destroy panel contract (mirroring core’s general-panel.js). Reusing SettingsPanel gives two-way atom.config binding for free, and — because it emits the same markup
as Core/Editor — the business themes style the whole pane once and the Tranquil tab inherits it.
- First setting —
tranquil.toastDuration(integer milliseconds, default3000). It is stored in ms (what the toasts read) but edited in whole seconds through a friendlier UI (see the sub-decision below). The twonotify()helpers read this key instead of the hard-coded constant; the browser helper keeps a longer floor for errors/warnings so they stay readable. - Removed the global 1s force-dismiss. The
onDidAddNotificationhandler intranquil-browserthat killed all info toasts at 1000ms is deleted — this is the real fix for the papercut; without it no toast setting could take effect. - No schema
minimum/maximum. settings-view clamps integer fields to the schema bounds on every keystroke and rewrites the input, making a value impossible to type. We omit the bounds and enforce a sane floor where the value is consumed (the bridge clamps to the notifications minimum). - A native number input, not the auto-generated mini-editor. For this key the factory replaces
SettingsPanel’satom-text-editor[mini]with a native<input type="number">(whole seconds, up/down steppers) bound to config, plus a “Test” button that previews a toast at the current value. - One-way bridge:
tranquil-configobservestranquil.toastDurationand writes it tonotifications.defaultTimeout(clamped to that key’s minimum), so the handful of directatom.notifications.add*callers (tranquil-drag-drop, tranquil-window-color) honor the same setting — without editing the notifications package. - Settings-pane theming:
styles/settings.lessin both business themes is expanded to cover the full surface and widgets (inputs, checkboxes, radios, selects, buttons/.btn-primary, badges, breadcrumb, alerts, cards), matching themain-view.htmldesign-system specs, so the pane — and the Tranquil tab — reads as Tranquil, not stock Pulsar.
Options considered
- Option A:
addCorePanelon the live instance + reuseSettingsPanel+tranquil.*namespace (chosen) — a genuine top-level tab, free config binding, auto-themed, extensible (future settings just extend the schema). Cost: leans on settings-view internals (addCorePanel,panelCreateCallbacks, theSettingsPanelmodule path). Mitigated by lazy-requiringSettingsPanelby resolved package path (only when the tab first opens), duck-typing theSettingsViewinstance, and guarding idempotency — a missing/moved internal degrades to “no Tranquil tab”, never a crash. - Option B: declare a
configSchemaand rely on the automatic per-package page under Packages — zero UI code, reusesSettingsPaneltoo. Rejected: it is not a top-level tab; the settings are buried underatom://config/packages/tranquil-configand read as a package’s internals, not a first-class Tranquil surface. - Option C: fork/modify settings-view to add a native Tranquil tab — cleanest integration. Rejected: violates the no-third-party-edits rule and forks a large upstream package for one tab.
- Option D: build a bespoke settings pane/UI from scratch — full control. Rejected: reinvents
SettingsPanel’s config binding and would need its own theming, all for no gain over reuse. - Setting sub-decision — own
tranquil.toastDuration+ bridge (chosen) vs. binding the UI directly tonotifications.defaultTimeout— a cleantranquil.*namespace keeps future settings coherent and lets one control drive both the helper toasts and the core path; the one-line bridge avoids editing the notifications package. - Control sub-decision — native
<input type="number">in seconds (chosen) vs. the coreatom-text-editor[mini]in raw ms — the mini-editor showed raw milliseconds (unfriendly for a stepper), rendered in the monospace editor font, collapsed in flex layouts, and was subject to the keystroke-clamp above. A native number input gives a whole-seconds value with real up/down steppers, stores ms behind the scenes, and matches the design system. Cost: it is bespoke (not auto-rendered), so it carries a little glue code and the native-input caveats in Consequences.
Consequences
- Extensible for free: future simple Tranquil settings are just new properties on the
tranquilschema — they auto-render in the tab and auto-theme, no per-setting UI or CSS. The number input + Test button are the one bespoke exception, kept in the panel factory. - Native-input caveats (documented): a native
<input>in the workspace needs thenative-key-bindingsclass (or Backspace/Cmd-A are swallowed), needscolor-schemeset (or its spin buttons are invisible on a dark surface), and cannot render::before/::after(so themed checkboxes draw their tick via abackground-imageSVG). These are collected in the Settings UI & Form Widgets guide. - Live, no restart (for the value):
notify()reads the config per-toast, so changing Toast Duration takes effect immediately. But schema/JS/DOM-factory changes need a full window reload — a stylesheet reload updates CSS only. - Internal-coupling risk: if upstream settings-view renames
addCorePanel, changespanelCreateCallbacks, or moveslib/settings-panel, the tab silently disappears (guarded, not fatal). Re-evaluate if we pull a major settings-view update. - Theme duplication: the expanded
settings.lessmust stay identical in both business themes (they differ only incolors.less) — the standing theme-sync obligation. - Re-evaluation triggers: settings-view exposing a real panel-registration service (adopt it); a
settings-view major upgrade; or the
tranquil.*schema growing enough to warrant grouping/sections.
Validation
Verified live: a Tranquil tab (gear) appears in the settings nav at atom://config/tranquil with
a themed Toast Duration control (whole-seconds number field + steppers + Test button); editing it
persists ms to ~/.tranquil/config.cson; the Test button and real notify() toasts both honor the
value now that the browser 1s force-dismiss is gone; error/warning browser toasts still linger longer;
the pane reads correctly in both business light and dark. (See the delivery notes and widgets guide
linked at the top for implementation notes and the reusable UI gotchas.)