Docs

Browser User-Agent — Delivery Notes

Delivery summary for the configurable browser User-Agent. For the decision record, read ADR-0019: Configurable browser User-Agent; this note says what shipped and where.

Status

Implemented across four packages; awaiting a manual pass. Not yet committed.

PieceState
tranquil.browserUserAgent setting (plain string, computed clean-Chrome default)✅ built
Editable combobox (Chrome / Firefox / Edge / Safari + free text) in the Tranquil settings tab✅ built
New tabs pick up the UA via the <webview useragent> attribute✅ built
Live changes: open tabs updated + value pushed to main over IPC✅ built
Main-process User-Agent header rewrite on every guest request✅ built

What shipped

A single setting, Browser User-Agent, that every browser tab honors. Its default is a clean stock-Chrome string (the app’s own UA with the Electron/… and Tranquil/… tokens stripped), derived at load so it tracks the bundled Chromium with no version to rot. The setting renders as an editable combobox: pick a Chrome / Firefox / Edge / Safari preset, or type any custom value. Chrome is the derived default, Edge reuses that Chromium version, and Firefox/Safari are fixed strings.

The value reaches guests through three layers:

  1. New tabs — the <webview useragent> attribute is set from the resolved UA before first load, and persists across in-tab navigations (so a tab is covered without a main-process round-trip).
  2. Live changes — a renderer config-observer applies the new UA to already-open tabs (on their next load) and pushes it to the main process over a set-user-agent IPC message.
  3. Header hardening — the main process rewrites the User-Agent request header on every guest request via onBeforeSendHeaders, covering the subresource/WebSocket requests the attribute and setUserAgent miss.

Where everything lives

tranquil-config (setting + UI)

  • src/index.jsBROWSER_UA_CHROME + BROWSER_UA_PRESETS module constants; the tranquil.browserUserAgent schema key (a plain string, no enum — see gotcha 1); and, in createTranquilSettingsPanel(), the editable combobox that replaces the auto-rendered field (an input + a role="listbox" preset list), with its subscription and document listener disposed in the panel’s destroy().

tranquil-browser (apply layers 1–2)

  • lib/utils.jsCLEAN_USER_AGENT (the app UA with Electron/…/Tranquil/… stripped) and resolveUserAgent() (the override if set, else the clean default).
  • lib/tranquil-browser-view.jsuseragent: resolveUserAgent() on the <webview> attributes.
  • lib/tranquil-browser.js — a atom.config.observe('tranquil.browserUserAgent', …) that calls setUserAgent on every open tab’s guest and ipcRenderer.send('set-user-agent', ua).

tranquil-client (apply layer 3, main process)

  • src/main-process/cz-init.js — a currentUserAgent seeded from a stripped app.userAgentFallback, a set-user-agent ipcMain handler that updates it, and a per-guest-session onBeforeSendHeaders that rewrites the User-Agent header (WeakSet-guarded so shared per-window partitions aren’t re-bound).

tranquil-business-dark / tranquil-business-light (styling)

  • styles/settings.less — the .tq-ua-combobox rules (field, chevron toggle, and the overlay preset listbox), added to both themes.

Gotchas worth remembering

  1. A config enum can’t be an editable dropdown. Pulsar’s config core rejects any value outside a declared enum, and settings-view renders an enum as a closed <select>. To get “presets and free text,” the schema must be a plain string and the dropdown must be our own DOM on the owned Tranquil tab.
  2. The <webview useragent> attribute persists across in-tab navigations, so it covers a tab for its whole life without a main-process round-trip. But setUserAgent on an already-open tab only takes effect on the next load — reload to apply it to the currently displayed page.
  3. The attribute / setUserAgent miss subresources and WebSockets (electron #7203 / #7205), which is exactly why the main-process onBeforeSendHeaders rewrite exists — it’s the only layer that reaches every request.
  4. Config is renderer-side; the header rewrite is main-side. The value crosses via the set-user-agent IPC message. The main process seeds its own clean default (stripped app.userAgentFallback) so guest requests are clean even before the renderer’s first push.
  5. Main-process code only reloads on a full quit. A window reload refreshes only the renderer, so while iterating on cz-init.js you need a full restart, not a reload.

Follow-ups

  • Bump the fixed Firefox/Safari preset strings when they drift — Chrome and Edge track the bundled Chromium automatically, but Firefox and Safari carry hardcoded engine/version tokens.
  • The preset listbox overlays absolutely under the field; if a scroll container ever clips it, revisit the overlay positioning.