feat(ENG-849, ENG-671): in-app software updates (shell reinstall notice + check-for-updates) - #453
Open
pnewsam wants to merge 10 commits into
Open
feat(ENG-849, ENG-671): in-app software updates (shell reinstall notice + check-for-updates)#453pnewsam wants to merge 10 commits into
pnewsam wants to merge 10 commits into
Conversation
The Electron shell (src/main, preload, runtime, native deps) isn't covered by
OTA — it only updates via a manual reinstall — so all we can do is notice a
newer published shell and point the user at the installer. Detection only; no
download/install (that's ENG-850).
Behavior
- The boot/periodic poll compares the installed shell CalVer against a new
`shellVersion` field in latest.json; if strictly newer it pushes a
`shell-available` status over the existing UI_UPDATE_STATUS channel.
- Sidebar: a dismissible "New version available — Download" banner opening the
installer URL; dismissal is per-version (localStorage) so it re-notifies when
a newer shell ships.
- Settings → Updates: an "App update available" card (current → latest +
Download), shown regardless of banner dismissal.
- Download targets the per-platform CloudFront installer (prod
mindshub-cowork-latest.{pkg,exe}).
Two deliberate decisions
- Prod-only: the manifest is prod-only and a non-prod build must never be sent
to a prod installer (ENG-676), so checkForShellUpdate self-gates to
buildKindStrict() === 'prod'. shellDownloadUrl still maps stable → -staging
(tested) so it stays correct if non-prod is ever enabled.
- Explicit shellVersion (not the manifest's UI-bundle `version`): publish-ui.yml
emits it ONLY on the auto-release path (via a new shell_version input from
release.yml), where a prod installer actually ships — so a UI-only re-publish
can't fabricate a phantom reinstall notice. Absent shellVersion → no notice.
- update-logic.ts: UIManifest.shellVersion + parse, shellUpdateIsNewer,
shellDownloadUrl (+ 11 unit tests).
- updater.ts: checkForShellUpdate() orchestrator + poll wiring.
- host.ts / global.d.ts: shell fields on the update-status payload.
- App.jsx / Sidebar.jsx / SettingsView.jsx: state, banner, and Settings card.
- publish-ui.yml / release.yml: emit shellVersion on the release path.
- docs/update-behavior.md: shell-notice section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pnewsam
force-pushed
the
paul/eng-849-surface-shell-installer-updates-in-app
branch
from
July 27, 2026 16:23
f86e718 to
c46da81
Compare
The shell-available notice is pushed on the boot/periodic poll, but an OTA auto-apply reloads the window and the re-mounted renderer loses that push — and the next one is up to a poll interval (4h) away. So on exactly the boot where UI/server also auto-update, the reinstall notice flashes and vanishes, hiding it from the users most likely to be on a stale shell. Cache the last shell status in main and let the renderer re-pull it on mount via a new UI_SHELL_UPDATE_GET channel (host.getShellUpdate). The pull is race- free (the renderer asks when its own effect runs) and degrades to null on web or an older shell whose main lacks the handler. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the OTA update When both a shell reinstall and an OTA (UI/server) update are pending, the sidebar stacked two near-identical sage banners with different versions and verbs (Install vs Download), which reads as a confusing double-ask. Reinstalling ships a current UI/server bundle anyway, so the shell notice supersedes the OTA one. Suppress the OTA banner while a shell reinstall is pending; dismissing the shell notice (its prop goes null upstream) brings the OTA banner back for anyone who can't reinstall now but could still apply the hot update. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This comment was marked as resolved.
This comment was marked as resolved.
…ell-aware Combines PR #449 (ENG-671) into this PR and integrates it with the shell notice so the two update paths agree (previously #449's manual check reported only UI + server, and could say "up to date" while a newer shell was available). - checkForUpdates() now runs the shell check alongside UI/server; a shell-check failure is swallowed to "no update" so it can't sink the whole check. - summarizeUpdateCheck folds the shell in: it counts toward updateAvailable (so "up to date" can't lie) but stays a distinct shellUpdateAvailable flag, since the shell is download-applied, not apply-in-place. - Settings: one unified "Software updates" section replaces the two separate ones. UI/server show an "Update now" apply card; a pending shell shows a "Download update" card, sourced from the fresh check or the background poll (so it still surfaces on a plain Settings visit, ENG-849). handleDownloadShell Update takes an optional explicit URL for the check-sourced case. update-logic.ts stays at 100%; added shell coverage to summarizeUpdateCheck and updated the exact-match assertions for the new field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This comment was marked as resolved.
This comment was marked as resolved.
6 tasks
…e release manifest Codex review on #453 found two blocking renderer bugs and two release-workflow gaps: - host.ts: getShellUpdate was a named export but missing from the curated `host` object App.jsx imports, so host.getShellUpdate() threw a TypeError at mount before the .catch could run. Add it to the object; add a test that guards the curated surface (not just the named export). - App.jsx: only the mobile SettingsView instance got shellUpdate / onDownloadShellUpdate; the desktop (two-column) instance didn't — so the Settings notice never showed on desktop and Download would call undefined. Pass both props to the desktop instance; cover the desktop download path with a render test. - release.yml: publish-ui depended only on auto-release, racing build-deploy-prod — it could write shellVersion before (or without) the installer uploading. Depend on build-deploy-prod too, so the manifest lands only after the installer. - publish-ui.yml: a UI-only publish (empty SHELL_VERSION) rewrote latest.json without shellVersion, erasing the last-published installer version and killing the reinstall notice. Carry the existing shellVersion forward when none is supplied. typecheck + purity + full suite (616) green; verified the manifest preserve logic across release / UI-only / first-publish / malformed-prior cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This comment was marked as resolved.
This comment was marked as resolved.
…ly try/catch Concision pass on the Settings "Software updates" section: - The apply-available and shell-reinstall cards shared an identical sage-card container + body style; hoist to UPDATE_CARD_STYLE / UPDATE_CARD_BODY_STYLE so they can't drift. - handleApplyUpdateNow had a try/catch whose branches both mapped the outcome + set the same two flags; normalize applyUpdate()'s throw to `false` via .catch so one mapping covers both the resolved-false and thrown cases. Roughly line-neutral — the win is de-duplication, not line count. Behavior unchanged; typecheck + purity + full suite (616) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… version" for the shell The Settings check surfaced two look-alike cards that read as "two app versions to manage", when the two are really different actions: UI/server updates apply themselves by restarting the app, while only the shell is a genuine new version the user downloads and reinstalls by hand. Split the language to match the behavior (copy-only; detection and actions unchanged): - UI/server card → "Update ready" + "Restart the app to apply it" + "Restart now" (was "Update available" / "Update now"). The sidebar OTA banner chip likewise becomes "Update ready" / "Restart" (was "Update available" / "Install"). - Shell card keeps the "New app version X" + "Download update" / reinstall wording — the one place version-management language belongs. - Section subtitle now states the model: UI/server apply automatically on restart; only a new app version is downloaded and reinstalled by hand. Interim step toward ENG-850 (shell self-update), which removes the distinction entirely. Tests updated + a new one locks the restart framing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 27, 2026
pnewsam
marked this pull request as ready for review
July 27, 2026 23:53
Contributor
Author
|
Preserving Claude's PR description here for reference: Description
How this relates to the existing OTA updatesThe crux for review: shell and UI/server OTA are two independent mechanisms sharing one 4h poll and one status channel (
Two deliberate design decisions (shell)
Type of change
Verification
Notes for reviewers
Fixes ENG-849. Closes ENG-671. 🤖 Generated with [Claude Code](https://claude.com/claude-code |
This was referenced Jul 28, 2026
Manual QA of the update banners surfaced that the sidebar "Restart" apply path went silent on failure: handleApplyUpdate's catch set the update status to phase 'error', but nothing in the renderer renders that phase — so a failed apply (network drop mid-download, corrupt bundle, SHA mismatch) just made the banner vanish until the next poll (up to 4h). The Settings "Software updates" apply path already surfaced "Couldn't apply / Try again"; the sidebar did not. Preserve the version on the error status and render an "Update failed / Try again" banner in the sidebar that re-triggers the apply, mirroring Settings. A pending shell reinstall supersedes it, same as the "Update ready" banner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shell-reinstall notice handed the user an installer download with no
next-step guidance: the Settings card only said "the app updates by
downloading and reinstalling" and the Download button gave no hint that the
app must be quit and the installer opened by hand. Manual QA hit exactly this
"now what?" gap once the file landed in Downloads.
- Settings subtitle now states the sequence ("Download the installer, then
quit MindsHub Cowork and open it to finish updating"); button reads
"Download installer" so it's clearly a file, not an in-app apply.
- After the click, the card flips to "Installer downloading — when it's done,
quit ... and open the installer" with a de-emphasized "Download again"
retry. Keyed by version so a newer notice later in the session starts fresh.
- Sidebar banner tooltip extended with the quit-and-open step.
Copy + one small click-state only; detection/gating unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZoranPandovski
approved these changes
Jul 29, 2026
ZoranPandovski
left a comment
Member
There was a problem hiding this comment.
I've added one comment
| return null; | ||
| } | ||
|
|
||
| export async function checkForUpdates(): Promise<UpdateCheckSummary> { |
Member
There was a problem hiding this comment.
in host.checkForUpdates(), if the reply has no ok field, assume it came from an old shell and translate it to the new format instead of passing it through:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Currently the hot/OTA updates flow is enabled for UI code at
cowork/src/rendererand server code atcowork-server. However there is no mechanism for hot-updating the electron shell code atsrc/main. The only way for an end-user to update the electron shell code is to redownload manually.This limitation is okay if we commit to keeping the electron shell thin, and updating it only rarely. But in practice there is still a lot of churn in the shell code. The proper long-term fix would be to introduce Electron autoUpdater which can provide auto-updates for the whole application, rather than just pieces of it here and there. That work is underway with this PR and this ticket, but as an interim fix we can make the user aware when there is a new shell version that needs to be downloaded. That is the purpose of this PR.
It includes two pieces:
If both "restart" and a "download" are both applicable, then the "download" banner takes precedence, because it will update both shell and ui/server.
Screenshots
New section

Demo of "Check for updates" flow
https://github.com/user-attachments/assets/23ea0085-a39b-4c40-b193-5d86b55b1e2c
Demo of "Download" banner flow
https://github.com/user-attachments/assets/d0fd0c76-0b1b-4bdd-849b-41805d510b5d
Demo of "Restart" banner flow
https://github.com/user-attachments/assets/832df5f0-7d4d-4316-b748-7e193d04468e
Testing
Tested the following behaviors manually, with Claude's guidance.
shellVersion(prod-only; fails closed otherwise), survives an OTA reload, dismissal is per-version.