Skip to content

fix(desktop): support channel-aware update checks#345

Merged
lefarcen merged 9 commits intomainfrom
fix/desktop-update-channels
Mar 23, 2026
Merged

fix(desktop): support channel-aware update checks#345
lefarcen merged 9 commits intomainfrom
fix/desktop-update-channels

Conversation

@lefarcen
Copy link
Copy Markdown
Collaborator

@lefarcen lefarcen commented Mar 22, 2026

Summary

  • embed the packaged desktop update channel and teach the updater to resolve stable, beta, and nightly feeds explicitly
  • improve update diagnostics by surfacing the effective feed URL plus remote version and releaseDate in updater events and desktop logs
  • cover runtime config and feed resolution behavior with focused desktop tests

Testing

  • pnpm typecheck
  • pnpm lint
  • pnpm test -- --run tests/desktop/runtime-config.test.ts tests/desktop/update-manager.test.ts tests/desktop/sentry-build-metadata.test.ts

Summary by CodeRabbit

  • New Features

    • Added "Check for Updates…" menu item on macOS and a manual check command
    • Introduced nightly update channel and build-time NEXU_DESKTOP_UPDATE_CHANNEL
    • Update UI shows “Checking for updates...” and “You're up to date” messages; added update message styling
  • Bug Fixes

    • Prevent concurrent update checks and surface richer diagnostics/logging with feed details
  • Documentation

    • Documented channel-specific auto-update requirements and diagnostic logging
  • Tests

    • Added tests for feed resolution, runtime config channel, and runtime model plugin

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 59ecdd4f-3f6d-4fad-9c9f-eaebc15a9d39

📥 Commits

Reviewing files that changed from the base of the PR and between 5fac878 and f584d57.

📒 Files selected for processing (7)
  • .github/workflows/desktop-build.yml
  • apps/desktop/main/index.ts
  • apps/desktop/main/updater/update-manager.ts
  • apps/desktop/shared/host.ts
  • apps/desktop/src/components/desktop-shell.tsx
  • tests/desktop/nexu-runtime-model-plugin.test.ts
  • tests/desktop/update-manager.test.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • .github/workflows/desktop-build.yml
  • tests/desktop/update-manager.test.ts
  • tests/desktop/nexu-runtime-model-plugin.test.ts
  • apps/desktop/main/index.ts
  • apps/desktop/main/updater/update-manager.ts

📝 Walkthrough

Walkthrough

Adds channel-aware desktop auto-update (stable/beta/nightly): build/runtime wiring, feed resolution and diagnostics, IPC/menu trigger for manual checks, renderer UI/banner and hook updates for "up-to-date" state, a small runtime-model resolve tweak, docs, and unit tests.

Changes

Cohort / File(s) Summary
Build & Workflow
/.github/workflows/desktop-build.yml
Validate inputs.channel (stable/beta/nightly) and inject NEXU_DESKTOP_UPDATE_CHANNEL into generated apps/desktop/build-config.json.
Runtime Config
apps/desktop/shared/runtime-config.ts
Add NEXU_DESKTOP_UPDATE_CHANNEL to BuildConfig, parse allowed channels, expose updates.channel (env → build → default stable).
Main Process & Updater
apps/desktop/main/updater/update-manager.ts, apps/desktop/main/index.ts
Add nightly feed mapping, feed resolution helpers, currentFeedUrl tracking, diagnostics generation/logging, include diagnostics in updater events, guard concurrent checks, and macOS menu + IPC trigger for manual check.
Shared Types & IPC
apps/desktop/shared/host.ts
Add desktop:check-for-updates command, extend UpdateChannelName with nightly, introduce UpdateCheckDiagnostic, and include diagnostics in updater event typings.
Renderer & Hooks
apps/desktop/src/main.tsx, apps/desktop/src/components/desktop-shell.tsx, apps/desktop/src/components/update-banner.tsx, apps/desktop/src/hooks/use-auto-update.ts
Wire update hook/banner into shell, handle desktop:check-for-updates by invoking update.check(), add "up-to-date" phase with auto-dismiss and userInitiated semantics, adjust banner visibility/messages.
UI Styling
apps/desktop/src/runtime-page.css
Add .update-card-message class for update card message styling.
Runtime Plugin
apps/controller/static/runtime-plugins/nexu-runtime-model/index.js
Compute providerOverride/modelOverride locally; if providerOverride starts with custom_ return only { modelOverride }, else return both.
Web UI
apps/web/src/components/channel-setup/wechat-setup-view.tsx
Make container padding/border conditional on showHeader.
Docs
AGENTS.md
Document that packaged builds must embed NEXU_DESKTOP_UPDATE_CHANNEL (stable/beta/nightly) and require logging of effective feed URL and remote version/releaseDate when available.
Tests
tests/desktop/nexu-runtime-model-plugin.test.ts, tests/desktop/runtime-config.test.ts, tests/desktop/update-manager.test.ts
Add tests for runtime-model plugin behavior, runtime-config channel default/override, and feed URL resolution (including nightly, github, and explicit overrides).

Sequence Diagram(s)

sequenceDiagram
  participant User as User/Menu
  participant Main as Main Process
  participant UpdateMgr as UpdateManager
  participant Feed as Update Feed (R2/GitHub)
  participant Renderer as Renderer

  rect rgba(200,230,255,0.5)
  User->>Main: Select "Check for Updates…"
  Main->>Renderer: send host command `desktop:check-for-updates`
  Renderer->>UpdateMgr: invoke update.check() (userInitiated)
  UpdateMgr->>UpdateMgr: resolveUpdateFeedUrl(env/feed/channel)
  UpdateMgr->>Feed: fetch remote info (version, releaseDate)
  Feed-->>UpdateMgr: return remote metadata
  UpdateMgr->>Main: emit diagnostic events (checking → available/up-to-date/error)
  Main->>Renderer: forward updater events with diagnostics
  Renderer->>Renderer: update banner UI (phase, message, auto-dismiss)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped the channels — stable, beta, nightly bright,
I fetched the feed, I logged the light.
Diagnostics hum, the banner gives a cheer,
Custom models whisper — no provider here.
Update checked, then vanished — tidy and right.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides a summary section explaining what was changed, but does not follow the required template structure (What/Why/How/Affected areas/Checklist sections). Restructure the description to match the repository template: add What, Why, How sections; check affected areas; and confirm all checklist items were completed.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding support for channel-aware update checks in the desktop app, which is the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/desktop-update-channels

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

mrcfps
mrcfps previously approved these changes Mar 23, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
.github/workflows/desktop-build.yml (1)

161-161: Invalid channel values silently default to stable at runtime.

The ${{ inputs.channel }} value is embedded directly without validation. If a typo occurs (e.g., "nightyl"), the runtime will silently fall back to "stable" (per readUpdateChannel() in runtime-config.ts), potentially causing the app to check the wrong update feed with no CI error.

Consider adding early validation in the workflow:

♻️ Optional: Add channel validation step
- name: Validate channel input
  shell: bash
  run: |
    case "${{ inputs.channel }}" in
      stable|beta|nightly) ;;
      *) echo "Invalid channel: ${{ inputs.channel }}" >&2; exit 1 ;;
    esac
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/desktop-build.yml at line 161, Validate the GitHub Actions
input before embedding it into NEXU_DESKTOP_UPDATE_CHANNEL to fail CI on typos:
add a workflow step that checks the inputs.channel value against allowed values
(stable, beta, nightly) and exits non‑zero on mismatch so the runtime fallback
in readUpdateChannel() (runtime-config.ts) cannot silently use "stable"; ensure
the step references the inputs.channel variable and produces a clear error
message on invalid input.
apps/desktop/src/components/desktop-shell.tsx (1)

52-52: Dependency on update causes unnecessary effect re-runs.

The useAutoUpdate() hook returns a new object literal on every render ({ ...state, check, ... } at line 161 of use-auto-update.ts), which isn't memoized. Adding update to this dependency array will tear down and re-register the onDesktopCommand listener on every render cycle.

Since only update.check is used inside the effect, consider depending on the stable check callback directly:

♻️ Suggested fix
+ const { check: checkForUpdates } = update;
+
  useEffect(() => {
    return onDesktopCommand((command) => {
      if (command.type === "desktop:auth-session-restored") {
        setWebSurfaceVersion((current) => current + 1);
        return;
      }

      if (command.type === "desktop:check-for-updates") {
-       void update.check();
+       void checkForUpdates();
        return;
      }

      setActiveSurface(command.surface);
      setChromeMode(command.chromeMode);
    });
- }, [update]);
+ }, [checkForUpdates]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/components/desktop-shell.tsx` at line 52, The effect in
desktop-shell.tsx depends on the unstable update object from useAutoUpdate(),
causing the onDesktopCommand listener to be torn down and re-registered on every
render; change the dependency to the stable callback used inside the effect by
importing/destructuring check from useAutoUpdate() (i.e., const { check } =
useAutoUpdate()) and replace update in the effect dependency array with check so
only the stable check function is tracked and the onDesktopCommand listener
remains stable.
apps/desktop/shared/host.ts (1)

285-292: Make UpdateCheckDiagnostic the single shared interface.

This is now a cross-process contract used by updater IPC payloads. Defining it as an interface here and importing it from main/renderer code avoids shape drift and matches the repo convention for object-shape contracts. As per coding guidelines, "Prefer interface for defining object shapes in TypeScript instead of type aliases".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/shared/host.ts` around lines 285 - 292, Change the exported
UpdateCheckDiagnostic type alias into an exported interface named
UpdateCheckDiagnostic (replace "export type UpdateCheckDiagnostic = { ... }"
with "export interface UpdateCheckDiagnostic { ... }"), update any
imports/usages across main/renderer/updater IPC code to import this shared
interface, and ensure optional fields (remoteVersion, remoteReleaseDate) keep
their optional markers; this centralizes the cross-process contract and prevents
shape drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/main/index.ts`:
- Around line 272-291: The "Check for Updates…" app menu item currently always
binds to triggerUpdateCheck() even when UpdateManager hasn't been constructed,
so clicking can silently no-op; update the menu entry (the object with label
"Check for Updates…" in the appMenu submenu) to either (A) be disabled by
default (add enabled: false) and then enable it when the UpdateManager instance
is created, or (B) change its click handler to call the live manager directly
(e.g., updateManager?.checkNow()) instead of triggerUpdateCheck(); ensure you
reference the UpdateManager instance creation point and flip the menu item's
enabled state or rebind the click when UpdateManager is instantiated so the item
is only active when checkNow() is available.

In `@apps/desktop/main/updater/update-manager.ts`:
- Around line 95-117: getDiagnostic currently exposes this.currentFeedUrl which
may contain credentials or sensitive query params; sanitize the feed URL before
including it in the diagnostics object (e.g., parse the URL and strip username,
password and search/query params) so the returned UpdateCheckDiagnostic contains
only a safe feedUrl. Update getDiagnostic (and any place that constructs
diagnostics for emission) to use the sanitized value, and keep the full
currentFeedUrl only in main-process-only fields if needed for internal use;
ensure logCheck/writeDesktopMainLog and any send(...) that emits update:* events
use the sanitized diagnostic. Reference: getDiagnostic, currentFeedUrl,
UpdateCheckDiagnostic, logCheck, writeDesktopMainLog, send.
- Around line 184-203: Add a simple in-flight guard to serialize concurrent
calls to checkNow(): introduce a private field (e.g. this._checkNowPromise or
this._checking) on the UpdateManager and, at the top of checkNow(), if that
field is set return the existing Promise so callers share the in-flight result;
otherwise assign the Promise for the ongoing check to that field, run the
existing try/catch body, and clear the field in a finally block so subsequent
calls (from startPeriodicCheck() or manual checks) will execute a new check.
Ensure the assigned Promise resolves to the same { updateAvailable: boolean }
shape and that errors still trigger the existing logCheck behavior before
returning { updateAvailable: false }.

In `@apps/desktop/src/main.tsx`:
- Around line 1203-1212: DesktopShell currently renders UpdateBanner but never
the companion UpdateBadge, so once a user dismisses a ready update there is no
in-session affordance to bring the restart CTA back; import and render the
UpdateBadge component (from apps/desktop/src/components/update-banner.tsx) in
the DesktopShell UI and show it conditionally when the update has been dismissed
(e.g., when update.dismissed is true and update.phase === 'ready' or
update.version is present). Wire the badge's click handler to a method that
clears the dismissed state (call update.show() or
update.clearDismiss()/update.setDismiss(false) — add that method to the update
controller if it doesn't exist) so clicking the badge restores the
UpdateBanner/CTA. Ensure the UpdateBadge receives any necessary props
(version/percent/phase) and lives alongside the existing UpdateBanner render
block.

In `@tests/desktop/nexu-runtime-model-plugin.test.ts`:
- Around line 4-7: The test currently uses a machine-specific hardcoded absolute
path in stateModulePath which will fail on other machines or CI; change
stateModulePath to be computed dynamically (like pluginModulePath) by resolving
it relative to the test file using Node's path utilities (e.g.,
path.resolve/path.join with __dirname) so it points to the same
nexu-runtime-model.json used by the plugin; update the assignment of
stateModulePath (and ensure pluginModulePath uses a similar relative resolution
if needed) so both paths are built at runtime rather than hardcoded.

In `@tests/desktop/update-manager.test.ts`:
- Around line 3-26: The test currently defines its own resolveFeedUrl and
bypasses production logic; update the test to exercise the real
UpdateManager.configureFeedUrl (or extract a new pure function used by
UpdateManager) so environment overrides and source routing are validated: call
UpdateManager.configureFeedUrl (or the extracted pure resolveFeedUrl used by
UpdateManager) instead of the local stub, set process.env.NEXU_UPDATE_FEED_URL
in the test and restore it after, instantiate UpdateManager (or a minimal mock)
to verify provider selection for source "r2" vs "github" and that feedUrl/env
var precedence matches the production expression using
process.env.NEXU_UPDATE_FEED_URL ?? this.feedUrl.

---

Nitpick comments:
In @.github/workflows/desktop-build.yml:
- Line 161: Validate the GitHub Actions input before embedding it into
NEXU_DESKTOP_UPDATE_CHANNEL to fail CI on typos: add a workflow step that checks
the inputs.channel value against allowed values (stable, beta, nightly) and
exits non‑zero on mismatch so the runtime fallback in readUpdateChannel()
(runtime-config.ts) cannot silently use "stable"; ensure the step references the
inputs.channel variable and produces a clear error message on invalid input.

In `@apps/desktop/shared/host.ts`:
- Around line 285-292: Change the exported UpdateCheckDiagnostic type alias into
an exported interface named UpdateCheckDiagnostic (replace "export type
UpdateCheckDiagnostic = { ... }" with "export interface UpdateCheckDiagnostic {
... }"), update any imports/usages across main/renderer/updater IPC code to
import this shared interface, and ensure optional fields (remoteVersion,
remoteReleaseDate) keep their optional markers; this centralizes the
cross-process contract and prevents shape drift.

In `@apps/desktop/src/components/desktop-shell.tsx`:
- Line 52: The effect in desktop-shell.tsx depends on the unstable update object
from useAutoUpdate(), causing the onDesktopCommand listener to be torn down and
re-registered on every render; change the dependency to the stable callback used
inside the effect by importing/destructuring check from useAutoUpdate() (i.e.,
const { check } = useAutoUpdate()) and replace update in the effect dependency
array with check so only the stable check function is tracked and the
onDesktopCommand listener remains stable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9a418f6-c34d-4a33-bc03-b9165ce8adfa

📥 Commits

Reviewing files that changed from the base of the PR and between 7cbca12 and 03b9082.

📒 Files selected for processing (16)
  • .github/workflows/desktop-build.yml
  • AGENTS.md
  • apps/controller/static/runtime-plugins/nexu-runtime-model/index.js
  • apps/desktop/main/index.ts
  • apps/desktop/main/updater/update-manager.ts
  • apps/desktop/shared/host.ts
  • apps/desktop/shared/runtime-config.ts
  • apps/desktop/src/components/desktop-shell.tsx
  • apps/desktop/src/components/update-banner.tsx
  • apps/desktop/src/hooks/use-auto-update.ts
  • apps/desktop/src/main.tsx
  • apps/desktop/src/runtime-page.css
  • apps/web/src/components/channel-setup/wechat-setup-view.tsx
  • tests/desktop/nexu-runtime-model-plugin.test.ts
  • tests/desktop/runtime-config.test.ts
  • tests/desktop/update-manager.test.ts

Comment thread apps/desktop/main/index.ts
Comment thread apps/desktop/main/updater/update-manager.ts
Comment thread apps/desktop/main/updater/update-manager.ts
Comment thread apps/desktop/src/main.tsx
Comment thread tests/desktop/nexu-runtime-model-plugin.test.ts Outdated
Comment thread tests/desktop/update-manager.test.ts Outdated
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.

3 participants