# Gotchas — landmines this codebase has already stepped on Every one of these caused a real bug or debugging spiral. Read before touching the adjacent code. ## Angular (v22, zoneless, signals, OnPush) - **Never consume push streams through signal+effect.** An effect that reads a signal and (directly or transitively) writes signals re-triggers itself — this froze the entire app on the first transfer. Push messages (transferUpdate etc.) go through plain callback registries in RpcService. Signal coalescing would also silently drop intermediate progress snapshots. - **No signal writes during template render** (NG0600). Anything that loads panels from code paths reachable during render defers with `queueMicrotask` (see `PanelsService.newWorkspace`). - **`[value]` on ``: utilities outrank the base layer, so a `px` utility would erase the padding-right the drawn caret lives in. - **A custom `::-webkit-scrollbar` turns off macOS overlay scrollbars.** The moment the pseudo-element is styled, WKWebView draws classic always-visible scrollbars that take layout width. Deliberate here (square design); don't reach for it to "tint" scrollbars without accepting that. - **An icon size is one of the four `--icon-size-*` tokens, never a one-off rem.** TDesign glyphs are 24-unit SVGs with 2-unit strokes, so a fractional size (0.9rem = 14.4px) lands every stroke between device pixels and blurs it. Per-template picks had drifted to 12 sizes, and neighbours stopped matching: a 14.4px cog beside a 12.8px +, and a 12px + beside 9.6px × in the shortcut chips. ## The interface scale (root font-size lever) - **electrobun's native page zoom is a silent no-op on Linux — do not reach for it.** `webviewSetPageZoom` in the shipped `libNativeWrapper.so` (v1.18.1, and the `_cef.so`, and the copy in our own build output) is a compiled empty-body stub: the disassembly has no `call` instruction, and `webkit_web_view_set_zoom_level` is not even in the library's imports. `webviewGetPageZoom` returns a hardcoded `1.0` from `.rodata`, so the failure is unobservable through the API — set 2.0, read back "1.0 = success". The JSDoc's "(WebKit only)" means macOS WKWebView. This is why the scale is CSS. (Disassembled on 1.18.1; 2.0.2's zoom fixes are CEF-only — re-check the native Linux wrapper before trusting it.) - **Everything the UI sizes with must be rem.** The scale works by setting the root font-size, so a px value freezes while everything around it grows. The px reservoirs were the panel-chrome tokens (`--row-h` etc.) and 93 `text-[11px]` arbitraries. 1px hairlines and focus rects stay px deliberately (crisp at any scale). - **px constants in TS that mirror rem-sized CSS are latent scale bugs.** The context menu's `MENU_W = 224` was a hardcoded mirror of `w-56` (14rem): box grew, math didn't, flip fired at the wrong edge. Convert through `@core/metrics` `remPx()` at use time — it reads the LIVE root font-size, and per call, since the scale changes mid-session. The editor's canvas-based `charWidth` is cached keyed on the computed font string for the same reason. ## Electrobun - **The `electrobun` npm package (2.x) is a bootstrap whose every export is a throwing stub** ("Electrobun 2.x APIs come from the Hutch devkit, not node_modules"). Every `electrobun/*` import must resolve through the projected `.hutch/devkit/`: the root tsconfig extends `.hutch/devkit/tsconfig.json`, and `ui/tsconfig.json` maps `electrobun/view` to `../.hutch/devkit/api/browser/index.ts` (the exact export-map target). Let an import fall through to `node_modules/electrobun` and the webview throws at load. tsc and the UI build fail on unresolved `electrobun/*` imports until `bunx electrobun prepare` has run (postinstall and every dev/build script do it). Never point anything into `~/.hutch` — that store is Hutch-owned. - **`build.mainProcess` defaults to Cottontail in 2.x**, and Cottontail ignores the whole `build.bun` block — the ssh2 externals included, silently (verified with `bunx electrobun config --env=stable`). `mainProcess: "bun"` in `electrobun.config.ts` is load-bearing. - **The browser SDK is raw TS compiled inside the Angular program**, so the UI's strictness flags apply to upstream code: 2.0.2's `createTransport().send` returns on one path only, which is why `ui/tsconfig.json` has `noImplicitReturns: false`. Its `import "./global.d.ts"` also prints "File '…/global.d.ts' not found in TypeScript compilation" on every build — benign, and adding the file to the program does not silence it. - **Window `close` is not cancellable, but 2.x added `will-close`.** The `close` event's response type is `{}`; `will-close` (fired by the titlebar/traffic-light close and `win.requestClose()`) honors `event.response = {allow:false}`, and `win.close()` skips it. The editor close safety predates this: all unsaved-state guards still intercept app-controlled close paths (⌘W, buttons) or survive the close (editor draft mirroring) — a `will-close` prompt is a possible follow-up. - **`Updater.applyUpdate()` asks the `before-quit` handlers first (2.x)**, so the transfers/unsaved guard can veto an update restart. Its "Quit Anyway" must re-run `Updater.applyUpdate()` (`events.applyUpdate` flags the quit as update-initiated) — a plain `Utils.quit()` there quits without installing. `Utils.quit()` runs the same approval, which is why the guard checks `quitConfirmed` first. - **macOS ⌘-keys go to the PAGE first; only unclaimed ones reach the app menu.** WKWebView's `performKeyEquivalent` hands key equivalents to the page while the webview is first responder (WebKit `WebViewImpl.mm`: "lets webpages have a crack at intercepting key-modified keypresses"); a key the page `preventDefault`s never reaches the menu. Old comments here claimed the opposite ("the menu intercepts ⌘W first") — what was really happening is that windows which didn't claim ⌘W let it fall through to the menu's Close Tab. Two consequences: a handled shortcut MUST `preventDefault` (`claim()` does), or the menu runs it a second time; and a ⌘W a secondary window doesn't claim still arrives as the MAIN window's `close-tab` action — bun checks the focused Settings window, then editors, first and closes THAT, so a new window kind must either claim close-window or join that check. - **One rpc instance per window** — BrowserView binding steals the rpc's transport; a shared instance silently sends to the wrong webview. - **RPC before socket open was silently lost on 1.18.1.** 2.x Electroview queues sends while its `hostSocket` (v1: `bunSocket`) is CONNECTING and falls back to the native bridge; RpcService still gates everything on the socket-open promise (`ready`) as belt-and-braces. - The emitter has no `off()`. Window event closures must not capture records you want garbage collected. - `electrobun build` defaults to the **dev** channel; release builds need `--env=stable`. The stable app name differs from the dev one (`miniSCP` vs `miniSCP-dev`), including the app-data dir implications when testing. - Tray: never call `setImage` after construction on macOS — the template treatment is lost and the icon renders black on dark menu bars. - `ApplicationMenu` is macOS-only in our usage; accelerators on Windows are single-character only. - **`ContextMenu.showContextMenu` is a silent no-op on Linux** (electrobun's own `.hutch/devkit/api/sdks/main/proc/linux.md` says so, still in 2.0.2; the app also prints "Application menus are not supported on Linux" at boot). Because the UI suppresses the webview's own right-click menu, that left EVERY panel context menu dead on Linux — no error, nothing happens. The menu is plain HTML now (`ui/src/app/skins/shadlike/context-menu.ts`) on every platform; don't reintroduce the native one for "nativeness" on mac/win without keeping a Linux path. - **Window-open flash prevention is three-layer** — remove any one and the white/inverted blink returns: (1) windows are created `hidden: true` and revealed by `showWhenReady` on the webview's first `dom-ready` (800 ms fallback), (2) the bun preload bakes the persisted `data-theme`/tone attributes and the interface scale before first paint (`buildPreload`), (3) `ui/src/index.html` carries an inline `