feat(desktop): say when the Mac is offline

Merged
DA
4d ago
+337-9

Summary

Implements proposal A, the sidebar pill, from the task "Add UI to show when offline" (https://tryluke.dev/stage/plan/519afac9-0e94-428c-bb83-2afe6908a2d0) and its proposals page. The developer chose A.

Before this change, nothing in the window said the Mac was offline. Each surface failed on its own with "… could not be read".

  • Source of truth (main). packages/host/src/client/network-watch.ts asks Electron's net.isOnline() once at launch and every 2 s after. It writes a new network: { online } slice into the app-state document, and only when the answer changes, so a steady network costs no version and pushes nothing to the windows. It is polled because Electron's main process gets no event when net.online changes. compose-desktop.ts links it only in runs that send network traffic, so fixture and capture runs never show offline.
  • The pill (renderer). offline-mark.tsx reads the slice on app:state like any other state, keeping the renderer's single state channel, and draws nothing while the Mac is online.
    • Where it shows: an amber "Offline" pill in the sidebar's foot, beside the Restart pill. While the sidebar is folded it moves to the title bar beside New task, drawn as just its glyph in a round one button wide; the toolbars' --sidebar-toggle-clearance grows by that button while offline, so nothing starts underneath it. The sign-in gate shows the pill in its title bar too, since signing in needs the network. Settings and version history draw their own title bars and are a follow-up.
    • What it says: the tooltip reads "You're offline. Luke will catch up as soon as you reconnect."
    • Narrow foot: if the foot is narrow, or the Restart pill is also showing, it shrinks to its glyph so the account name keeps its room.
    • Accessibility: it is a role="status" element named "Offline".

false from net.isOnline() is reliable. true cannot tell apart a captive portal or a service that is down. Showing "Can't reach Luke" by carrying account-call.ts's NETWORK fault up to the UI is a follow-up, as are proposals D (toasts) and E (precise per-surface failures).

Evidence

  • pnpm check:affected: typecheck ✅ and test ✅. The lint/knip/discarded-effect group aborted only because discarded-effect ran out of heap in the sandbox. Re-run one by one: pnpm lint ✅, pnpm knip ✅, NODE_OPTIONS=--max-old-space-size=6500 pnpm discarded-effect ✅ ("No discarded Effect, Stream, or Layer in 2079 files").
  • New tests:
    • packages/host/src/client/network-watch.test.ts, 4 tests on TestClock: lost and regained within one interval, offline at launch shown at once, a steady network moves no version, the watch stops with its scope.
    • sidebar-foot.test.ts, +1: the foot says Offline only while the document says offline.
    • desktop-shell.test.ts, +2: the folded title bar says Offline only while offline, and the sign-in gate says it while offline.
    • I checked that each new renderer test fails with the pill removed.
  • Looked at by hand in the Electron app on Linux (fixture run, offline forced in a local build only). The pill and its hint draw in the foot.

Invariants

  • ./scripts/check.sh green. (CI will confirm; affected checks pass locally)
  • JSON Schema goldens unchanged.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.runPromise/runSync/runFork outside the runtime edges.
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch; the poll is a Schedule.spaced forked into the client's scope.
  • No Effect, Stream, or Layer written as a statement and dropped.
  • Test count: +7 (4 in network-watch.test.ts, 1 in sidebar-foot.test.ts, 2 in desktop-shell.test.ts).
  • No hand-rolled file replaced.
  • No CLAUDE.md/AGENTS.md sentence names a changed part.
  • PRIVACY.md unchanged: the probe reads a local flag and sends nothing.
  • Package deps unchanged.
  • Barrel/door rule: the renderer imports nothing new beyond lucide-react's WifiOffIcon.

Notes

  • Mac check: this has not been run on a Mac. Turning Wi-Fi off on a dev build should show the pill within about 2 s, and turning it back on should clear it.
  • Leaving: the pill disappears at once when the network returns, the same way the Restart pill does, rather than being held through an exit fade.

Made with Luke


Summary by cubic

Adds an offline indicator to the desktop so users can tell when their Mac has lost its network, instead of only seeing each surface fail with "… could not be read".

  • Main polls Electron's net.isOnline() at launch and every 2 seconds into a new network.online slice of the app-state document, written only when the answer changes so a steady network costs no version.
  • The renderer draws an amber "Offline" pill in the sidebar's foot next to the Restart pill, or beside New task in the title bar while the sidebar is folded; it reads the same app-state channel as everything else.
  • The sign-in gate, which needs a network, shows the pill in the title bar while it stands.
  • The tooltip reads "You're offline. Luke will catch up as soon as you reconnect."
  • The watch is linked only in runs that send network traffic, so fixture and capture runs never show offline.
  • false from net.isOnline() is reliable; true cannot tell apart a captive portal or a down service.