Migrate to Electrobun 2.0.2 (Hutch)

Electrobun 2.x replaces the vendored CLI with Hutch: the npm package is now a
bootstrap whose exports are a throwing stub, and the SDK is projected into
.hutch/devkit. The app stays on the Bun main process — `mainProcess: "bun"` is
load-bearing, since the 2.x default (Cottontail) ignores build.bun and with it
the ssh2 externals — and the bundle now ships the pinned Bun 1.4.0.

- electrobun 2.0.2 as an exact devDependency. `electrobun prepare` takes the
  old CLI re-sign step's slot in postinstall and every dev/build/start/release
  script: tsc (root tsconfig extends the devkit's) and the Angular build
  (electrobun/view mapped into the devkit) resolve the SDK there.
- Imports move to electrobun/main. The CLI re-sign script and the v1 "three"
  type shim are gone (Hutch's binaries are validly signed).
- UI: Electroview's bunSocket is hostSocket; noImplicitReturns is off because
  the devkit's raw-TS browser SDK compiles inside the UI program.
- Updater.applyUpdate() now consults before-quit, so the transfers/unsaved
  guard's "Quit Anyway" re-runs the apply instead of quitting without it.
- 2.x applies trafficLightOffset from macOS's default button position (1.18.1
  clamped it inside the titlebar); {12, 4} centres the lights in the header.
- Releases: mac-x64 dropped (2.x publishes no macOS x64 core, so no Rosetta
  path). artifacts/ is emptied before each build and published verbatim.
  Installers lost the stable- prefix (the update feed kept it), so release.ts,
  deploy.sh and the site links follow. Linux ships Electrobun's Setup
  installer instead of the portable tarball and its miniSCP.sh wrapper.
- Docs cover all of the above, plus re-checks against 2.0.2: still no native
  modal windows; window close is now cancellable through will-close.

Verified: tsc, build:ui, a stable mac-arm64 release (dmg + the v1-compatible
stable-macos-arm64 feed) and dev launches (RPC, Settings window, traffic
lights). Windows/Linux builds and the v1 -> 2.x Windows update bridge still
need their first run on native machines.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-10-05 11:08:03 +02:00
co-authored by Claude
parent 9bd237d89c
commit 0eb5608873
30 changed files with 431 additions and 447 deletions
+26 -14
View File
@@ -6,15 +6,21 @@ Dense reference for development and debugging. Companion files:
## Process model
- **Bun main process** (`src/bun/`): electrobun runs it inside a WebWorker of
its native launcher. Owns every privileged capability: filesystem, SSH,
window management, native menus/dialogs/tray, the connection/settings stores.
- **Bun main process** (`src/bun/`): the native launcher runs electrobun's
`main.js` on the Bun the 2.x devkit pins (1.4.0, shipped in the bundle —
`build.mainProcess: "bun"`), which starts our bundle in a Worker. Imports the
SDK as `electrobun/main` (projected into `.hutch/devkit/`). Owns every
privileged capability: filesystem, SSH, window management, native
menus/dialogs/tray, the connection/settings stores.
- **Webviews** (`ui/` Angular): one per window, system WebKit on macOS /
WebView2 on Windows / WebKitGTK on Linux. Zero privileged access — everything
goes through RPC.
- **RPC transport**: one WebSocket server in the bun process, multiplexed by
`webviewId` (each webview auto-connects using natively injected globals).
Requests are typed by the `AppRPC` schema in `src/shared/rpc.ts`.
`webviewId` (each webview auto-connects using natively injected globals;
packets AES-GCM-encrypted per webview). 2.x Electroview queues sends while
its `hostSocket` connects and falls back to a native bridge when the socket
is unavailable. Requests are typed by the `AppRPC` schema in
`src/shared/rpc.ts`.
### The one-schema rule
@@ -39,7 +45,9 @@ Settings window). `rpc.send.*` on an instance targets exactly its own webview.
exist until after construction). `AppDeps.events` is a mutable object of
no-op functions, reassigned in `src/bun/index.ts` after everything exists:
`sessionClosed`, `connectionsChanged`, `openEditor`, `settingsChanged`,
`openConnectionManager`, `openSettings`, `showAbout`.
`openConnectionManager`, `openSettings`, `showAbout`, `applyUpdate` (the
update restart lives in index.ts beside the before-quit guard it must get past —
see build-and-release.md "Auto-updater").
## Window kinds
@@ -50,9 +58,9 @@ scale so the first paint is already right. `ui/src/main.ts` picks the root
component from `APP_MODE`; all four roots use selector `app-root` (index.html
hosts only that tag). `chromeOptions()` is the ONLY place the native frame is
decided, spread by the main window and `openSecondaryWindow`: macOS `hiddenInset` +
`trafficLightOffset {x:12,y:13}` (the UI renders an `h-10 pl-24` drag-region
header via `IS_MAC_CHROME`); other platforms keep the native titlebar and a
compact `h-9` non-drag header.
`trafficLightOffset {x:12,y:4}` (2.x semantics — see gotchas; the UI renders an
`h-10 pl-24` drag-region header via `IS_MAC_CHROME`); other platforms keep the
native titlebar and a compact `h-9` non-drag header.
`WindowRegistry<Rec>` (same file): keyed window map with dedupe/activate,
cascade placement off an anchor frame, focus tracking. electrobun's emitter
@@ -67,8 +75,9 @@ through a concrete session, and `local` is one shared session anyway).
Re-opening focuses the existing window. The webview fetches its own content
(`readFile`) — `openEditor` just creates the window with ctx in the preload.
**Close safety** (electrobun window `close` is NOT cancellable — verified, no
`allow` response like `before-quit` has):
**Close safety** (designed when electrobun window `close` was NOT cancellable —
1.18.1 had no `allow` response like `before-quit`; 2.x adds a cancellable
`will-close` for the titlebar close, not used yet — see gotchas):
1. ⌘W (routed via the app menu → `editorCloseRequest`) / Close button →
styled in-window Save/Don't Save/Cancel; the confirm path calls
`editorClose` which sets `suppressPrompt` before `win.close()`.
@@ -92,7 +101,9 @@ not `findInPage`, which is a shim on WebView2).
### Connection-manager window (`src/bun/manager-window.ts`)
Single instance, centered over main, **modal to main only** (editors stay
usable). No native modality exists, so three layers approximate it: bun
usable). No native modality exists (re-checked on 2.0.2: no parent/owner/sheet
option or input lock in any SDK or the core's exports; `styleMask.DocModalWindow`
is only the cosmetic NSWindow bit), so three layers approximate it: bun
bounces main's `focus` event back to the manager; the main webview renders a
click-swallowing overlay (`managerState` message); main drops its keyboard
shortcuts and bun drops main-targeted menu actions while open. Connect /
@@ -208,8 +219,9 @@ component; tokens → `ui/src/styles.css`.
through plain callback registries, never signals**: a signal consumed from
an effect that also writes signals re-triggers itself (this froze the whole
app once), and signal coalescing drops progress snapshots. All requests are
gated on the socket-open promise — calls fired during boot would otherwise
be silently lost. `request` and `send` are ready-gated Proxies.
gated on the socket-open promise (`electroview.hostSocket`) — on 1.18.1 calls
fired during boot were silently lost; 2.x Electroview queues them itself, so
the gate is belt-and-braces now. `request` and `send` are ready-gated Proxies.
- `panels.service.ts` — `PanelState` (per panel): sessionId, fsKey, cwd,
entries, sort, cursor, selection, type-to-filter. `sorted` computed:
filter → dirs-first sort (name/size/mtime numeric, kind/rights/owner/group