feat(updater): in-app wrapper update button#360
Conversation
The local update manager installs a ready update automatically when the app exits (auto_install_on_app_exit defaults to true and nothing writes config.toml, so it is effectively always on). Give users a way to opt out and require an explicit Update click instead. - Add a "Updates" section to the Linux Keybinds settings page with an "Install updates when you close Codex" toggle, backed by the new codex-linux-auto-update-on-exit settings key. Defaults on, preserving current behavior; turning it off makes updates wait for the in-app Update button. - The key is allowlisted automatically via linuxSettingsKeys, so the existing main-bundle persistence helper writes it to settings.json. - codex-update-manager reads the toggle from settings.json as an overlay over its config: present -> override auto_install_on_app_exit, absent -> keep config/default. Path + value coercion mirror the launcher's linux_setting_enabled helper (CODEX_LINUX_SETTINGS_FILE / XDG_CONFIG_HOME / $HOME/.config, app-id from CODEX_LINUX_APP_ID/CODEX_APP_ID). Fails open to the default on any missing/malformed input. Tests: settings override reads bool/string/number, falls back to None on missing/malformed/absent-key.
codex-update-manager only tracked the upstream Codex DMG. The wrapper repository ships its own Linux features and fixes, but a user running the local update manager had no way to learn about — or pull — newer wrapper builds, and never saw what changed. Add an opt-in wrapper-version axis: - New `wrapper` module: git-based, read-only detection against the builder bundle checkout. Reads the installed commit + version (updater/Cargo.toml), queries the remote head with `git ls-remote`, and fetches objects (not the working tree) so the candidate changelog can be read. Never mutates the user's checkout. A non-git (packaged, frozen) bundle degrades gracefully to no-op. - New `changelog` module: pure Keep-a-Changelog parser + semver selection. Surfaces curated sections newer than the installed version, including `[Unreleased]`; the caller falls back to `git log --oneline` commit subjects when versions can't be mapped. - State gains optional wrapper fields (installed/candidate version + commit, changelog); config gains `enable_wrapper_updates` (default false), `wrapper_remote`, and `wrapper_branch`. All new fields use serde defaults so existing state.json / config.toml keep parsing. - New `check-wrapper` subcommand; `check-now`/daemon detect wrapper updates alongside the DMG check when enabled; `status --json` and a desktop notification report the detected update and its changelog. Off by default, so existing DMG-only behavior is unchanged. Tests: changelog parse/semver (pure), git-based detection against a local remote (newer head, up-to-date, non-git dir), all serialized via the shared env lock and a $PATH-independent git binary.
Wrapper updates (this repo's own Linux features/fixes) were detection-only — a notification/CLI, never an in-app button. Add a SEPARATE in-app Update button for the wrapper axis, distinct from the Sparkle DMG button, invisible unless a wrapper update is pending, that works for all install types. Detection (wrapper.rs) reworked to be universal and git-only (no gh/curl): - Compare the installed build timestamp (package version prefix) against the upstream HEAD commit date obtained via a git shallow fetch into a cache dir. Works for packaged and user-local installs alike; never touches the user's working tree. Changelog read from the fetched CHANGELOG.md. Enablement: a "Check for Codex Desktop Linux updates" settings.json toggle (codex-linux-wrapper-updates-enabled) overrides enable_wrapper_updates, paralleling the auto-install toggle. Off by default. Apply path (wrapper_apply.rs + apply-wrapper-update subcommand): user-local reuses ~/.local/bin/codex-desktop-update (in-place install.sh, no pkexec); packaged fetches fresh wrapper source, rebuilds a package from the cached DMG via builder::build_update_from, and installs with pkexec — degrading to a notification when build tools are absent. Button: new opt-in linux-features/codex-wrapper-updater feature (custom DOM button, distinct handler id/marker/position), reads the wrapper state fields, writes a wrapper-update-pending marker + quits on click. Launcher: apply_pending_wrapper_update consumes the marker before warm-start detection, runs apply-wrapper-update, re-execs into the new build on success, and leaves the marker for retry on failure. Tests: universal timestamp-compare + shallow-fetch detection, offline/garbage graceful None; all existing tests green.
Live testing surfaced three issues preventing the wrapper Update button from ever showing: - Launcher resolved the updater binary with /usr/bin (system .deb) ranked before any user-local path, so a stale packaged binary without check-wrapper shadowed a newer user-local build. Add ~/.local/bin and rank user-local locations ahead of /usr/bin; resolve+export CODEX_UPDATE_MANAGER_PATH before launching Electron so in-app features can spawn the updater regardless of the Electron process PATH. - The button spawned a bare `codex-update-manager`, which failed when it was not on Electron's PATH. Spawn via CODEX_UPDATE_MANAGER_PATH when set. - Detection relied solely on the main-process startup spawn, which raced the button's first poll (the git shallow fetch lands seconds later). The button now triggers a check itself on startup and on each interval, and polls status a few times early (2/5/9/15/22s) so it appears within seconds instead of waiting a full 30s cycle. Verified end-to-end: clean cold-start populates candidate_wrapper_commit and the green Update button renders top-right.
The injected Update button rendered but physical clicks passed straight through to the content behind it (the button inherited pointer-events:none from the app shell's drag region). Programmatic .click() and the IPC handler worked, which masked it; only real mouse clicks were no-ops. Set pointer-events:auto on the button so it actually receives clicks. Verified end-to-end via an agent-workspace launch: physical click now lands on the button, writes the wrapper-update-pending marker, and the app exits to apply on next launch.
…ix routing Three follow-ups so the wrapper Update button behaves end-to-end: - Detect alignment by commit identity. Read the installed wrapper commit from the running app's <app_dir>/.codex-linux/build-info.json (source.commit) and compare it to the upstream HEAD commit. Equal => no update => button hidden. This fixes the "never aligns" bug where a user-local rebuild left the stale system .deb package version in place, so the timestamp compare kept showing the button forever. Falls back to the package-version build timestamp when build-info is absent. - Auto-reopen after update. The button's install action now spawns a detached watcher (mirrors the DMG install-after-quit pattern) that waits for the app to exit, runs `apply-wrapper-update`, then relaunches via CODEX_LINUX_LAUNCHER_CMD. No manual relaunch needed. The marker + launcher handoff remain as a fallback. - Fix install-type routing for machines with both a .deb and a user-local install. detect_install_type now prefers the launcher's CODEX_LINUX_APP_DIR hint (under /opt => packaged, else user-local) over the ambiguous builder-bundle/dpkg heuristic. The user-local apply path gains an install.sh fallback when the contrib codex-desktop-update helper is absent, and clears the pending marker on success. Launcher exports CODEX_LINUX_APP_DIR and CODEX_LINUX_LAUNCHER_CMD to Electron. Tests: commit-equality (aligned) and commit-differs (update) detection paths.
ilysenko
left a comment
There was a problem hiding this comment.
Looks good as an opt-in Linux feature concept, but I would not merge this stack yet.
Blockers:
- The branch conflicts with
mainand is stacked on unresolved wrapper-update work. The foundation issues from #359 still matter here: commit inequality is treated as an update without an ancestry check, and stale wrapper candidate/changelog state is not cleared when a later check finds no update. - The PR also carries the auto-install-on-exit settings overlay from #357, which has its own unresolved behavior mismatch. Please do not stack this feature on blocked changes.
launcher/start.sh.templatecallsapply_pending_wrapper_update "$@"before exportingCODEX_LINUX_APP_DIRandCODEX_LINUX_LAUNCHER_CMD.wrapper_apply.rssays those launcher env vars are used to choose the correct install type and relaunch path, but the manual-relaunch marker path will run without them. That can mis-detect user-local vs packaged installs or fail the user-local fallback.- The marker paths are hardcoded under
codex-desktopinstead of using the active app id. Side-by-side identities can write/read the wrong marker or miss their own pending update.
Other risks:
- The packaged path rebuilds and installs a native package from a GitHub-fetched wrapper checkout. That is a large runtime/update surface for an in-app button, so it needs much stronger tests around failure, retry, install type detection, and marker cleanup.
- The webview runtime injects a floating DOM button by heuristically finding headers. Since this is an opt-in Linux feature that may be acceptable, but it should be documented as best-effort and should fail closed without visual overlap.
missing_build_dependency()only checks a small subset of tools. A packaged rebuild can still fail later for missing package tooling or build dependencies; the UI should make that failure visible and recoverable.
Suggested path:
- First land a fixed, rebased wrapper-detection foundation.
- Then keep this PR focused on the opt-in Linux feature and launcher handoff only.
- Export the launcher env needed by
apply-wrapper-updatebeforeapply_pending_wrapper_update, and make marker paths app-id aware. - Add tests for the marker/app-id path and install-type detection.
@ilysenko if its ok, it will be much easier if we first land the non stacked that you agree on, then the diff will be more focused. my intent, and i might missed something is to install the default of the user (.deb/..../git), mine is git because im working on the repo and breaking my app daily. |
|
@ilysenko i see that you did, great, will iterate one by one. |
|
@avifenesh I think I’ll wait for #357 and #359 first. Don’t worry too much about rebasing every dependent PR right now; if conflicts remain after the fixes, I can help resolve/rebase them on my side before merging. |
|
My current thinking is that this should live almost entirely as a Linux feature, disabled by default. The reason is maintenance: this is useful for some workflows, but it is a fairly large update/apply surface. If too much of it becomes part of the core launcher/updater path, we will likely create long-term maintenance headaches for users who never enabled this feature. Suggested target shape:
|
|
@ilysenko I think that making the button an optional feature will kill the cause. But as always, your repo, your rules. lmk. |
fbbb375 to
9ec79c6
Compare
Keep wrapper-update detection on the merged ilysenko#359 foundation and add the smallest apply/UI layer around it. The new codex-wrapper-updater Linux feature stays disabled by default, stages CODEX_LINUX_ENABLE_WRAPPER_UPDATES=1 when enabled, adds an optional app-id-scoped in-app Update button, and writes a pending marker for the launcher to apply before cold start. The launcher exports app-dir/launcher/update-manager context before the handoff, prefers user-local updater binaries over stale packaged ones, and keeps the marker on apply failure so launch can continue and retry later. The updater gets apply-wrapper-update plus a wrapper_status field; packaged apply rebuilds from a fetched wrapper checkout, while user-local apply reuses the installed helper. Add focused feature tests and changelog docs.
9ec79c6 to
0087dc1
Compare
|
@ilysenko I'm still coding :) |
Add coverage for the codex-wrapper-updater feature staying opt-in while staging declarative prelaunch/after-exit hooks. Cover app-id-scoped marker resolution, fail-closed retry preservation, the skip-prelaunch guard, and lock handling without changing runtime behavior.
|
Pushed a follow-up that is tests-only: it only expands coverage for the opt-in wrapper updater feature hooks and marker handling. No behavioral/runtime changes in this push. |
|
@avifenesh I had some time, so I pushed my current take on isolating this into a Linux feature. If you prefer to remove or rewrite my commit while you keep iterating, no problem. My main concern is the default path. You and I are developers, so wrapper self-updates are useful for us, but most users installing this repo just want Codex to launch and update the upstream app. They will never think about wrapper branches, local rebuilds, package rebuilds, or git-based update flows. If this is enabled by default, the updater surface becomes much larger: git/network checks, markers, rebuilds, package installs, relaunch flow, and failure recovery. Even if the check/apply flow is fail-closed, a bad wrapper merge on That is exactly why we have So my preference is: ship it as an optional Linux feature, disabled by default. It can be easy to enable, but it should not affect the default install/update path. |
|
@avifenesh One more maintainer-cost point: if this ever becomes part of the default path, it has to work like clockwork. I watch this repo constantly, review PRs continuously, and often spend time running Codex reviews on high effort - not just occasionally when I feel like it. I think part of the difference here is perspective: you are looking at this as a useful playground for advanced update workflows, while I have to look at the default build as something regular users can simply download, launch, and trust. We already found several issues very early in this feature and spent a lot of time tightening the design. That is normal for a complex updater/apply flow, but it also tells me the future maintenance cost will be real. If you are busy, away, or on vacation, that support burden still lands on me and on the repo. That is another reason I want this to stay optional and disabled by default unless/until it proves extremely stable over time. P.S. A possible path forward: let's keep it optional first and test it there, both you and me. If bugs shake out and it proves stable, we can revisit making it default later. If/when we do that, I would not want it to track arbitrary |
|
@ilysenko I absolutely understand! Was my take only. To maintainer note: |
ilysenko
left a comment
There was a problem hiding this comment.
Reviewed current head 7f9eb85. The wrapper updater is isolated as an opt-in Linux feature, the runtime setting now persists through the Linux global-state path, and the app-id-aware marker/retry flow is covered by tests. Local focused tests and GitHub Actions are green.
Summary
codex-wrapper-updaterLinux feature (distinct from the upstream DMG Sparkle button, which is unchanged).install.sh) installs.Why
The wrapper-update axis (detection + changelog) only fired a notification / CLI output — there was no way to "open Codex, see there's an update, click, continue." This adds that surface as an opt-in feature, mirroring the DMG button's invisible-unless-pending behavior.
Stacked on
This PR builds on two others in the same stack and is diffed against them until they merge:
Please merge #357 and #359 first; this PR's diff will then reduce to just the button + apply path. I'll rebase after they land.
Design / safety
linux-features/features.example.jsonstays empty; nodefaultEnabled). Enable via the "Check for Codex Desktop Linux updates" toggle (codex-linux-wrapper-updates-enabled).gh/curl); offline simply shows no button.pkexec, and degrades to a desktop notification when the build toolchain (cargo / DMG extractor) is missing — never fails mid-rebuild. User-local apply reuses~/.local/bin/codex-desktop-update(no privilege escalation).Source-of-truth files
linux-features/codex-wrapper-updater/{feature.json,README.md,stage.sh,patch.js}(new opt-in feature).updater/src/wrapper_apply.rs(new) — apply per install type.updater/src/{wrapper.rs,app.rs,cli.rs,builder.rs}— universal detection rework,ApplyWrapperUpdate,build_update_fromseam.launcher/start.sh.template— consume thewrapper-update-pendingmarker, apply, and exec-relaunch into the new build.scripts/patches/{shared.js,keybinds-settings.js}— wrapper-enable toggle row.Validation
cargo test -p codex-update-manager— pass (139)cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings— cleanbash -n launcher/start.sh.template;node -e "require('./linux-features/codex-wrapper-updater/patch.js')";bash tests/scripts_smoke.sh— passVersioning
Backward-compatible feature addition to the updater crate; left the version bump out pending merge order of the stack (see #357/#359).
Notes
No linked issue. The auto-reopen watcher and hide-when-aligned behavior are covered; a follow-up PR adds a feature picker on reinstall and a dev-mode/SHA chip on top of this.