The previous run went green and produced an installer with no system-audio
capture in it — the exact silent failure this module exists to prevent.
Two causes. electron-rebuild takes -w as one comma-separated list, not a
repeated flag; passing it twice made argv.w an array and the CLI threw
'argv.w.split is not a function', which also broke uiohook-napi's rebuild that
had been working. And pnpm 10 refuses to run a dependency's build script unless
it is listed in onlyBuiltDependencies, so node-gyp never ran for it at all —
the log said 'Ignored build scripts' and nothing else complained.
Neither surfaced because desktop's postinstall ends in , which
exists so contributors without build tools can install. That is reasonable
locally and dangerous in CI, so the workflow now asserts a compiled .node
exists and fails loudly when it does not, instead of trusting an exit code that
was designed to lie.
First release built from the fork. electron-builder names artifacts and the
GitHub release from package.json rather than the git tag, so the two have to
agree or electron-updater compares mismatched versions and never offers the
update. v1.0.0 is already taken by upstream's tag in this history.
release.yml already builds and publishes desktop installers for every platform
on a v* tag, so no new workflow was needed — it needed adapting to the fork.
electron-builder published to upstream's repository, so releases (and with them
the electron-updater feed, which had no source at all) went nowhere useful.
More consequential: postinstall ran electron-rebuild against uiohook-napi only.
electron-native-screenshare would have been compiled for Node's ABI rather than
Electron's and failed to load at runtime — and since the loader degrades
quietly by design, the symptom would have been screen sharing with no sound and
no error, which is the exact bug this module exists to fix.
Documented in docs/build-desktop.md, including why the build has to run on
GitHub (Gitea Actions provides no hosted runners, and the module needs MSVC)
and why the Windows runner stays pinned to windows-2022.
Electron's audio: 'loopback' captures the whole output mix, this app's own
playback included — so everyone else's voices went back out inside the share
and each listener heard themselves. Not acoustic echo but a digital copy of
the output, which is why headphones never helped, and why shareAudio already
defaulted to off in the desktop app.
Electron offers no way to exclude our own audio: the docs allow only
'loopback' or 'loopbackWithMute', and the handler discards the renderer's
constraints (restrictOwnAudio never arrives). electron-native-screenshare does
it at the OS level — WASAPI process loopback on Windows — capturing only the
shared window when its pid resolves, and otherwise everything except us.
The module hands raw PCM to the main process, so it crosses IPC and is
scheduled onto a running cursor in Web Audio to become a MediaStreamTrack,
published as ScreenShareAudio. Loading is optional and failure degrades to a
silent share rather than blocking the app or the screen share.
The browser path is untouched: Chrome honours restrictOwnAudio and has no echo.
Verified by typecheck (web and Electron main) and the web suite. The audio path
itself cannot be exercised here — no Windows, no Electron, no audio device.
The @backspace/desktop postinstall runs `electron-rebuild -f -w uiohook-napi`,
which compiles a native module from source and needs a C++ toolchain
(make/g++/python3). On a bare host without those — a typical VPS a self-hoster
clones onto — that failure took the ENTIRE `pnpm install` down (exit 255), so the
server never installed either, even though uiohook-napi is desktop-only and
irrelevant to hosting.
Make the rebuild non-fatal: on failure it now prints a clear one-line warning and
continues. Machines with build tools (CI, desktop devs) are byte-identical — the
fallback never fires; only toolless boxes (which aren't building the desktop app
anyway) skip it. Verified on a bare x86 box: full `pnpm install` now exits 0 and
the server builds and boots from source. Docs note the build-tool requirement for
desktop work and point self-hosters at the Docker installer.
The flat app-icon.svg's gradient B mark has a bright (#fff) sheen that runs
to the badge perimeter with no dark separation. At favicon sizes (16/32px)
that edge anti-aliases into a white halo that reads as a border around the
icon — visible in Safari browser tabs, and the same defect in the small
Windows .ico / Linux launcher reps that also rendered from the flat SVG.
The committed 3D raster masters (used by every >=128px output already) frame
the mark in a dark surround and stay clean down to 16px. Set RASTER_THRESHOLD
0 so all app-icon sizes route through the raster path; the flat SVG is kept
as a gated source, re-enablable only with a corrected flat mark. Regenerated
favicons + small desktop reps; output remains byte-deterministic. Updated the
generator header/comments, README source matrix, and the dated icon spec.
Replaces desktop and web icon binaries with the new brand mark, ships
PNG raster exports under assets/brand/ alongside the existing SVG
sources, adds Alternative Styles artwork variants, and updates the
icon generation script and System Prompt doc. deploy.sh excludes
the new local-only artifacts (*.rtfd, assets/brand, electronbuild.sh,
multi-platform-roadmap.md) so they don't get rsync'd to the live
boxes. Drops ARCHITECTURE_AUDIT.md (obsolete).
Electron derived userData from package.json's `@backspace/desktop` name, leaking
the monorepo's pnpm scope into ~/Library/Application Support/. Now `app.setName`
runs at module load before any userData consumer, and a one-shot migration
atomically moves the historical folder to <appData>/Backspace, cleaning the
empty @backspace/ parent. Conservative on conflict — never clobbers an existing
populated target. EXDEV fallback to recursive copy. Smoke-recovery path flipped
back to Backspace.
CRITICAL BUG. In real SPAs, useEffect fires during document load (microtask
after bundle execute + React render), which is BEFORE did-finish-load fires
(after window.onload). Without this fix, the ping arrived when bootArmed=false
(no-op), then did-finish-load armed a timer nothing would clear → 20s later
every successful packaged build falsely entered recovery.
Caught by smoke scenario 13 (positive control: page that DOES ping should NOT
recover). The smoke proved the page's script ran AND the ping was sent, yet
recovery still fired.
Fix: module-level pingReceivedThisNav flag, reset on did-navigate, set in
handleRendererReady, checked in armBootTimer (early-return if true). Late-ping
case (ping after arm) preserved via existing 'if (bootArmed) clearBootTimer()'.
Also exports resetBootTimerStateForTest() to ensure full module-state isolation
between tests (pingReceivedThisNav is module-level and must not bleed across
test cases in the same run).
3 new tests pin the early-ping, late-ping, and per-nav persistence semantics.
48/48 tests pass. Build clean.
Spec + docs updated.
UX bug found during smoke testing: clicking Change Instance immediately
deleted the saved instance URL and showed an empty picker, with no way
back if the user changed their mind.
Fix:
- Don't clearInstanceUrl() in recovery action 'change-instance' — picker
is now non-destructive
- Picker pre-fills the input with the current saved URL when present
- Cancel button (shown only when a saved URL exists) returns to current
instance via idempotent setInstanceUrl re-save
- Header copy switches to 'Switch instance' / 'Cancel to stay' framing
when a saved URL is present
- URL only overwrites on explicit Connect to a different instance
Also: add console.log enter/exit lines in enterRecoveryMode and the
clear-recovery-state action handlers, so smoke-test scripts can grep
stderr for recovery activity without UI introspection.
Spec + docs/systems/desktop.md updated.
Two real bugs from final review:
- Clear recovery state on window 'closed' so macOS dock-activate doesn't
drop the recovery surface (window recreated with stale recoveryStore.mode)
- Hoist setOnQuitRequested before createWindow so synchronous boot failures
reach a wired Quit handler
Three polish items:
- Tray's Change Instance now routes through handleRecoveryAction so both
paths share one implementation; recovery action's change-instance also
show()+focus() for hidden-window tray clicks
- install-update action guards against state.updateState !== 'downloaded'
(defense in depth against malicious or buggy renderers)
- Object.freeze rationale documented in RecoveryStateStore.update
Eliminates the brief "Version: loading…" flash in the diagnostic block
on first render. Resolves getRecoveryState + getVersion in parallel via
Promise.all so the first render call has both. Drops the redundant
module-load .then() that set cachedVersion before render ran.
- rendererReady (boot-completion ping)
- getRecoveryState / onRecoveryStateChanged (recovery.html subscribers)
- recoveryAction (button click dispatcher with enum action)
Type declarations kept ambient (no export) to preserve window.backspace
global augmentation — exporting from an ambient .d.ts converts it to a
module and breaks the Window interface extension.
buildAppMenuTemplate's actions param is Partial<MenuActions>, so the
tray-only callbacks (onShow/onHide/onQuit) are simply ignored. Removes
the three-callback duplication between the two objects without changing
behavior. Future onChangeInstance/onCheckForUpdates/onRestartToInstall
changes only need to be made in one place.
- setMainWindow on createWindow, setMainWindow(null) on closed
- attachRecoveryHandlers wires Electron unresponsive/crash/load-fail events
- Store subscriber drives tray context menu + macOS app menu + mode-gated
recovery-state-changed push to renderer; single applyMenusForState
function shared between subscriber and initial fire (no drift)
- Old hard-coded createTray Menu and macOS app-menu construction deleted;
Win/Linux Edit-only menu retained as one-time setup for keyboard accelerators
- requestQuit exported, wired via setOnQuitRequested callback
- Recovery IPC handlers: renderer-ready, recovery-action, get-recovery-state
- setAppUserModelId('com.backspace.desktop') for Win32 notification attribution
- Extended showNotification with optional onClick (existing 2-arg callers unchanged)
Adds buildAppMenuTemplate pure function to recovery.ts that produces the
three-submenu macOS app menu (App/Edit/Window), reusing MenuActions and
checkForUpdatesItem from T4. Includes Restart to Install Update item
conditionally on updateState=downloaded. Tests use destructuring to satisfy
noUncheckedIndexedAccess. 26 tests pass, tsc clean.
Pure buildTrayMenuTemplate function returns MenuItemConstructorOptions[]
without constructing real Menu objects, enabling full test coverage.
All 7 new tests pass (22 total); MenuActions interface and
checkForUpdatesItem helper are intentionally unexported.
- Snapshot listener set before notifying so subscribers can subscribe/
unsubscribe during notification without breaking the pass
- Per-callback try/catch so one throwing subscriber does not silence others
- Object.freeze on each state object so the live reference returned by
get() cannot be accidentally mutated externally (compile-time
Readonly<> is hint only)
- 3 new tests pinning these invariants
Chromium gates the PulseAudio loopback path behind the
PulseaudioLoopbackForScreenShare feature flag. Without it, returning
audio: 'loopback' from setDisplayMediaRequestHandler rejects the whole
getDisplayMedia request, so screen share never starts when the user has
"Share system audio" enabled. Also surface a clear warning toast on
loopback failure (PipeWire-only without pulse compat, macOS without
Catap) instead of failing silently — no auto-retry, since the picker
selection is already consumed.
Strip the desktop package description to "Backspace" so Windows Task
Manager shows the bare product name instead of the long tagline.
Unify the web meta description and PWA manifest description on a single
positioning line that names Discord and TeamSpeak as the comparison
targets — improves link-preview copy and SEO surface.
icon.png at repo root and gen-icns.sh are replaced by the SVG-driven
generator. tray-icon@2x.png is a Cocoa convention only — macOS uses
the template, not the colored variant. None have remaining consumers.
Three branches now: macOS template (.png + @2x, auto-recolour), Windows
multi-size .ico (DPI auto-pick), Linux 22x22 PNG. Drops the runtime
.resize({16,16}) — pre-rendered assets are at correct platform sizes;
runtime resize re-introduced bicubic blur. Defensive fallback to the
programmatic blurple circle is preserved but should not trigger in
practice now that templates ship populated.
build/icon.png and build/icon.icns are now committed pre-rendered
artefacts produced by scripts/gen-icons.mjs. The cp lines that seeded
gen-icns.sh and the gen-icns.sh invocation itself are no longer needed.
Preserves the macOS dock-icon override (cp into electron.icns) — that
is independent of how the icns is generated.
win.icon now references the hand-crafted multi-size .ico (16/24/32/48/
64/128/256) rather than letting electron-builder synthesise from a
single 1024 PNG — the latter was the cause of the 'Windows looks low
quality' complaint. linux.icon explicitly references build/icons/ dir
mode (was implicit before).
First run of pnpm gen-icons against the new artwork. All 22 outputs
generated deterministically (verified by hashing twice). Replaces the
dated raster set everywhere — desktop app icons, tray, web favicons,
PWA, in-app logo.
When autostart is disabled, Windows deletes the Run registry entry, so
deriveStartMinimizedFromArgs(undefined) was returning false and silently
resetting the user's startMinimized preference the next time autostart
was re-enabled. Both the get- and set-auto-launch-settings handlers now
fall back to the disk-cached value when no OS entry exists, matching the
existing macOS/Linux fallback pattern.
macOS: add args:[--hidden] alongside openAsHidden for defence-in-depth detection on macOS 13+.
Windows: add enabled/path/args/name so re-enabling the toggle clears the StartupApproved\Run disable marker.
Linux: add deterministic name:'backspace' so the .desktop filename is stable across Electron/AppImage updates.