ADR-0007: Per-window identity accent color
Status: Active · Date: 2026-07-07
Context
ADR-0005 made every window its own browser session, so windows are now distinct “profiles” (work vs. personal, two tenants). But they remain visually identical, so there is no way to tell at a glance which window is which account — the exact problem browser profile colors and VS Code’s Peacock solve.
Two constraints shape any solution. First, theming is global: core.themes is a single
all-windows config array, read identically by every renderer, so you cannot “load a different theme
per window.” Second, theme colors are compile-time LESS hex (the business themes bake literal
values in styles/colors.less), not runtime CSS variables — there is no existing custom-property
layer to override (the only --* property in the tree was --editor-font-family). So a per-window
color cannot come from the theme system as it stands.
What the windows do have is per-window isolation the feature can hang on: each renderer has its own StyleManager (so atom.styles.addStyleSheet only affects that window), and each window already
carries the durable windowSessionId uuid from ADR-0005.
Decision
A per-window accent is an injected CSS custom property, keyed by windowSessionId. A small owned
package (tranquil-window-color) injects a per-window stylesheet setting --tq-window-accent (the
color) and --tq-window-accent-fg (an auto-contrasted near-black/white foreground, chosen by the
accent’s luminance) on :root; the UI themes read var(--tq-window-accent, <fallback>) to fill the status bar (the bottom band). With no color assigned the fallback wins, so the window looks exactly
as before. The hooks live in all four UI themes (tranquil-business-dark / -light, tranquil-theme-dark / -light) so the accent works whichever is active.
- Storage is one global config map,
tranquil-window-color.assignments = { <windowSessionId>: <presetKey> }. Because the session id is restored on relaunch (ADR-0005), the accent reappears automatically — with no main-process or core changes. - Picker is a curated preset palette (Blue, Teal, Green, Amber, Orange, Rose, Purple, Gray),
each with a dark and light hex so contrast holds under either business theme; the variant is chosen
from the active
core.themesand re-picked when the global theme flips. It is surfaced as one command per preset (tranquil-window-color:set-<key>) plus a clear command — no external picker UI, so every color is discoverable straight from the command palette and the Window menu. - Scope is an identity accent, not a recolor — the tint marks the window; it does not restyle its content.
Options considered
- Option A: Route color through the theme system — e.g. a per-window theme package or per-window
core.themes. Rejected: theme activation is global in core and theme colors are compile-time LESS; there is no per-window theming seam without a large core change. - Option B: Per-window injected custom property + themed
var()hooks (chosen) — leverage the per-rendererStyleManagerto inject--tq-window-accent, and have the theme consume it with a fallback. Small, additive, no core/main-process changes, no-op when unset. Cost: the tinted surfaces must be hooked in the theme (duplicated across both business themes, which differ only incolors.less). - Option C: Self-contained package styling (no theme edits) — have the package inject not just the
variable but also the full status-bar selector rules (
.status-barbackground/foreground). Rejected: couples the package to theme DOM/markup and duplicates appearance logic the theme should own; light/dark contrast handled better inside each theme. - Storage sub-decision — main-process per-window persistence (rejected) — persist the color
alongside window options in
application.json(likewindowSessionIditself). Rejected: it touches core save/restore for no gain, since a global config map keyed by the already-durablewindowSessionIdgives the same restart persistence entirely within owned code (honors the third-party-package rule). - Picker sub-decision — free color picker (rejected for now) — a full color wheel / hex input. Rejected in favor of presets: arbitrary colors clash with the theme and need per-color contrast handling; a curated palette stays on-brand and legible in both themes.
Consequences
- No-op when unset: every
var(--tq-window-accent, <fallback>)usestransparent/the existing value as its fallback, so an unassigned window is pixel-identical to today — no regression risk for users who never touch the feature. - Native title bar not tinted: the macOS title bar is OS-drawn and
nativeTheme.themeSourceis process-global (only toggles light/dark), so the accent shows in in-DOM chrome only. A colored title bar would require adopting the custom in-DOM title bar — explicitly out of scope. - Not per-window light/dark: this varies only the accent. Because theme activation is global, independently making one window light and another dark is a separate, much larger effort and is a non-goal here.
- Theme edits duplicated: the
var()hooks live in all four UI themes (tranquil-business-dark/-light,tranquil-theme-dark/-light) — each theme styles its own status bar, so the hook must be added to whichever the user might run. New tinted surfaces must be added to every theme. - Orphaned assignments: closing a window leaves its
windowSessionId → presetKeyentry in the config map (a single renderer can’t enumerate all live session ids to prune). Tiny keys; a future cleanup could prune alongside the ADR-0005 orphaned-partition cleanup. - Re-evaluation triggers: if users want arbitrary colors, revisit the free-picker option; if a colored title bar becomes necessary, that pulls in the custom title bar decision.
Validation
Verified live: two windows carry distinct status-bar accents that persist across a quit/relaunch, and
the accent re-picks its light/dark variant when the global theme flips. The var() hooks are in all
four UI themes with no-op fallbacks, so an unassigned window is unchanged. Implementation notes are in Per-Window Accent Color — Delivery Notes.