Skip to content

Make update reach the desktop app, and make the desktop app render - #9

Merged
ralyodio merged 3 commits into
mainfrom
fix/update-follows-desktop
Aug 30, 2026
Merged

Make update reach the desktop app, and make the desktop app render#9
ralyodio merged 3 commits into
mainfrom
fix/update-follows-desktop

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Two bugs that combined into "I keep updating and the desktop UI is still garbage".

1. diskpush update could never install the desktop app

runUpdate re-ran the installer with --cli-only whenever the manifest said desktop: false:

spawnSync('sh', ['-s', '--', ...(manifest.desktop ? [] : ['--cli-only'])], )

install.sh's has_desktop_session() only checks DISPLAY / WAYLAND_DISPLAY / XDG_CURRENT_DESKTOP, so installing from a tty or over SSH silently produces a CLI-only install — which then stayed CLI-only forever, however many times it was updated, and said nothing about why.

  • A CLI-only install is now left to the installer's own detection, so a machine that has a desktop gets the desktop app on its next update.
  • A desktop install is pinned to --desktop, so updating from cron or SSH cannot quietly downgrade it and orphan its files.
  • --cli-only / --desktop on the update command override both, and a Surfaces: line reports the choice.

The same fix applies to the startup auto-update in self-update.ts, which had an identical copy of the expression.

2. The desktop app 404'd every one of its own assets

The window used loadFile, so the page origin was file://. Next emits root-absolute asset URLs (/_next/static/…), which under file:// resolve against the filesystem root (file:///_next/…). The stylesheet and all five chunks 404'd, leaving prerendered HTML with 0 CSS rules and no React — unstyled serif text, native form controls, permanent "Loading…".

Next can't emit relative paths (next/font rejects an assetPrefix without a leading slash) and the CSP sends base-uri 'none', so a <base> tag is out. The bundle now gets its own origin via a standard scheme, which also makes the existing default-src 'self' mean the bundle rather than the whole disk. Files are read with fs rather than net.fetch('file://') so it keeps working inside the packaged asar.

Before / after, measured against the real export in headless Chromium

before after
failed requests 6 0
CSS rules applied 0 124
React hydrated no yes
body background transparent rgb(7, 12, 22)
font Times New Roman Geist

This is why the bug survived a UI pass: the export was verified over a static http:// server, where those absolute paths resolve fine. Only file:// breaks them.

Tests

12 new tests (installer-flags.test.ts, bundle-path.test.ts) covering the flag matrix, asset path resolution, directory-traversal refusal, and content types. Full suite: 258 passing, typecheck and build clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CBc6KV8dL7oLS5okq15B4W

ralyodio and others added 3 commits August 30, 2026 05:46
`diskpush update` re-ran the installer with --cli-only whenever the manifest
said desktop:false, so an install made from a tty or over SSH — where
has_desktop_session() finds no DISPLAY — could never gain the desktop app,
however many times it was updated, and printed nothing to say so. The desktop
app looked stale because it had never been installed.

A CLI-only install is now left to the installer's own detection, so a machine
that has a desktop gets the desktop app on its next update. A desktop install
is pinned the other way, to --desktop: updating one from cron or SSH has no
DISPLAY to detect, and auto-detect would quietly strip the app out from under
a working desktop and orphan its files. --cli-only and --desktop on the update
command override both, and a Surfaces: line now says which it chose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBc6KV8dL7oLS5okq15B4W
… asset

The window loaded out/index.html with loadFile, so the page origin was file://.
Next emits root-absolute asset URLs (/_next/static/...), which under file://
resolve against the filesystem root — file:///_next/... — so the stylesheet and
all five chunks 404ed. What shipped was the prerendered HTML with zero CSS
rules applied and no React: unstyled Times New Roman, native form controls, and
a permanent "Loading…". It reads as a broken UI rather than a broken path,
which is why it survived a UI pass — the export was verified over http://,
where those same absolute paths resolve fine.

Next cannot emit relative ones: next/font rejects an assetPrefix without a
leading slash, and the CSP here sends base-uri 'none', so a <base> tag is out
too. Instead the bundle gets an origin of its own via a standard scheme, which
also makes the existing default-src 'self' mean the bundle rather than the
whole disk. Files are read with fs, not net.fetch('file://'), so it keeps
working inside the packaged asar.

Verified in headless Chromium against the real export: 6 failed requests and 0
CSS rules before, 0 failed requests and 124 after, with React hydrating and the
dark theme and Geist both applied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBc6KV8dL7oLS5okq15B4W
The launch check dies at the display, so it never loads a page — which is why
it watched v0.2.0 ship a window where the stylesheet and all five chunks 404ed
and reported ok. Two checks that need no display now run first: every
root-absolute asset in the export must resolve to a real file through the same
function that serves it, and the main process must still register the bundle
scheme and must not load the renderer with loadFile.

The second is the one with teeth. Assets resolve whatever the window does with
them; what broke the app was the page origin, so the mechanism is pinned rather
than only the paths. Verified by reintroducing loadFile into the built main and
watching the check fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBc6KV8dL7oLS5okq15B4W
@ralyodio
ralyodio merged commit d42d95e into main Aug 30, 2026
4 checks passed
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