You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 102ad5e
Browse filesBrowse the repository at this point in the historyBrowse files
authored
feat(desktop): show live download progress for app updates (#336)
* feat(desktop): show live download progress for app updates
The downloading state carries a whole-number percent now: the updater
controller gains reportDownloadProgress (floored, downloading-only, and
deduped per whole percent so chunk-level events do not fan out to every
window), fed from electron-updater's download-progress event. The update
dialog renders it as a progress circle and also opens while a download
is running, so an automatic download in flight is visible instead of
silent until ready.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: changelog entry for update download progress
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Quickbeard <quickbeard@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: DIVERGENCES.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -251,7 +251,7 @@ from the squash message first, or use a plain merge.
251
251
|`packages/app/src/context/server.tsx`|`createServerProjects.close()` wraps its two store writes — `remove(directory)` on `projects`, then the `recentlyClosed` record — in `batch()` (already imported). The recently-closed list is computed before the batch so the pair is one update. | The two writes are separate store keys and Solid flushes effects after each one, so between them a project is in neither list. Nothing upstream observes that gap, but the fork's `src/sandbox/projects.ts` seeding effect reconciles against **both** lists, saw a mounted folder that was neither open nor closed, and opened it straight back up — closing a Work folder was undone the instant it happened. | Keep the batch. On conflict take upstream's `close()` and re-wrap the two writes. Any future write pair that spans `projects` and `recentlyClosed` needs the same treatment; regression cover is `packages/app/test-browser/sandbox-projects.test.ts` ("closing a mounted folder sticks when the guest knows the project"). |
252
252
253
253
| WSL auto-install from the sandbox dialogs: `packages/app/src/i18n/{en,es}.ts`, `packages/desktop/src/preload/{index,types}.ts` | i18n gains the `sandbox.wsl.*` family (install action/retry, the permission-and-size hint, download progress with and without a content-length, the elevated step, restart-required, the restart-Windows action with its confirm step, and the failure line). preload exposes five channels on the existing `api.sandboxes` object — `sandbox-wsl-install{,-cancel,-state}`, a `sandbox-wsl-install-subscribe`/`-event` pair, and `sandbox-wsl-restart` (runs `shutdown.exe /r /t 0` for the restart a restart-required install asks for; the renderer confirms first) — and re-exports `WslInstallState`. All the logic is fork-only: `src/main/sandbox/wsl-install.ts` (the state machine), `src/main/wsl/install-bundle.ts` (manifest-driven per-arch MSI acquisition), `src/app/sandbox/wsl-install.tsx` (the shared UI both create-sandbox dialogs render in place of their form). | A Windows machine without WSL could not create a sandbox and was told to go install it by hand — while the app already shipped an installer for the Code side's WSL servers. The sandbox needs only the runtime (it imports its own rootfs and mounts its own server), so this reuses `installWslRuntimeDualPath` and skips the distro and CoDev-in-distro steps entirely. Its own channel set rather than a second `sandbox-event` type because this is platform state that exists before any sandbox does, and it lives in main rather than in the dialog because a ~250MB download on a whitelisted-proxy link must survive the dialog closing. | Keep the key family and the five channels; take upstream's surrounding changes. The channel names are preload/renderer contract — renaming one side alone breaks the dialogs silently. If upstream reworks `api.sandboxes`, keep the install methods on it rather than promoting them to a new top-level namespace: the renderer reaches them through `platform.sandboxes`. |
254
-
| Update checking and download controls: `packages/app/src/updater.ts`, `packages/app/src/components/{updater-action.ts,settings-general.tsx}`, `packages/app/src/pages/layout.tsx`, `packages/app/src/i18n/{en,es}.ts`, `packages/desktop/src/main/{updater.ts,updater-controller.ts,index.ts,ipc.ts}`, `packages/desktop/src/preload/{index,types}.ts` | `UpdaterState` gains `available` (found, not fetched) and the shared `UpdaterPrefs` (`autoCheck`, `autoDownload`, both defaulting true — upstream's behaviour). The controller takes `initialPrefs`/`savePrefs`, stops a check at `available` when `autoDownload` is off, gains `download()` for the consent step, gains `getPrefs`/`setPrefs`/`subscribePrefs`, skips the launch check when `autoCheck` is off, and short-circuits a repeat check while `available`. `main/updater.ts` reads and writes the prefs in the existing `opencode.updater` store under a `prefs` key; `main/index.ts`'s ten-minute timer consults `getPrefs().autoCheck` at fire time; `ipc.ts` adds `updater-download` and `updater-set-prefs` and registers a second listener on the existing `updater-subscribe` so prefs broadcast to every window; preload exposes `download`, `setPrefs`, `subscribePrefs`. The renderer adds two `SettingsRow` switches guarded by `platform.updater` (the Updates section is not desktop-gated, so the web build must not render them), the toast offers Download when the update is not on disk, and the titlebar pill deliberately stays `ready`-only — it installs, and an undownloaded update cannot be installed. i18n drops the dead `settings.updates.row.startup.*` keys (translated, never rendered) for `settings.updates.row.{autoCheck,autoDownload}.*`, `settings.updates.action.download` and `toast.update.description.available`. | Upstream checks and downloads unconditionally. The fleet is on metered, whitelist-only links where a several-hundred-megabyte installer arriving unannounced is a real cost, and the app already shipped the copy for a setting it never built. Prefs live in the main process because that is where the timer runs; putting them in the renderer's settings store would have meant main parsing the renderer's persisted format. | Keep the `available` state and the prefs plumbing together — the state is unreachable without `autoDownload`, and the setting is meaningless without the state. Take upstream's other changes to these files. If upstream reworks `createUpdaterController`, re-apply the two gates (`autoCheck` in `start()`, `autoDownload` in `check()`) rather than the whole diff; both are covered by `updater-controller.test.ts`. Defaults must stay true on both — a machine that never opens Settings has to behave exactly as it did before. |
254
+
| Update checking and download controls: `packages/app/src/updater.ts`, `packages/app/src/components/{updater-action.ts,settings-general.tsx}`, `packages/app/src/pages/layout.tsx`, `packages/app/src/i18n/{en,es}.ts`, `packages/desktop/src/main/{updater.ts,updater-controller.ts,index.ts,ipc.ts}`, `packages/desktop/src/preload/{index,types}.ts` | `UpdaterState` gains `available` (found, not fetched) and the shared `UpdaterPrefs` (`autoCheck`, `autoDownload`, both defaulting true — upstream's behaviour). The controller takes `initialPrefs`/`savePrefs`, stops a check at `available` when `autoDownload` is off, gains `download()` for the consent step, gains `getPrefs`/`setPrefs`/`subscribePrefs`, skips the launch check when `autoCheck` is off, and short-circuits a repeat check while `available`. `main/updater.ts` reads and writes the prefs in the existing `opencode.updater` store under a `prefs` key; `main/index.ts`'s ten-minute timer consults `getPrefs().autoCheck` at fire time; `ipc.ts` adds `updater-download` and `updater-set-prefs` and registers a second listener on the existing `updater-subscribe` so prefs broadcast to every window; preload exposes `download`, `setPrefs`, `subscribePrefs`. The renderer adds two `SettingsRow` switches guarded by `platform.updater` (the Updates section is not desktop-gated, so the web build must not render them), the toast offers Download when the update is not on disk, and the titlebar pill deliberately stays `ready`-only — it installs, and an undownloaded update cannot be installed. i18n drops the dead `settings.updates.row.startup.*` keys (translated, never rendered) for `settings.updates.row.{autoCheck,autoDownload}.*`, `settings.updates.action.download` and `toast.update.description.available`. The `downloading` state additionally carries a whole-number `percent`: the controller gains `reportDownloadProgress(percent)` (floored, no-op unless currently `downloading` and the whole percent moved — the backend reports every chunk and each transition is a log line plus an IPC send to every window), `main/updater.ts` feeds it from `autoUpdater.on("download-progress")`, and the fork-only update dialog (`dialog-update.tsx`) renders it as a progress circle and opens on `downloading` too, so an automatic download in flight is visible rather than silent until ready. i18n adds `toast.update.description.downloading`. | Upstream checks and downloads unconditionally. The fleet is on metered, whitelist-only links where a several-hundred-megabyte installer arriving unannounced is a real cost, and the app already shipped the copy for a setting it never built. Prefs live in the main process because that is where the timer runs; putting them in the renderer's settings store would have meant main parsing the renderer's persisted format. | Keep the `available` state and the prefs plumbing together — the state is unreachable without `autoDownload`, and the setting is meaningless without the state. Take upstream's other changes to these files. If upstream reworks `createUpdaterController`, re-apply the two gates (`autoCheck` in `start()`, `autoDownload` in `check()`) rather than the whole diff; both are covered by `updater-controller.test.ts`. Defaults must stay true on both — a machine that never opens Settings has to behave exactly as it did before. |
255
255
| `packages/app/src/context/global.tsx` | Two lines: imports `seedSandboxProjects` from the fork-only `packages/app/src/sandbox/projects.ts` and calls it with `server.list` + `ensureServerCtx`, beside the memo that already creates a ctx per connection. All the logic lives in the fork-only module. | A sandbox exposes exactly one directory (`/workspace` on vz/bwrap, the wslpath translation on WSL) and the guest registers it as its own project, but the app's per-server project list starts empty and nothing ever fills it. With no project, every "New session" entry point — `home-model.openNewSession`, the titlebar's `openNewTab` fallback chain — finds nothing for that server and falls back to another one, so the sandbox looks selected while the session runs on the local server. The seed reads `sync.data.path.directory`, which every server ctx already fetches, so it stays backend-agnostic. The same module also names that project after the sandbox's host folder (via `client.project.update`, the call behind rename-project), because the guest sees only its mount point and every sandbox would otherwise read "workspace" in the sidebar, tabs and palette. | Keep both lines; take upstream's other changes to the file. The call must stay inside the Global context init (it needs `ensureServerCtx`) — `SandboxesProvider` is mounted above `GlobalProvider` in `app.tsx`, so it cannot live there. |
256
256
257
257
| `packages/app/src/pages/layout/project-avatar-state.ts` | Both `ensureServerState` lookups are gated on `connection()`: `unread` before `notification.ensureServerState`, and the `permissionState` memo before `permission.ensureServerState` (which also moves below `connection`, and its consumer in `hasPermissions` handles the undefined state). Matches the guard `hasPermissions`/`hasQuestions`/`loading` already apply to `sync()`. | `ensureServerState` throws on a key absent from the connection list, and the throw propagates out of a render-time memo, killing the whole renderer. Sidecar connections are listed only while healthy — `readySandboxConnections`/`readyWslConnections` publish a sandbox or WSL server only in the `ready` state — so any removal, restart, or crash of one drops its key while its session tabs are still mounted. Reproduced by removing a sandbox that had an open session tab: `Error: Notification server not found: sandbox:<name>`. The permission lookup is the sharper of the two because `createMemo` computes eagerly: for a tab mounted after its server disappeared it throws during construction, before any downstream guard runs — `Error: Permission server not found: sandbox:<name>`, reproduced by deleting a sandbox from Settings while the Work side was open. | Keep the guard; take upstream's other changes to the hook. This is an upstream bug (WSL servers hit the identical path), so if upstream fixes it their way, drop ours in favour of theirs. |
0 commit comments