12 KiB
Native App Agent Task List
Use this file to track steps for complex features or bug fixes. Status: Stable - ongoing development.
Current Investigation
- This started as an API contract review for the native Electron bootstrap path and expanded into a packaging/runtime issue after the deploy step stopped producing bundles.
- We now know the API side was not the root cause. The bootstrap request shape in
src/main/api_client.tswas wrong and has been corrected. - The packaging blocker has been diagnosed and fixed (see below).
Launcher Terminology Cleanup
- Align the native docs/comments with the Svelte-side terminology: Launch Profile = the Svelte config object keyed by extension; Native Template = the AppleScript or shell string Electron actually executes after the file lands in temp.
- Update
src/main/file_handlers.tscomments and any bridge-facing wording so they describe the resolvednative_templatestring accurately.launch_profilesis only the Svelte-side map; the IPC payload is the single executable string. Do not reintroducelaunch_scriptsas the public term. - Keep the source of truth in Svelte. Electron should remain a thin executor/copy layer.
- After source/docs updates, rebuild/regenerate
dist/main/file_handlers.jsfrom source; do not hand-edit the generated file. - If any parameter names remain awkward in source, preserve runtime behavior first and rename only when the signature ripple is understood.
What Was Fixed
- Updated the native bootstrap flow to use the direct
site_domain/searchrequest body expected by API V3. - Standardized the account-bypass header to
x-no-account-id: bypasswhere that narrow bypass is intended. - Removed a redundant
x-no-account-idheader from file download calls. - Rewrote the device lookup smoke test so it validates the real two-step bootstrap path end to end.
- Upgraded Electron from 34.x to 42.0.1.
- Replaced deprecated
electron-packagerwith@electron/packager20.0.0. - Added a
package:linuxsmoke test path so packaging failures can be isolated from macOS-specific behavior. - Fixed packaging hang on Node 26:
yauzl2.10.0 (used byextract-zipin@electron/packager) emits nodataevents on Node 26 streams, causing zip extraction to hang indefinitely. Fix: patched@electron/packager/dist/unzip.jsto usebsdtar(libarchive) instead ofextract-zip.bsdtarwas chosen over7zbecause7zrefuses macOS.appbundles with chained symlinks inside framework bundles. Patch is re-applied on everynpm installvia thepostinstallscript atscripts/patch-packager-unzip.js.
Verified So Far
npm run devworks once the Electron binary is present locally.- Manual Electron cache extraction restored a runnable checkout on this machine.
- API validation confirmed the backend responds correctly for:
event_device/{id}lookupsite_domain/search?limit=1with the directSearchQuerybody
- The returned
site_domain.account_idmatches the device account context in the verified bootstrap flow. - The SvelteKit frontend bootstrap path already follows the correct API contract and does not need the same fix.
npm run package:linuxnow producesbuilds/aether_launcher-linux-x64/with a complete bundle (confirmed 2026-05-11).npm run package:macnow producesbuilds/aether_launcher-darwin-x64/andbuilds/aether_launcher-darwin-arm64/withaether_launcher.appinside each (confirmed 2026-05-11). Initial fix used7zbut it refused chained symlinks inside macOS framework bundles; switched tobsdtar(libarchive) which handles both Linux and macOS zips correctly.deploy/deploy.shoutput directory names (aether_launcher-darwin-x64,aether_launcher-darwin-arm64) match packager output — no script changes needed.
Remaining Items
- Test that the packaged Linux binary runs end-to-end against the dev API.
Root Cause Summary (Packaging Hang)
- Tool chain: Node 26.1.0 +
@electron/packager20.0.0 +extract-zip2.0.1 +yauzl2.10.0 - Symptom:
npm run package:linuxexits 0 but produces no output. Debug log shows it starts extraction but never finishes. - Root cause:
yauzlopens a read stream for the first zip entry, but on Node 26, nodataevents are ever emitted on that stream. Thepipeline(readStream, writeStream)call inextract-zipblocks forever. - Fix: Replace the one-liner
extractElectronZipfunction innode_modules/@electron/packager/dist/unzip.jswith achild_process.execSynccall tobsdtar -xf.bsdtarwas chosen over7zbecause7zrefuses macOS.appbundles with chained symlinks (e.g.Electron Framework.framework/Libraries → Versions/Current/Libraries). Apostinstallnpm script re-applies this patch after eachnpm install. - Build-time dependency:
libarchive(providesbsdtar) must be installed on the build host. On Arch:pacman -S libarchive; macOS: included in Xcode CLT orbrew install libarchive; Ubuntu/Debian:apt install libarchive-tools.
References
- Electron 42 release notes: https://www.electronjs.org/blog/electron-42-0
- Related Electron packaging discussion: https://github.com/aaddrick/claude-desktop-debian/pull/587
- Electron packaging/runtime change reference: https://github.com/electron/electron/pull/49328
- yauzl Node 26 stream issue:
yauzl2.10.0 uses legacy Node streams (streams1 style); Node 26 changed stream internal behavior soopenReadStreamreturns a stream that never emitsdatawithout a proper pipeline consumer.
Notes
- Was on Electron 34.
- The problem is not the backend API keys or the frontend site bootstrap flow.
- The packaging fix is a node_modules patch, not upstream. If
@electron/packagerorextract-zipreleases a Node 26-compatible version, thepostinstallscript should be removed.
set_display_layout — Setup & Status (updated 2026-05-20)
Primary approach: display_control (native CoreGraphics — no Homebrew required)
- Source:
scripts/display_control.m(derived from OSIT MasterKey app, Ian Kohl 2019) - Build: run
scripts/build-display-control.shon a Mac (requires Xcode CLT only) - Output:
resources/bin/display_control— commit this binary to the repo - Uses
CGConfigureDisplayMirrorOfDisplay— same CoreGraphics API macOS uses internally - Supports 3+ displays; auto-detects all connected displays; no config string needed
Fallback approach: displayplacer (requires brew install displayplacer on each venue Mac)
- Reference: jakehilborn/displayplacer
- Still used when
display_controlbinary is not present - Also used for per-device
configStroverrides (displayplacer-format strings inevent_device.data_json)
Current state (2026-05-20):
- ✅ Correct
mirror_of_display:<uuid>syntax used in displayplacer fallback - ✅ Failures logged to Electron console (
[Launcher] set_display_layout:) - ✅ Display Mode toggle in Launcher config (Native OS section) — always visible
- ✅
display_controlbinary built (universal x86_64 + arm64), committed to repo - ✅ Idempotency —
mirrorandextendboth no-op with a clean message if already in the requested state (no display flicker) - ✅
list-modes— JSON array of all online displays + every usableCGDisplayMode(width, height, refresh, pixel size, HiDPI flag, is_current) - ✅
set-mode— sets resolution/refresh viaCGConfigureDisplayWithDisplayMode; supports--refresh,--hidpi,--no-hidpi; auto-prefers HiDPI on built-in, non-HiDPI on externals - ✅ IPC handlers
native:list-display-modes+native:set-display-modewired through full bridge stack (system_handlers → preload → types → electron_relay) - ✅ Remote build script (
scripts/remote-build-display-control.sh) — compiles on laptop-01 via SSH from Linux workstation; usesssh catpipe pattern (avoids scp space-in-username bug)
To rebuild display_control after source changes:
# From repo root on workstation (laptop-01 must be reachable):
./scripts/remote-build-display-control.sh
# Or directly on a Mac:
./scripts/build-display-control.sh
# Test with a second display connected:
./resources/bin/display_control status
./resources/bin/display_control extend
./resources/bin/display_control mirror
./resources/bin/display_control list-modes
./resources/bin/display_control set-mode 0 1920 1080
./resources/bin/display_control set-mode 1 1920 1080 --refresh 60 --no-hidpi
# Commit:
git add resources/bin/display_control
git commit -m "build: update display_control binary (universal)"
Optional per-device override (displayplacer format, for edge cases):
For rooms where auto-detection produces the wrong result, store the raw configStr in event_device.data_json:
{
"displayplacer_config_extend": "<output of displayplacer list in extended layout>",
"displayplacer_config_mirror": "<output of displayplacer list in mirrored layout>"
}
configStr is passed from the Svelte call site and uses the displayplacer fallback path directly.
Future Ideas
Capabilities worth adding as the Launcher matures. Roughly ordered by venue-day impact.
1. Display reconfiguration events (push IPC)
CGDisplayRegisterReconfigurationCallback fires when a display is connected or removed. Wrapping this in a webContents.send('native:display-changed', payload) push event would let the Svelte UI auto-mirror the moment a projector cable lands — eliminating the most common operator action during show setup. Currently the UI must poll status or the operator presses Mirror manually.
2. Audio output routing
When mirroring to a projector the audio output should follow. CoreAudio (AudioObjectSetPropertyData on kAudioHardwarePropertyDefaultOutputDevice) can switch the default output device programmatically. Candidate bridge method: set_audio_output({device_name?, prefer_hdmi?}). Could be called automatically as part of the mirror flow, or exposed as a standalone control.
3. Battery / power status in telemetry
get_device_info returns CPU and RAM but nothing about power. On venue MacBook Airs this matters operationally. IOKit (IOPSCopyPowerSourcesInfo / IOPSGetPowerSourceDescription) can surface: charge %, is-charging, time-remaining, health. Low-cost addition to the existing telemetry handler.
4. Presentation state feedback
control_presentation is fire-and-forget. AppleScript can query the current slide index and total slide count from both PowerPoint (current slide index of active presentation) and Keynote (slide number of current slide of front document). A get_presentation_state() bridge method returning { app, slide, total, presenting } would let the Launcher UI show "Slide 7 of 42" — useful for operators monitoring multiple rooms.
5. Push event channel (IPC renderer notifications)
All bridge calls are currently request-response. Adding a webContents.send channel for unsolicited Electron → renderer events would unlock: display plug/unplug (#1 above), file download progress, network state changes, "presentation ended" detection. A thin ipcMain.on('native:subscribe', ...) registration pattern on the Electron side and a corresponding ipcRenderer.on listener in the preload would cover all use cases without breaking the existing handler structure.
6. Kiosk / accidental-quit hardening
A speaker or operator can accidentally Cmd+Q the launcher mid-presentation. app.on('before-quit') with either a confirmation dialog or an API-controlled lock flag (event_device.data_json.kiosk_locked: true) would prevent this. Can be toggled remotely — lock before the show, unlock after.