Skip to content

feat(updater): in-app wrapper update button#360

Merged
ilysenko merged 13 commits into
ilysenko:mainfrom
avifenesh:feat/in-app-update-button
May 30, 2026
Merged

feat(updater): in-app wrapper update button#360
ilysenko merged 13 commits into
ilysenko:mainfrom
avifenesh:feat/in-app-update-button

Conversation

@avifenesh
Copy link
Copy Markdown
Collaborator

Summary

  • Add a separate in-app Update button for wrapper updates, shipped as the opt-in codex-wrapper-updater Linux feature (distinct from the upstream DMG Sparkle button, which is unchanged).
  • The button is invisible unless a wrapper update is pending, shows the changelog as a tooltip, applies the update after the app exits, and relaunches into the freshly built version; it hides again once the install is aligned with upstream.
  • Universal git-only detection that works for both packaged (.deb/.rpm/pacman) and user-local (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

  • Opt-in feature, disabled by default (linux-features/features.example.json stays empty; no defaultEnabled). Enable via the "Check for Codex Desktop Linux updates" toggle (codex-linux-wrapper-updates-enabled).
  • Detection is git-only (shallow fetch, no GitHub API / gh / curl); offline simply shows no button.
  • Packaged apply rebuilds + installs via 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).
  • The button uses the custom-DOM pattern with a distinct handler id, marker, and screen position so it never collides with the DMG button.

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_from seam.
  • launcher/start.sh.template — consume the wrapper-update-pending marker, 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 — clean
  • bash -n launcher/start.sh.template; node -e "require('./linux-features/codex-wrapper-updater/patch.js')"; bash tests/scripts_smoke.sh — pass
  • Live: clicked the button in an isolated desktop; the app exited, the launcher rebuilt + relaunched into the new build, and the button hid once aligned.

Versioning

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.

avifenesh added 8 commits May 29, 2026 06:39
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.
@avifenesh
Copy link
Copy Markdown
Collaborator Author

This is part of a focused stack of Linux update-UX PRs, split per the "one logical change per PR" guidance. Merge order (independents first):

Each PR is diffed against its dependencies until they land; I'll rebase the dependents as the bases merge. The three independents (#357/#358/#359) can be reviewed and merged in any order. Happy to reshape the stack or squash if you'd prefer fewer PRs.

Copy link
Copy Markdown
Owner

@ilysenko ilysenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good as an opt-in Linux feature concept, but I would not merge this stack yet.

Blockers:

  • The branch conflicts with main and 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.template calls apply_pending_wrapper_update "$@" before exporting CODEX_LINUX_APP_DIR and CODEX_LINUX_LAUNCHER_CMD. wrapper_apply.rs says 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-desktop instead 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-update before apply_pending_wrapper_update, and make marker paths app-id aware.
  • Add tests for the marker/app-id path and install-type detection.

@avifenesh
Copy link
Copy Markdown
Collaborator Author

Looks good as an opt-in Linux feature concept, but I would not merge this stack yet.

Blockers:

  • The branch conflicts with main and is stacked on unresolved wrapper-update work. The foundation issues from feat(updater): track wrapper releases and surface a changelog #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 feat(updater): add "install on close" toggle for auto-updates #357, which has its own unresolved behavior mismatch. Please do not stack this feature on blocked changes.
  • launcher/start.sh.template calls apply_pending_wrapper_update "$@" before exporting CODEX_LINUX_APP_DIR and CODEX_LINUX_LAUNCHER_CMD. wrapper_apply.rs says 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-desktop instead 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-update before apply_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.
Are we fine with first merging the ones that has no dispute?

@avifenesh
Copy link
Copy Markdown
Collaborator Author

@ilysenko i see that you did, great, will iterate one by one.

@ilysenko
Copy link
Copy Markdown
Owner

@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.

@ilysenko
Copy link
Copy Markdown
Owner

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:

  • Rebase on current main first. feat(updater): track wrapper releases and surface a changelog #359 is merged now, and this branch still carries old wrapper-state/app/changelog files and conflicts with current main.
  • Keep codex-wrapper-updater as a self-contained optional Linux feature, disabled by default.
  • Core should stay minimal and generic. At most, add small generic hook points that Linux features can use.
  • Core should not hardcode codex-wrapper-updater marker paths, apply flow, or UI behavior.
  • The feature should own the webview button, marker handling, docs, staged hooks/scripts, and best-effort UI behavior under linux-features/codex-wrapper-updater/.
  • If a launcher hook is needed, make it generic for Linux features, app-id aware, and run only after the launcher exports the env needed by the updater.
  • Marker/state paths must use the active app id / XDG paths, not hardcoded codex-desktop.
  • The apply path must fail closed: if deps are missing, git/network fails, rebuild fails, install fails, or relaunch cannot be resolved, normal app launch must continue and the state must not be marked as installed.
  • Do not clear the wrapper candidate or pending marker unless the update was actually installed.
  • Use the bounded/non-interactive git behavior from the merged wrapper detection code.
  • Add tests for disabled feature no-op, app-id scoped markers, launcher env order, install type detection, missing dependency behavior, failed apply retry/marker cleanup, and packaged vs user-local paths.
  • The webview patch should be optional, idempotent, and avoid duplicate or overlapping buttons.

@avifenesh
Copy link
Copy Markdown
Collaborator Author

@ilysenko I think that making the button an optional feature will kill the cause.
You have it in Codex, nothing that I invented, but instead of the user manually checking or having a transparent cron, he gets a small notifier. I can make it opt-out if someone doesn't want it, but making opt-in a feature that's supposed to help users stop marking things opt in, is a little overkill of the feature.
For the tests, sure, we'll make it more extensive.

But as always, your repo, your rules. lmk.

@avifenesh avifenesh force-pushed the feat/in-app-update-button branch from fbbb375 to 9ec79c6 Compare May 30, 2026 06:37
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.
@avifenesh avifenesh force-pushed the feat/in-app-update-button branch from 9ec79c6 to 0087dc1 Compare May 30, 2026 06:37
@avifenesh
Copy link
Copy Markdown
Collaborator Author

@ilysenko I'm still coding :)
Hands off?

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.
@avifenesh
Copy link
Copy Markdown
Collaborator Author

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.

@ilysenko
Copy link
Copy Markdown
Owner

@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 main can still become an update candidate; if it builds and installs successfully but breaks at runtime, users who never asked for this channel can end up with a broken Codex install and we get a wave of issues.

That is exactly why we have linux-features/, and now also local/private Linux features: advanced users can enable or build whatever workflow they want without putting that risk on everyone else.

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.

@ilysenko
Copy link
Copy Markdown
Owner

ilysenko commented May 30, 2026

@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 main commits directly. We should tie updates to an explicit wrapper release/version channel, so only intentionally published versions become update candidates.

@avifenesh
Copy link
Copy Markdown
Collaborator Author

@ilysenko I absolutely understand! Was my take only.
I'll just add that Linux users who use Codex are most certainly devs, but I genuinely understand your point.
I aligned my changes with your decision, as well as the stacked PRs.

To maintainer note:
I'm kind of “working” in OSS.
At work I have to be “on call” since it’s work.
At my private repos and as a belief, although I don't always manage to follow it, you are working for free. If something breaks, it's ok; you don't owe anything. If somebody is not happy he can find another solution or contribute a fix. The expectation from owners and maintainers to work for free and be on the clock to make everything work and if something breaks, not to sleep until it's fixed, kills open source.

Copy link
Copy Markdown
Owner

@ilysenko ilysenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@ilysenko ilysenko merged commit fc08988 into ilysenko:main May 30, 2026
6 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.

2 participants