Context
ADR 0249 builds web, PWA and Electron desktop from one app member, but does not say how an app picks its targets or which window surfaces the desktop target offers. A desktop app can be a dock app, a menu-bar app or both, and can show floating overlays opened by a system-wide shortcut. A browser tab or an installed PWA can do none of the tray, global-shortcut or always-on-top work, and config must not pretend otherwise. The owner decided targets, desktop modes and two overlay styles in chat on 2026-10-22.Decision
-
One member, one codebase, listed targets. An app member declares its targets in
stackra.config.tsat the member root, which holds the one app definition per member:targets: [Target.Web, Target.Pwa, Target.Desktop].stackra genbuilds only the listed targets. There is no second codebase per target (ADR 0249).- Required root file.
stackra.config.tsjoins the app and service member shapes of ADR 0126 as a required file at the member root, andpnpm newcreates it. - Config files by topic. Following the owner’s “one fact, one file” decision of 2026-10-21, an app’s config files
sit under
src/config/and are named by fact or topic, not by package:desktop.config.ts,theming.config.ts,consent.config.tsand so on. This amends ADR 0146 decision 1 (CF1,<pkg>.config.ts) and the 2026-10-21 amendment of ADR 0126 (decision 1,src/config/<name>.config.ts) for app members. Desktop options live insrc/config/desktop.config.ts.
- Required root file.
-
Capability matrix. What each target can offer:
Offline follows ADR 0252 (offline on every client). Web uses the
@stackra/storageweb drivers (IndexedDB) and the offline mutation queue. PWA has the same, plus the service worker for the app shell and background sync (ADR 0255). Desktop uses SQLite through the./sqlentry of@stackra/storage(ADR 0256 Phase 2), and thefsdriver until that lands. Config never claims a capability its targets lack. Validation rejects a desktop-only option (mode,overlays, a global shortcut, the tray) whenTarget.Desktopis not intargets. -
Desktop mode.
desktop.config.tssetsmodeto oneDesktopMode:Window: a dock app with no tray icon.Tray: a menu-bar app only. There is no dock or taskbar icon, and the window opens from the tray icon.Hybrid: a window and a tray icon. Closing the window keeps the app in the tray only when the user has turned that on; it is off by default (ADR 0254, tray keep-alive).
-
Named overlays.
desktop.config.tsdeclares namedoverlays. Each has astyle(OverlayStyle):Launcher: a wide prompt bar centred on the active display that grows downward with results (Claude style). It opens by global shortcut or tray menu and closes on Esc or blur. Its width is fixed and its height follows the content.Panel: a card docked to a screen edge or under the tray icon (Gemini style). It opens by shortcut, tray menu or an in-app button and closes by its button. It can be pinned, is resizable, and remembers its size and position.
route;shortcut(user-rebindable, registered by the main process only, decision 6);alwaysOnTop; the behaviour on blur; a vibrancy or blur background (opaque where the OS has none);anchor(OverlayAnchor:ScreenCenter,TrayIcon,EdgeRight,EdgeLeft,Cursor); and the displays the overlay may open on. -
Two shells, no second implementation.
@stackra/platform/reactshipsLauncherOverlayandPanelOverlay. Any route renders inside the configured shell, and the same route also works in the main window. A surface never gets its own copy of a screen. -
Global shortcuts and screen capture. Global shortcuts are registered by the Electron main process only, never by
the renderer. Every shortcut is listed in config, checked for conflicts at startup, and can be rebound or disabled by
the user in settings. Screen capture (
display-capture) is a permission that is off by default; the renderer stays inside the hardening baseline of ADR 0251. -
Defaults.
- The console app:
Web,PwaandDesktop, desktop modeHybrid, with aquickoverlay of styleLauncheronCommandOrControl+Shift+Spaceand anassistantoverlay of stylePanelanchoredEdgeRight. - A web app:
WebandPwaonly.
- The console app:
-
Where the enums live. Each enum is a const object plus a same-named union type in a
<name>.enum.tsfile (ADR 0128).Targetlives in@stackra/contracts.stackra.config.tsis read bystackra gen, the CLI and the scaffolder (pnpm new … --targets, ADR 0249) as well as by@stackra/platform, so it is a shared primitive under ADR 0117 C1. Placing it in@stackra/platformwould make the build tools depend on the package that holds the Electron and Expo drivers.DesktopMode,OverlayStyleandOverlayAnchorlive in@stackra/platform. Onlydesktop.config.tsand the platform shell read them, the same way each package owns the vocabulary of its own config (ADR 0117 decision 3, the membership test of ADR 0015; ADR 0146).
Consequences
pnpm newwritesstackra.config.tsfor app and service members, and the scaffold test checks it.- An app chooses its surfaces in two config files, and a feature written once runs in a tab, a PWA, the desktop window and an overlay.
- A PWA config that asks for a tray or a global shortcut fails validation instead of shipping a silent no-op.
@stackra/platformgrows a tray port, a global-shortcut port, an overlay window manager and the two React shells; the desktop drivers carry the per-OS differences (dock hiding, vibrancy, display placement).- Settings gains a shortcuts page (rebind and disable) and a screen-capture permission toggle.
- Linear: the overlay shells, the tray and the shortcut work land in the
@stackra/platformwave, not in a separate project.
Enforcement
- planned — the
stackra.config.tsanddesktop.config.tsschemas instackra genreject desktop-only options whenTarget.Desktopis not a target, and gen emits only the listed targets. - planned — an oxlint boundary rule allows Electron
globalShortcutandTrayimports only in the desktop main-process entry; the renderer cannot reach them under ADR 0251 (context isolation, sandbox, generated IPC allow-list). - planned — a desktop driver test registers every configured shortcut at startup and fails on a conflict; a test checks
that the permission handler denies
display-captureuntil the user grants it. - planned — a
@stackra/platformtest renders one route in the main window,LauncherOverlayandPanelOverlay.
Supersedes and sources
Supersedes: none. Amends: 0126 (its 2026-10-21 amendment for app members, and the app and service member shapes), 0146 decision 1 (CF1). Extends: 0249, 0250, 0251. Consistent with: 0254 (tray keep-alive opt-in), 0252 (offline on every client), 0255 (PWA offline), 0256 (desktop SQLite through the./sql entry, fs driver until then). Sources: owner decision in chat,
2026-10-22 (STA-292).