~/fireleash/field-manual ● beta v1.1.01
FireLeash
Field Manual — knowledge base & lessons learned

FireLeash

A Firefox-first tiling browser shell. Every pane is a real Firefox window — tiled like a window manager, all sharing one login.

1 → N
one firefox process, N tiled windows
1 jar
one cookie jar · import login once
0 flags
no automation flag → no Google block
~14 GB
reclaimed on the proper install
what it is

Real browsers, tiled — not an embedded webview

You want several logged-in web apps side by side. Embedded webviews (Electron BrowserView) get refused by Google's sign-in policy, and one browser-per-pane means one login to babysit per pane. FireLeash instead runs the genuine Firefox, embeds its windows into a tiling shell, and points them all at a single profile.

 FireLeash shell (Electron 31, GPU accel off)
pane 1owned window
pane 2owned window
pane 3owned window
 each pane = one window, forwarded via -new-window 
one firefox.exe · one profile · one cookie jar → sign in once, every pane is signed in
architecture

Five load-bearing decisions

embed

Owned top-level windows

Each pane is a real top-level browser window made an owned window of the shell (GWLP_HWNDPARENT), stripped to WS_POPUP+WS_EX_TOOLWINDOW, positioned in screen coords. Not a child window — owned top-levels keep their own DWM surface, so they never blank. The komorebi / GlazeWM model.

login

One shared profile

The first pane launches Firefox; every later pane is a -new-window that forwards into that same profile-scoped process. Same profile → one cookie jar → the login imported once is shared by every pane, for free.

identity

HWND-diff, never titles

Each new window is pinned to its pane by a before/after HWND diff of the process's windows — because Firefox shows the raw URL in the caption, not a stable title. Title-based identity was the bug that sank the first attempt.

access

Un-driven, single chrome

No -marionette, no --remote-debugging-portnavigator.webdriver = false → Google signs in normally. Firefox's own toolbar is hidden; the FireLeash omnibox drives each pane over a guarded autoconfig channel (still no automation flag). Automation flags are the block, not the user-agent.

state

Lean inherit + safe config

First run copies cookies, key4.db, logins, extensions & localStorage from the real profile but skips IndexedDB/cache (GBs, useless for auth). Config saves are atomic with a rolling .bak; the app lives in its own clean %APPDATA%\FireLeash.

the mark

A leash of foxes

FireLeash needed a mark that read as many browsers, one grip. The answer is three fox heads locked together in a fire ramp — a leash of foxes. Raster is the final master; a vector trace was attempted and rejected for flattening the ember gradients.

FireLeash mark
three heads

Exactly three — a leash is three foxes. Not two, not a pack.

fire ramp

Yellow → orange → deep red — the same ember the whole UI runs on (#ff7a1a at the core).

near-black

Always on a near-black ground (#0b0b0d). The mark glows; it never sits on white.

placement

Full-res is the hero: greeting (140px) and the new-tab split (300px, locked). The 32px asset in the toolbar; the icon ladder (16–256) for the app & taskbar. Sparingly — it's a hero.

flat

The mark sits flat — never a glow, drop-shadow, or halo. The ember in the art carries the warmth.

split

On a fresh split it's centered across the content, so the divider halves it; each real Firefox pane covers its side, so the mark drops out a half at a time as panes fill.

the failure ledger

What we tried, and why it broke

The wrong-architecture patterns only showed up across attempts. Each fix below was earned by a dead end — kept here so none of them get re-tried.

L1Reparenting with SetParent / WS_CHILDpanes blank on defocus
Fail

Cross-process child windows went blank whenever the shell lost focus or was dragged — DWM stops servicing a reparented child's surface. Survived every GPU / occlusion / software-render flag.

Fix

Owned top-level windows via GWLP_HWNDPARENT — never WS_CHILD. Owned windows composite independently and cannot blank by construction.

L2Identifying pane windows by titlepanes float, stuck on a sentinel
Fail

Firefox puts the data-URL in the window caption, not a <title>. Panes floated instead of embedding, stuck on a sentinel bootstrap, then regressed to processes with no window at all.

Fix

Identify each new window by a before/after HWND diff (EnumWindows over the process, one window opened at a time).

L3A separate Firefox profile per panenew tab isn't signed in
Fail

Each pane re-imported the login into its own profile — “new tab doesn't bring in the Google account I just imported.” Logins also drifted apart over time.

Fix

One shared profile for all panes; a pane is a -new-window of the single process. Import once, shared everywhere.

L4Built-in Chromium (BrowserView) paneswebview OAuth refused by Google
Fail

They run in a session separate from Firefox (never signed in), and Google's policy refuses OAuth inside an embedded webview regardless of UA spoofing — by design, not a bug.

Fix

Firefox-only. Built-in Chromium retired from every new-pane path and from Settings; genuine browser windows do OAuth normally.

L5Driving the browser (Marionette / debug port)“browser may not be secure”
Fail

-marionette sets navigator.webdriver=true; Chrome's --remote-debugging-port trips its own client-side block — both yield Google's “this browser may not be secure.”

Fix

Launch native & un-driven. Where programmatic control is needed, a guarded in-profile autoconfig WebSocket channel drives it — no automation flag on the wire.

L6Baking autoconfig into a copied FirefoxSxS “mozglue not found”
Fail

Copying a running, auto-updating install produced an inconsistent mozglue → Windows SxS “mozglue could not be found.”

Fix

Install the guarded autoconfig into the real Firefox install (elevated; promptless with UAC off). Guard makes it inert for every non-FireLeash profile.

L7Inheriting the whole profile storage/new panes froze for minutes
Fail

One profile's facebook.com + messenger.com IndexedDB alone was ~2.5 GB. The copy is awaited before the window spawns, so new panes froze for minutes (“not loading”).

Fix

Copy storage/permanent fully; for storage/default skip idb+cache. Auth is cookie-based — profile drops ~2800 MB → ~260 MB.

L8Overriding termsofuse.* to skip onboarding“Welcome to Firefox” blocks it
Fail

Setting an old accepted-date made Firefox treat the inherited Terms acceptance as stale and re-prompt with the “Welcome to Firefox” modal — which blocked the pane.

Fix

Never touch termsofuse.*. The inherited prefs.js already carries the user's real, current acceptance.

L9Resetting config while the app was runningreverts to old workspace on close
Fail

Writing config.json under a live instance — it held the old config in memory and wrote it back on quit. Looked like “it reverted to my old workspace after I closed it.”

Fix

Never write the live config while running: stop the app, then write, then relaunch. Backed by a clean single-folder install.

L10Non-atomic save + default-fallback resave4 workspaces collapsed to 1
Fail

A torn write plus a load path that resaved the one-group default over a real config → a 4-workspace layout silently collapsed to one, no backup.

Fix

Atomic save (temp + rename), rolling .bak, corrupt-file preservation, and never overwrite a real config with the default.

hard-won gotchas

The traps that cost hours

koffi ABI

The FFI addon is built for Electron's ABI. Run window-enumeration spikes under Electron, not plain node.

launcher pid

Firefox's launcher relaunches into a different pid that owns the window. Identify our FF by the profile-path token in its command line, not the spawned pid.

-no-remote

It disables the remote server that -new-window forwards into. The shared model must NOT pass it for the first window.

no global WebSocket

Electron 31 main = Node 20 has none. The control server must require('ws').

UAC off ≠ elevated

A normal process still can't write Program Files. Use Start-Process -Verb RunAs (promptless with UAC off).

GetWindowLongPtr

Not a real 64-bit user32 export (it's a macro). Call GetWindowLongPtrW, or the enumeration throws mid-callback.

detached firefox

A spawn-detached Firefox survives killing the shell and holds the profile / floats orphan windows. Kill it explicitly by tag.

case-insensitive paths

Windows paths are case-insensitive: %APPDATA%\App and \app are one folder, not two. Dedupe before deleting or the counts lie.

silent stdout

Electron GUI console.log is invisible from the Windows console. Have spikes write results to a file.

koffi handles

HWNDs come back Number-or-BigInt. Normalize to BigInt at the boundary or === silently mismatches.

GPU compositing

Chromium's DirectComposition paints over native child windows. app.disableHardwareAcceleration() on the shell — it only draws chrome.

re-asserted geometry

A native window re-applies its own saved position on startup and overrides ours. Re-assert the layout a few times (400–7000 ms) until it settles.

nativeHandle = HWND

nsIBaseWindow.nativeHandle from chrome JS is the top-level HWND (a hex string) — it matches EnumWindows after BigInt-normalizing. That's per-window identity on one shared process.

start() race

A control server's start() must be concurrency-safe: hand simultaneous callers the same in-flight promise, never resolve with a port that isn't assigned yet (0).

userChrome !important

A user-origin !important beats an inline style, so you can't reveal a hidden toolbar by setting inline CSS. Gate the collapse behind :not([attr]) and toggle the attribute.

lessons, crystallized

What FireLeash taught

  1. Embed real windows as owned top-levels, never children. It's what production tiling window managers ship, and it's the only thing that doesn't blank.
  2. Identify windows by structural diff, never by caption. Titles are page-authored and unstable; a before/after HWND set is ground truth.
  3. One profile, many windows, is shared login for free. Firefox forwards -new-window into the running profile instance — lean on the browser, don't reinvent sessions.
  4. Don't drive the browser if you want it to sign in. The automation flag is the block, not the user-agent. Native & un-driven beats any spoof.
  5. Prove the risky mechanic in an isolated throwaway spike first. Shared login was reverted once for skipping this; the spike that replaced it passed on the first real integration.
  6. Never write the live config under a running app, and make every save atomic — a workspace is the one thing you can't ask the user to rebuild.
  7. Keep a running failure ledger. The wrong architecture is invisible in any single attempt; it only rhymes across them.
state & roadmap

Where it stands

✓ shipped & verified

Owned-window tiling — real FF panes, resizable, never blank
Shared login — N panes = 1 process = 1 cookie jar (proven)
Firefox-only — built-in Chromium retired
Unified omnibox drives the exact shared pane (per-window HWND identity)
Single chrome — Firefox toolbar hidden; reveal-on-demand toggle for bookmarks/extensions
Config safety — atomic save, own clean folder, proper NSIS install (~14 GB reclaimed)
Launch status cards + live import file-counter