Skip to content

Add lively.app: NW.js desktop distribution#1794

Open
merryman wants to merge 32 commits intomainfrom
feature/nwjs-app
Open

Add lively.app: NW.js desktop distribution#1794
merryman wants to merge 32 commits intomainfrom
feature/nwjs-app

Conversation

@merryman
Copy link
Copy Markdown
Member

Summary

  • Add lively.app/ sub-package that bundles lively.next as a standalone NW.js desktop app — users download one archive, double-click, lively runs. No clone, no terminal, no PATH setup.
  • Multi-platform distribution (Linux x64, macOS arm64, Windows x64) built via a cross-platform Node build script (lively.app/scripts/build.mjs).
  • Nightly GitHub Action (.github/workflows/build-desktop-app.yml) produces downloadable artifacts on the Actions run page.
  • Drop google-closure-compiler deps (~571 MB) since SWC replaced them and the legacy path was gated behind an opt-in freezer UI flag.

How it works

  • NW.js as the shell: the desktop app is the NW.js binary + a bundled lively.next tree + a standalone Node.js binary for the server subprocess.
  • Server in-process where possible, child process where not: ESM loader hooks (module.register, registerHooks, NODE_OPTIONS --experimental-loader) all crash NW.js's Blink renderer, so start-server.cjs (node-main) spawns lively.server/bin/start-server.js as a managed subprocess with flatn's --experimental-loader. A parent-PID watchdog (watchdog.cjs) preloaded into the subprocess ensures it dies when NW.js dies — including force-kill / crash — no orphans.
  • Dev mode vs bundled mode: start-server.cjs auto-detects which mode it's in (based on whether __dirname is inside rootDir) and logs to lively.app/boot.log vs ~/.local/share/lively.next/boot.log accordingly, sets up FLATN env itself, uses the bundled node binary when present.
  • install.sh hook: after the freezer build, setup.sh fetches the NW.js SDK binary directly (the nw npm package's postinstall can't decompress through flatn's flat layout). Opt out with ./install.sh --no-desktop.

Bundle contents (1.9 GB uncompressed Linux)

  • NW.js runtime (nw / nwjs.app / nw.exe, lib/, *.pak, ...)
  • Standalone Node.js binary (~123 MB) for the server subprocess
  • app/ with the whole monorepo minus build artifacts, tests, docs, wrong-platform native bindings, puppeteer, and runtime caches
  • desktop/ with start-server.cjs, watchdog.cjs, server-config.js
  • Platform launcher: .desktop + launch.sh (Linux), renamed .app (macOS), renamed .exe + .bat (Windows)

Test plan

  • Clean-install the monorepo → produces working ./start-server.sh
  • node lively.app/scripts/build.mjs → produces dist/lively.next-linux-x64/
  • Launch bundle with bare env (no nvm, no FLATN_*): env -i HOME=$HOME PATH=/usr/bin:/bin DISPLAY=:1 bash dist/lively.next-linux-x64/launch.sh → server boots in-bundle on 127.0.0.1, full lively.next dashboard renders (verified with screenshot)
  • Force-kill NW.js with SIGKILL → server subprocess exits within 1s (watchdog confirmed, no orphans)
  • ./install.sh --no-desktop skips the NW.js SDK fetch
  • macOS arm64 bundle — first run via CI (build.mjs has branches, not locally verified)
  • Windows x64 bundle — first run via CI (build.mjs has branches, not locally verified)
  • Manually trigger build-desktop-app.yml on this branch before merge to smoke-test all three platforms

Open follow-ups (not in this PR)

  • The legacy compileOnServer / Terser+Closure code path in lively.freezer/src/util/helpers.js + the "Terser + Babel" option in ui.cp.js — dead code after the dep removal, clean up pass
  • lively.context CDP hybrid (the original motivation for NW.js) — this PR is just the shell; the Smalltalk-style debugger integration using node:inspector is tracked separately in plan_nwjs_app.md
  • Dev-deps pruning in lively.next-node_modules/ to further shrink the bundle
  • macOS code-signing + Windows code-signing for notarized / SmartScreen-friendly distributables

🤖 Generated with Claude Code

Robins Kiste and others added 30 commits April 17, 2026 12:14
New sub-package that bundles lively.next as a standalone desktop
application via NW.js, so end users can launch a running lively session
without cloning the repo, installing deps, or managing a server.

Architecture:
  - package.json doubles as NW.js manifest: `main` points at a loading
    screen (boot.html), `node-main` points at start-server.cjs which
    runs in Node context before any window opens
  - node-main spawns lively.server as a managed child process with
    --experimental-loader for flatn's ESM resolver (ESM loader hooks
    crash NW.js's Blink renderer, so in-process loading isn't viable)
  - watchdog.cjs preload polls the parent PID every second; the server
    dies whether NW.js exits cleanly, crashes, or is force-killed — no
    orphans
  - server-config.js excludes puppeteer-dependent plugins (test-runner,
    lively.headless) since NW.js provides its own Chromium
  - setup.sh downloads the NW.js SDK binary directly (flatn installs
    the `nw` npm package but its postinstall can't decompress through
    flatn's flat layout — yauzl-promise → @node-rs/crc32 native addon)
  - start.sh sources lively-next-env.sh, clears NODE_OPTIONS (ESM
    loaders via NODE_OPTIONS also crash Blink), and launches NW.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
So flatn picks it up as a dev package and its dependencies (nw SDK)
get installed into lively.next-node_modules during install.sh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Registering lively.app in packages-config.json makes flatn install the
`nw` package source, but its postinstall fails to decompress the binary
(yauzl-promise → @node-rs/crc32 native addon can't resolve through
flatn's flat layout). Call lively.app/setup.sh to pull the SDK tarball
directly after the freezer build.

Opt out with `./install.sh --no-desktop`. Skipped automatically if
lively.app/ is absent, and idempotent — setup.sh short-circuits when the
binary is already in place.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Google Closure was the minifier used by the legacy Terser branch of
lively.freezer/src/bundler.js, gated by useTerser which defaults to
false. Minification now flows through SWC in-process (bundler-swc.js)
and the Closure path is only reachable from the freezer UI's explicit
"Terser + Babel" selection.

The two google-closure-compiler-{linux,osx} packages weigh ~571MB — a
big win for any fresh install and even more so for distribution bundles.

If someone opts into useTerser=true after this change, compileOnServer
will fail fast with MODULE_NOT_FOUND instead of silently succeeding
with whatever Closure happens to do. Cleaning up the legacy code path
itself (helpers.js compileOnServer, ui.cp.js compiler selector) is
left for a follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Teach start-server.cjs to work in both modes:
  - Dev: lively.app/ inside the monorepo (rootDir = <root>)
  - Bundled: standalone distribution where this script lives at
    <bundle>/desktop/ and the monorepo content is at <bundle>/app/

Detection compares __dirname vs rootDir: in dev __dirname is under
rootDir (lively.app/desktop/); in bundled mode it isn't
(<bundle>/desktop/ vs <bundle>/app/).

Bundled mode additionally:
  - Logs to ~/.local/share/lively.next/boot.log (user-writable,
    not alongside read-only source)
  - Sets FLATN_* env vars itself since there's no launcher script
  - Uses <bundle>/node/bin/node (bundled) instead of PATH
  - Creates the runtime dirs (esm_cache, snapshots, local_projects,
    custom-npm-modules) that the server's library-snapshot step tars

Gitignore dist/ at both the repo root and inside lively.app/ so the
build artifact tree doesn't show up in git status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Produces dist/lively.next-<platform>-<arch>/ — a self-contained
distribution that runs by double-clicking its launcher on any machine,
no monorepo, no nvm, no PATH setup. Verified end-to-end: launched with
bare env (env -i HOME=... PATH=/usr/bin:/bin DISPLAY=...), the server
boots in-bundle and serves lively.next on a random localhost port.

Pieces:
  - NW.js Normal flavor binary at bundle root (launcher)
  - desktop/ with start-server.cjs + watchdog.cjs + server-config.js
  - app/ with the full lively monorepo content
  - node/bin/node — standalone Node.js (NW.js's embedded node can't
    be invoked as a plain node subprocess)
  - launch.sh (cross-platform shim) + lively-next.desktop (Linux)

Size trimming versus a naive rsync:
  - Anchored root-level excludes (/.git/ not .git/) so legit nested
    dirs like systemjs/0.21.6/dist/ survive
  - Cross-platform native bindings filtered per target
    (@swc/core-darwin-* stripped from a Linux bundle, etc.)
  - Chromium locales pruned to en-US only by default; override with
    LOCALES="en-US fr de" or LOCALES=all
  - Tests, examples, docs, source maps stripped from deps
  - lively.headless/chrome-data-dir stripped (kept sources — tar step
    needs the package directory to exist)
  - swc-plugin Rust build artifacts excluded (the .wasm is prebuilt)
  - Puppeteer excluded — desktop config already omits the plugins
    that need it

Result for Linux x64: ~1.7–1.9 GB uncompressed. Pack to tar.gz with
PACK=1 for distribution.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Runs lively.app/scripts/build.sh on ubuntu-latest at 04:00 UTC daily
and uploads dist/lively.next-linux-x64.tar.gz as a GitHub artifact
downloadable from the Actions run page. Retention 30 days.

Mirrors the install recipe from check-pr.yml / daily-ci-checks.yml:
  - Node 24, bun, Rust (wasm32-wasip1) toolchains
  - lively.next-node_modules/ cache keyed on package.json hashes
  - ./install.sh with retry on transient failures
  - --no-desktop skip on install since build.sh fetches its own
    NW.js (Normal flavor) directly

Also triggerable on-demand via workflow_dispatch for pre-release
smoke tests.

Linux-only for now; matrix expansion to macOS (osx-arm64) and
Windows (win-x64) is the next step once build.sh is verified on
those platforms.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the Linux-only bash build.sh with a Node.js-based build.mjs
that runs identically on Linux, macOS, and Windows. The nightly
workflow now builds a bundle per OS into separate GitHub artifacts:
lively.next-linux-x64.tar.gz, lively.next-osx-arm64.tar.gz,
lively.next-win-x64.zip.

Why Node instead of bash:
  - rsync isn't on Windows; reimplementing with fs.cp or a filter
    function works on all three
  - Git Bash on Windows CI is fragile for anything beyond trivial
  - Modern tar ships with Linux/macOS and Windows 10+ (bsdtar) so
    extraction via child_process.execFileSync('tar', ...) works
    across the board

Per-platform finalizers:
  - Linux: writes launch.sh + lively-next.desktop (double-click target
    in most file managers)
  - macOS: renames nwjs.app → lively.next.app so the whole bundle is a
    native .app; patches Info.plist CFBundleIdentifier/Name/DisplayName
    so the OS sees it as our app (not a generic NW.js)
  - Windows: renames nw.exe → lively.next.exe + writes launch.bat

Cross-platform extras:
  - Auto-detects target platform/arch with --platform=X --arch=Y
    override (lets you cross-target in local testing even if full
    cross-compile only works from matching platforms)
  - Cross-platform native binding stripping expanded — @swc/core-*
    and @rollup/rollup-* are kept only for the target's
    platform-arch tag, rest filtered
  - Downloads NW.js + standalone Node.js into dist/.cache/ with a
    .extracted flag file so rerunning skips the fetch/extract
  - Pure-Node recursive-copy with a tiny rsync-style pattern matcher
    (anchored / vs anywhere, ** glob, *, ?)

CI matrix (.github/workflows/build-desktop-app.yml):
  - ubuntu-latest   → lively.next-linux-x64.tar.gz
  - macos-latest    → lively.next-osx-arm64.tar.gz
  - windows-latest  → lively.next-win-x64.zip
  fail-fast: false so one broken OS doesn't drop the others.
  shell: bash default so install.sh keeps running via Git Bash on
  Windows runners.

Verified on Linux end-to-end (server boots, dashboard serves at
127.0.0.1). macOS / Windows first-time builds will surface from the
first nightly run; finalizers are straightforward renames + plist
tweaks and match the layouts nw-builder and other NW.js packagers use.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
workflow_dispatch only works once the workflow file is on main, and
this repo's default branch doesn't have it yet. Add a pull_request
trigger scoped to paths that actually affect the bundle (lively.app/**,
install.sh, flatn/**, lively.installer/packages-config.json, and the
workflow file itself) so reviewers can download artifacts from the PR
Checks tab before merge.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Native Windows isn't a supported lively.next platform — maintainers
and users run lively via WSL on Windows, which means the Linux bundle
is the Windows distribution too.

The windows-latest runner's install.sh failure (Git Bash passes MSYS
paths like /d/a/... to Node's --experimental-loader, which Windows
Node interprets as D:\d\a\... i.e. a drive-letter-prefixed literal)
isn't worth chasing for a platform the project doesn't support.

build.mjs keeps its Windows branches for anyone who wants to
cross-target a Windows bundle from a Linux or macOS host.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
End users on Windows need a double-clickable .exe, not "use WSL first."
But a windows-latest CI runner can't run install.sh cleanly: Git Bash
hands MSYS paths (/d/a/...) to Node's --experimental-loader, which
Windows Node interprets as drive-letter-prefixed literals (D:\d\a\...).
That's a path-mangling fight we don't need to have.

Instead, cross-compile: build.mjs is a pure Node script that downloads
the target-platform NW.js and Node.js binaries and stages them. None
of the copying or renaming cares what host OS it's running on — only
the platform finalizers differ, and those are just fs.rename + string
replacement.

Matrix change: both linux-x64 and win-x64 bundles now build on
ubuntu-latest via --platform=X --arch=Y; macOS stays on macos-latest
(native install.sh works fine there). Added build.mjs fallback for
zip packing on Linux hosts (`zip -r` since GNU tar doesn't do `-a`).

Verified locally: cross-compiled win-x64 bundle, 1.6 GB unpacked,
692 MB .zip, lively.next.exe is a genuine PE32+ x86-64 binary,
node.exe and launch.bat in place.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
macOS runners ship Python 3.14 now, which removed the distutils stdlib
module. node-gyp@9 (dragged in by leveldown's native addon build via
flatn's bun-install step) still does `from distutils.version import
StrictVersion`, so every dep install blows up with
ModuleNotFoundError: No module named 'distutils'.

Pin Python 3.11 via actions/setup-python — it still has distutils,
so bun install / node-gyp succeed. Applies to both runners so Ubuntu
stays green when it eventually bumps past 3.11 as its default.

Upstream fix is to bump node-gyp to 10.x in the relevant package
trees (leveldown, whatever else); out of scope for this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Before, the macOS bundle was a folder containing lively.next.app
alongside boot.html, desktop/, app/, node/, and NW.js tarball leftovers
(credits.html etc.). Users who downloaded the archive saw junk next to
"the app".

Move everything into lively.next.app/Contents/Resources/app.nw/ (the
NW.js-convention location for an app payload) and drop the stray
files at the bundle root. Result: the bundle IS just the .app. One
double-click target, nothing else to see.

Layout now:
  lively.next.app/
    Contents/
      MacOS/nwjs
      Resources/
        Info.plist                   ← patched CFBundle* metadata
        app.nw/                      ← our payload
          package.json               ← NW.js manifest
          boot.html
          desktop/{start-server,watchdog,server-config}
          app/                       ← monorepo content
          node/bin/node              ← standalone Node for the server subprocess

Relative paths in start-server.cjs still work: __dirname resolves to
app.nw/desktop/, so __dirname/../app → app.nw/app (still finds
lively.installer/packages-config.json and wins the findRootDir loop).

Note on "damaged" Gatekeeper error: the .app is unsigned and therefore
quarantined on download. Users strip it with `xattr -cr lively.next.app`
or via Privacy & Security → "Open anyway". Proper code-signing +
notarization is follow-up work (requires Apple Developer account).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Until code-signing + notarization lands (tracked in #1795), end users
downloading the unsigned .app hit "lively.next is damaged and can't be
opened" — standard Gatekeeper behavior for quarantined downloads, but
the error message makes it look like the archive is broken.

Drop a tiny README next to the .app inside the bundle with:
  - why it happens (unsigned + quarantined, not damaged)
  - the xattr -cr one-liner fix
  - the GUI alternative (Privacy & Security → Open anyway)
  - a note that this goes away once signing is set up

So the download is lively.next.app + README-macOS.txt — still one
obvious double-click target, plus a text file users read exactly once.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
install.sh skips the landing-page freezer build under CI=true (the
other CI workflows — check-pr, daily-ci-checks — don't need it and
the build is ~2 min). But the desktop bundle's world-loading server
plugin opens landing-page/index.html on the first GET / and crashes
ENOENT if it's missing:

  Error: ENOENT: no such file or directory, open
    '.../app.nw/app/lively.freezer/landing-page/index.html'
  Node.js v25.6.1
  ERROR: Server crashed (exit code 1)

Add an explicit `npm --prefix lively.freezer run build-landing-page`
step in the desktop-build workflow so the artifact is present before
build.mjs rsyncs the monorepo into the bundle.

Local builds weren't hitting this because install.sh's `if [ -z $CI ]`
branch runs `build-unified` (both landing-page and loading-screen) on
dev machines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ry snapshot

Two independent wins for cold-start time on the bundled desktop app:

1. NODE_COMPILE_CACHE for the server subprocess
   Node 22+ caches V8 bytecode between runs. start-server.cjs now
   points the subprocess at ~/.cache/lively.next/v8 (Linux/Win) or
   ~/Library/Caches/lively.next/v8 (macOS). First launch is unchanged;
   subsequent launches skip most of the JS parse + compile work
   (typically 20-40% faster on subsequent launches).

2. Ship a pre-built library snapshot in the bundle
   LivelyDAVPlugin.compressLibraryCode normally tars + gzips ~28
   lively package dirs on every server startup (3-5s of IO +
   compression per launch). This is the blob served at
   /compressed-sources to browser clients for fast module lookup.

   New lively.server/scripts/build-library-snapshot.cjs produces the
   exact same tar.gz standalone (self-sufficient: sets FLATN env
   itself). The CI workflow runs it right before build.mjs, and the
   resulting lively.server/.library-snapshot.tar.gz gets rsynced into
   the bundle's app/lively.server/ dir.

   dav.js checks process.env.LIVELY_PREBUILT_LIBRARY_SNAPSHOT at
   startup: if set and the file exists, read it into memStore and
   skip the tar+gzip step. start-server.cjs sets the env var pointing
   at the bundled file whenever bundled=true. Dev mode path is
   unchanged (env var unset → original regeneration).

   Log confirms it works — 4ms to load from disk vs. the 3-5s
   regeneration it replaces:

     [lively.server] loading pre-built library snapshot: .../app.nw/
       app/lively.server/.library-snapshot.tar.gz
     [lively.server] pre-built library snapshot loaded

Staleness note: the snapshot embeds source paths / contents at build
time. If a user somehow modifies package sources inside the .app
after extraction, the snapshot will serve out-of-date code to the
browser (the file-hash map, which IS recomputed fresh each startup,
would detect this but the snapshot wouldn't update). Acceptable
trade-off for a distribution bundle — nobody's going to hand-edit
files inside Contents/Resources/app.nw/.

Out of scope: the bigger startup cost (~15s in steps 1+2 — systemjs +
lively.modules + config + plugin imports) needs pre-bundling
lively.server to a single CJS file. Separate follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Normal flavor has no Chromium DevTools, which makes diagnosing
client-side issues in the bundle (blank screen, failed asset loads,
JS errors) a back-and-forth guessing game. SDK flavor ships full
DevTools — users hit Cmd+Opt+I / right-click → Inspect to see the
console + network.

Costs ~20% more bundle size (roughly +100-150MB uncompressed).
Switch back to FLAVOR=normal once the desktop app is stable and
end-users don't need inline debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On macOS the NW.js window stayed blank after the server finished
booting. Tested the same URL directly in Safari / Chrome — works fine.
So the server is healthy; the problem is specifically the NW.js
window's navigation.

Setting win.window.location.href from node-main context crosses the
node↔DOM boundary and behaves inconsistently across NW.js versions
(reliable on Linux in our testing, blank on macOS). The canonical
pattern is to emit an event to the page and let its own script
assign location.href from the DOM context.

- boot.html subscribes to a new "lively-boot-navigate" event and
  does window.location.href = url itself.
- start-server.cjs emits that event with the server URL instead of
  poking location directly.
- While we're here, target /dashboard/ explicitly so the window
  doesn't have to follow a 301/302 redirect from / — one fewer
  moving part in the cross-context hop.

Can't reproduce the bug locally (Linux navigates fine with the old
code too), so this is verified by shape rather than repro. The DOM-
side pattern is what every NW.js tutorial recommends for this flow;
worst case it's a correctness cleanup with no regression.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous "nw.Window.get().emit()" attempt failed with TypeError on
macOS because nw.Window.get() in node-main returns a handle that does
NOT expose EventEmitter methods (emit is undefined). All my earlier
emitStatus / emitError calls had been silently swallowed in try/catch
for the same reason — that's why boot.html only ever showed the
hardcoded "Starting server..." and never live status updates.

Right pattern: expose helper functions on the DOM window in boot.html
(window.livelyBoot.status / .error / .navigate) and call them from
node-main via win.window.livelyBoot.foo(...). That's a regular method
call on a regular object; works across contexts the way a bare
location.href setter doesn't reliably.

Navigation still targets /dashboard/ directly (avoid the / redirect).

User reported blank screen on macOS with win.window.location.href
assignment; that's what this approach replaces. Falls back to the
direct assignment if boot.html's script somehow hasn't run yet —
shouldn't happen in practice, but logged so we'd know.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
them breaks lively's client bundle

The dashboard client, when given access to Node's require() via
node-remote, takes lively's "I'm in a Node environment" code path
and calls require('socket.io-client'). Node's standard CJS loader
can't find it because flatn's flat node_modules layout doesn't match
what require() expects, and the page fails to bootstrap — blank screen
inside NW.js, despite rendering cleanly in a regular Chrome/Safari tab
from the same server.

Removing node-remote means pages loaded from the localhost server run
as pure browser pages (no require, no process, no Buffer). lively then
takes its browser-only code path (fetches socket.io-client over HTTP
via SystemJS) — same as any user hitting the server with their normal
browser.

boot.html still works because it's loaded as `main` (local file://),
not via node-remote, and NW.js always exposes `nw.Window` / `nw.App`
to pages it loads directly regardless of node-remote. So the
livelyBoot helper-call mechanism for status + navigation is unchanged.

Re-enable node-remote later when the lively.context / node:inspector
integration needs it, but with a scoped URL pattern (e.g.
http://127.0.0.1:*/lively-context/*) that only targets debugger
endpoints — not the whole dashboard.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
F12 / Cmd+Opt+I / right-click → Inspect aren't reliably bound in
NW.js on macOS even in SDK flavor (can vary by build and by OS
keyboard configuration). Enabling the Chrome DevTools Protocol
endpoint at localhost:9222 gives a dependable fallback: open
chrome://inspect in any regular Chrome while the app is running,
find the NW.js window, click Inspect → full DevTools.

Works in both Normal and SDK NW.js flavors, so it's a no-cost
fallback independent of which flavor CI ships. Single-user desktop
app so port 9222 collision isn't a concern.

Leaving FLAVOR=sdk for now so in-app DevTools ALSO works when it
feels like working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two UX fixes:

1. boot.html re-styled to match the dashboard aesthetic
   - Golden→orange linear-gradient background (same palette as the
     landing page: #F1C40F → #F39C12)
   - Two translucent triangle accents echoing the dashboard's
     triangular backdrop
   - IBM Plex Sans with system-font fallback
   - Inlined lively-next logo (from lively.morphic/lively-next-logo.svg),
     recolored white so it reads against the gradient
   - Replaced the spinning circle with a slim progress bar that slides
     left-to-right — looks calmer and more purposeful for a multi-second
     boot than a spinner
   - Error state renders as a red inline badge instead of overwriting
     the status text in red (more legible)

2. nw.Menu with Dashboard / Reload / DevTools shortcuts
   Once the window's open and navigated, the user has no way back from
   a project view to the dashboard. Install a menubar (system menu bar
   on macOS, window menu elsewhere) with:
     - Dashboard (Cmd+D / Ctrl+D) — navigates location.href to
       /dashboard/
     - Reload (Cmd+R / Ctrl+R)
     - Toggle DevTools (Cmd+Opt+I / Ctrl+Alt+I) — complements the
       --remote-debugging-port chrome flag, which still works as a
       fallback when DevTools isn't bound via the in-window shortcut
   createMacBuiltin adds the standard app/Edit/Window menus on macOS
   so Quit/Hide/etc. work as users expect.
   menu setup is wrapped in try/catch + non-fatal log so a bad NW.js
   version or flavor doesn't block the rest of the boot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three polish fixes rolled into one:

1. Triangles now match the dashboard exactly
   Read the three Polygon morph coordinates out of
   lively.freezer/src/landing-page.cp.js (vertices + positions)
   and replicate them as SVG <polygon> elements in boot.html with
   preserveAspectRatio=slice. Single fill color (rgba(230,126,34,0.6))
   matching the landing-page's 60%-opacity orange. Drops the two
   hand-drawn CSS-border triangles that were roughly in the right
   direction but didn't line up with the dashboard aesthetic.

2. Menu items now actually navigate
   The previous approach set nw.Window.get().window.location.href
   from the menu's click handler — same node→DOM flaky-assignment
   pattern that caused the initial blank-screen on macOS. Fix: every
   page loaded in the NW.js window now gets desktop/inject.js
   injected via the manifest's inject_js_end, which exposes
   window.livelyNav(url). Menu click handlers call THAT — plain
   function call across the boundary, works consistently.

   Bonus: inject.js also binds Cmd+Shift+D (Ctrl+Shift+D on Linux)
   as a keyboard shortcut for "Go to Dashboard" that works from any
   page, including the ones lively serves. Shortcut was changed
   from plain Cmd+D because Chromium reserves Cmd+D for bookmarks.

   Also renamed the custom submenu from "lively.next" (duplicated
   the macOS app-menu label) to "Go", which is clearer.

   Menu shortcuts also migrated: Dashboard is Cmd+Shift+D (was
   Cmd+D), Toggle DevTools is Cmd+Opt+I. Reload entry dropped
   since Chromium binds Cmd+R natively anyway.

3. Proper app icon
   New lively.app/assets/icon.svg source: white rounded-square with
   the orange "engine" glyph from the lively-next logo centered,
   22% corner radius to match macOS squircle aesthetic.

   lively.app/scripts/generate-icons.sh rasterizes it to PNGs at
   7 sizes and packs them into .icns (macOS via iconutil or Linux
   via png2icns), .ico (ImageMagick), and a 512px PNG (Linux).

   CI installs the required tools (librsvg2-bin, libicns-utils,
   imagemagick on Ubuntu; librsvg + imagemagick from brew on macOS)
   then runs the generator before bundling.

   finalizeMacOS places icon.icns at Contents/Resources/app.icns,
   strips the stock nw.icns, and patches Info.plist
   CFBundleIconFile to "app". finalizeLinux copies icon.png to the
   bundle root for the .desktop file's Icon= reference.

   Windows .exe icon embedding (needs rcedit) left for follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
libicns-utils / icnsutils was removed from Ubuntu 24.04 (noble); the
apt install in the workflow fails with "Unable to locate package
libicns-utils". Switch the Linux runner's .icns generation from
png2icns to a tiny pure-Node script (scripts/build-icns.mjs) that
constructs the .icns file format manually: 4-byte magic + total size,
then a sequence of <type><size><png-bytes> chunks. No native deps, no
system packages to install.

macOS runner still prefers native iconutil — it handles Retina @2x
filenames correctly and is 10 lines less script. Both paths produce
bit-identical-enough .icns files (file(1) happily identifies the
Node-built one as "Mac OS X icon" with correct magic and type codes).

Leaves rsvg-convert (librsvg2-bin / brew librsvg) and ImageMagick as
the only apt/brew dependencies, both of which are still packaged and
maintained in 24.04.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
build.mjs's desktop-file copy list was ['start-server.cjs',
'watchdog.cjs', 'server-config.js'] — missing inject.js. So every
bundle we've produced has had the manifest reference
"inject_js_end": "desktop/inject.js" but no such file, which NW.js
silently no-ops on.

Net result: window.livelyNav was never defined in pages, menu click
handlers fell through to the window.location.href = url fallback,
and that's the flaky node→DOM assignment path that doesn't navigate
in NW.js on macOS — which is why menu items appeared to do nothing.

Add inject.js to the list. Verified locally: the file lands at
<bundle>/desktop/inject.js and NW.js will pick it up via the
inject_js_end manifest field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
menu items still appear inert

Every theory about why the menu doesn't work is currently untestable
from the boot log because we don't know which layer is failing:
  - Is the click handler firing at all?
  - Is inject.js actually being injected by NW.js?
  - Is window.livelyNav defined on the page at click time?
  - Does navTo throw?

Add log() calls at every step:
  - inject.js sets window.__LIVELY_INJECT_LOADED__ to a timestamp
    as a marker that node can read, and console.log()s when it runs
  - Menu click handlers log on entry
  - navTo logs window presence, inject marker, livelyNav type,
    current URL, and whether the livelyNav call returned
  - toggleDevTools logs same

After user clicks a menu item, the boot log should show one of:
  A. "Dashboard item clicked" missing entirely → click callback
     never fires (NW.js bug? menu structure issue?)
  B. "inject.js loaded: no" → inject_js_end isn't actually injecting
  C. "typeof livelyNav: undefined" → inject ran but helper wasn't set
  D. livelyNav called but page didn't navigate → something deeper

Removes the guesswork.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recent debugging has been hampered by not knowing which CI bundle the
user is actually running — an observed boot.log could match pre- or
post-fix code, we have to guess.

Write a build-info.json into the bundle during CI (LIVELY_APP_BUILD_SHA
comes from github.sha in the workflow) and have node-main read it on
startup, logging `build: {"sha":"...","builtAt":"..."}`. Next bug report
has the commit identity right at the top of boot.log.

Locally-built bundles get sha="(local)" so the field is always present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Research (nwjs/nw.js wiki + issues #4313 #5150 #8240 and the Transfer-
objects-between-window-and-node guide) points at the right pattern for
in-app navigation from a native NW.js menu:

  1. Create the menubar only AFTER `win.on('loaded', ...)` fires. Wiring
     nw.Menu up earlier from node-main is the flaky case — clicks often
     silently don't fire. The recent versions' fix lands once the
     window's own context is attached.
  2. Click handlers DO NOT set location.href themselves. That assignment
     across the node↔DOM boundary has been broken since NW.js 0.12.x
     (see #4313). Post a structured message to the DOM instead —
     win.window.postMessage({type:'lively-nav', url}, '*').
  3. DOM-side (desktop/inject.js, injected via inject_js_end) listens
     for 'message' events and does the location.href assignment in its
     own context. Same listener also binds Cmd/Ctrl+Shift+D as a
     keyboard fallback independent of the menu.

Revert the inject.js floating-button approach — that was a workaround
for the wrong symptom. Do it the documented way now.

Remaining click handlers just log on failure rather than explode; the
menu creation and message passing should both work on 0.110.1 given
the 'loaded' timing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant