Use codex update for standalone installs#3626
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review 2 blocking correctness issues found. This PR introduces significant new update functionality for standalone Codex installs with complex shell command generation and UI state management. Three unresolved review comments identify potential bugs in the notification dismissal and toast lifecycle logic that warrant human review. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b49bd08. Configure here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| const relevantGroups = groups.filter((group) => | ||
| relevantEnvironmentIdsRef.current.has(group.environmentId), | ||
| ); | ||
| if (rows.length === 0 && relevantGroups.every((group) => group.connectionState === "ready")) { |
There was a problem hiding this comment.
🟡 Medium components/ProviderUpdateEnvironmentRows.tsx:480
onEmpty?.() at line 481 can dismiss the wrong notification. After the user interacts, the host keeps the old toast mounted across notificationKey changes and updates notificationKeyRef.current to the latest key. When the old interacted toast's rows empty out and onEmpty fires, the host closes that toast — but dismissPrompt dismisses notificationKeyRef.current, which is now the new key, not the old toast's key. The fresh update set is marked dismissed and its prompt never appears. Consider tracking each toast's own key so onEmpty dismisses only the toast it belongs to, or guard onEmpty so it does not fire when the notification key has changed since the toast was interacted with.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ProviderUpdateEnvironmentRows.tsx around line 480:
`onEmpty?.()` at line 481 can dismiss the wrong notification. After the user interacts, the host keeps the old toast mounted across `notificationKey` changes and updates `notificationKeyRef.current` to the latest key. When the old interacted toast's rows empty out and `onEmpty` fires, the host closes that toast — but `dismissPrompt` dismisses `notificationKeyRef.current`, which is now the *new* key, not the old toast's key. The fresh update set is marked dismissed and its prompt never appears. Consider tracking each toast's own key so `onEmpty` dismisses only the toast it belongs to, or guard `onEmpty` so it does not fire when the notification key has changed since the toast was interacted with.
There was a problem hiding this comment.
Thanks for flagging this. This path does not invoke dismissPrompt: closeEmptyPrompt calls toastManager.close(active.toastId) directly, while our custom toast.data.onClose callback is invoked only by handleToastDismissClick for an explicit X-button click. A programmatic manager.close() therefore cannot call dismissNotificationKey or dismiss the newer key. The live-key behavior on a manual X click is intentional and separate from the onEmpty path.
| const visibleResult = | ||
| (group.candidates.length > 0 && storedResult !== undefined && !hasAttemptedCandidate) || | ||
| (storedResult?.view.phase === "succeeded" && hasUnattemptedCandidate) | ||
| ? undefined | ||
| : storedResult?.view; |
There was a problem hiding this comment.
🟡 Medium components/ProviderUpdateEnvironmentRows.tsx:439
visibleResult never clears a stored success when group.candidates becomes empty. The condition (group.candidates.length > 0 && ...) || (storedResult?.view.phase === "succeeded" && ...) both short-circuit when there are no candidates, leaving visibleResult set to storedResult?.view. The row stays in "success", and the .filter(...) keeps rows.length nonzero because the status isn't "idle" — so onEmpty never fires and the host toast stays open indefinitely after a successful update. Consider adding a guard so visibleResult returns undefined when group.candidates.length === 0.
| const visibleResult = | |
| (group.candidates.length > 0 && storedResult !== undefined && !hasAttemptedCandidate) || | |
| (storedResult?.view.phase === "succeeded" && hasUnattemptedCandidate) | |
| ? undefined | |
| : storedResult?.view; | |
| const visibleResult = | |
| group.candidates.length === 0 || | |
| (group.candidates.length > 0 && storedResult !== undefined && !hasAttemptedCandidate) || | |
| (storedResult?.view.phase === "succeeded" && hasUnattemptedCandidate) | |
| ? undefined | |
| : storedResult?.view; |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/ProviderUpdateEnvironmentRows.tsx around lines 439-443:
`visibleResult` never clears a stored success when `group.candidates` becomes empty. The condition `(group.candidates.length > 0 && ...) || (storedResult?.view.phase === "succeeded" && ...)` both short-circuit when there are no candidates, leaving `visibleResult` set to `storedResult?.view`. The row stays in `"success"`, and the `.filter(...)` keeps `rows.length` nonzero because the status isn't `"idle"` — so `onEmpty` never fires and the host toast stays open indefinitely after a successful update. Consider adding a guard so `visibleResult` returns `undefined` when `group.candidates.length === 0`.
|
Closing this as stale. I’ll follow up with a smaller, focused replacement. |
|
Replacement: #4185. This version is intentionally scoped to delegating the normal standalone install update to |

Summary
Standalone Codex installs update via
codex updateinstead of npm global installs.packages/standalone/releases/<version>/codex, which thecurrentand~/.local/bin/codexsymlinks resolve to — pluscurrent/codex,current/bin/codex, and customCODEX_HOMEroots.CODEX_HOMEpinned to the matched install root, sincecodex updatelocates its install via$CODEX_HOMEand the maintenance runner spawns with the server env.updateCommandmirrors the spawned action (full path for explicitly configured binaries), so the copyable manual command targets the selected install.updateActionKey(JSON-encoded lockKey/executable/args/env) gates one-click grouping to instances that run the same update action.Package-manager installs (npm/pnpm/bun/Vite Plus/Homebrew) keep their existing update paths.
Known limitation: instances reaching the same install through different spellings (bare name vs explicit path) don't group for one-click; each still updates individually.
UI Changes
No visual changes.
Validation
Checklist
Note
Medium Risk
Touches provider update spawning (env, resolved executables) and advisory/UI grouping logic; incorrect path or env handling could update the wrong install or mislead one-click grouping, though behavior is heavily covered by new tests.
Overview
Adds native
codex updatefor standalone Codex installs: detectspackages/standalonelayouts (including symlinkedcurrentand versioned release binaries), pinsCODEX_HOMEon the spawned update, and disables one-click updates when the configured binary is a pinned release path thatcodex updatewould not replace.Refactors provider maintenance so native updates use the resolved binary path (drops static
executableon Claude/OpenCode/Codex), supports optionalderiveEnvon update actions, passesenvthrough the maintenance runner spawn, and builds copyable manual commands via shell-safe quoting (POSIX / PowerShell) instead of naive joins. Version advisories gain optionalupdateActionKeyso the UI can tell apart actions that share the same command string.Update notification UI splits outdated providers into visible
candidates, group-safeoneClickCandidates, and dispatchablerunnableCandidates; blocks one-click while a same-driver sibling is active; prefers active instances in the sidebar pill; and tightens row/toast lifecycle (no update for settings-only rows,onEmptywhen nothing remains, stale errors cleared when targets disappear).Reviewed by Cursor Bugbot for commit 2d6adf5. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add native one-click update support for Codex standalone installs
isCodexStandaloneCommandPathandderiveCodexStandaloneUpdateEnvironmentto CodexDriver.ts to detect standalone installs and deriveCODEX_HOMEfor update commands; pinned release binary paths are excluded from one-click eligibility.resolvePackageManagedProviderMaintenancein providerMaintenance.ts so the resolved command path is used as the executable and derived env vars are passed through, replacing statically configured executables.nullwhen safe rendering is not possible.ServerProviderVersionAdvisoryschema with an optionalupdateActionKeyfield, used by the frontend to deduplicate and gate one-click update actions across driver instances.ProviderUpdateEnvironmentRowsto dispatch updates only forrunnableCandidates, hide Update/Retry buttons when no runnable candidates exist, and close the toast viaonEmptywhen all interacted environments reach a ready state.updateCommandin advisories may now benull; callers that assumed a non-null string must handle the null case.Macroscope summarized 2d6adf5.