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.
| Piece | State |
|---|---|
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:
- 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). - 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-agentIPC message. - Header hardening — the main process rewrites the
User-Agentrequest header on every guest request viaonBeforeSendHeaders, covering the subresource/WebSocket requests the attribute andsetUserAgentmiss.
Where everything lives
tranquil-config (setting + UI)
src/index.js—BROWSER_UA_CHROME+BROWSER_UA_PRESETSmodule constants; thetranquil.browserUserAgentschema key (a plainstring, noenum— see gotcha 1); and, increateTranquilSettingsPanel(), the editable combobox that replaces the auto-rendered field (an input + arole="listbox"preset list), with its subscription and document listener disposed in the panel’sdestroy().
tranquil-browser (apply layers 1–2)
lib/utils.js—CLEAN_USER_AGENT(the app UA withElectron/…/Tranquil/…stripped) andresolveUserAgent()(the override if set, else the clean default).lib/tranquil-browser-view.js—useragent: resolveUserAgent()on the<webview>attributes.lib/tranquil-browser.js— aatom.config.observe('tranquil.browserUserAgent', …)that callssetUserAgenton every open tab’s guest andipcRenderer.send('set-user-agent', ua).
tranquil-client (apply layer 3, main process)
src/main-process/cz-init.js— acurrentUserAgentseeded from a strippedapp.userAgentFallback, aset-user-agentipcMainhandler that updates it, and a per-guest-sessiononBeforeSendHeadersthat rewrites theUser-Agentheader (WeakSet-guarded so shared per-window partitions aren’t re-bound).
tranquil-business-dark / tranquil-business-light (styling)
styles/settings.less— the.tq-ua-comboboxrules (field, chevron toggle, and the overlay preset listbox), added to both themes.
Gotchas worth remembering
- A config
enumcan’t be an editable dropdown. Pulsar’s config core rejects any value outside a declaredenum, and settings-view renders anenumas a closed<select>. To get “presets and free text,” the schema must be a plainstringand the dropdown must be our own DOM on the owned Tranquil tab. - The
<webview useragent>attribute persists across in-tab navigations, so it covers a tab for its whole life without a main-process round-trip. ButsetUserAgenton an already-open tab only takes effect on the next load — reload to apply it to the currently displayed page. - The attribute /
setUserAgentmiss subresources and WebSockets (electron #7203 / #7205), which is exactly why the main-processonBeforeSendHeadersrewrite exists — it’s the only layer that reaches every request. - Config is renderer-side; the header rewrite is main-side. The value crosses via the
set-user-agentIPC message. The main process seeds its own clean default (strippedapp.userAgentFallback) so guest requests are clean even before the renderer’s first push. - Main-process code only reloads on a full quit. A window reload refreshes only the renderer, so
while iterating on
cz-init.jsyou 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.