Skip to content

fix(desktop): show NexuLoader instead of error card while controller is starting#893

Merged
anthhub merged 1 commit intoshared/rewards-center-integrationfrom
fix/issue-876-relaunch-loading-state
Apr 7, 2026
Merged

fix(desktop): show NexuLoader instead of error card while controller is starting#893
anthhub merged 1 commit intoshared/rewards-center-integrationfrom
fix/issue-876-relaunch-loading-state

Conversation

@anthhub
Copy link
Copy Markdown

@anthhub anthhub commented Apr 7, 2026

What

On desktop app relaunch, replace the "Starting controller... / Retry controller" text card that briefly appeared before the home page with the brand NexuLoader animation.

Why

Issue #876 (P0): after quitting the desktop app and launching it again, users saw a white card with the heading "Starting controller...", body copy "Desktop is waiting for the local controller to report ready before loading the workspace surface.", and two buttons "Retry controller" / "Open control plane" for several seconds before the actual home page appeared. The card looked like an error page, so users reported that the app "lands on the wrong page after loading".

The root cause is in apps/desktop/src/main.tsx: during the short window where the controller is booting but not yet ready, desktopWebUrl is null, so the renderer fell back to a runtime-empty-state text card. The same card was shown for both the normal polling state and the genuine failed state β€” only the heading text differed, and both had a "Retry controller" button, making the "still starting" case indistinguishable from "actually broken".

The setup animation only runs on first install / post-update (gated by needsSetupExtraction), so on every subsequent relaunch there was no animation to cover the polling window, and the card was exposed.

Closes #876

How

Split the activeSurface === "web" fallback into three branches:

  • desktopWebUrl ready β†’ SurfaceFrame with the webview (unchanged)
  • controllerSurfaceState is failed or recovering β†’ the original error card with the Retry button (failure path preserved)
  • normal polling β†’ SurfaceFrame with src={null}, which reuses the built-in NexuLoader overlay that SurfaceFrame already renders before webview did-finish-load

Because the polling loader and the post-mount webview loader are now the same NexuLoader component with the same animation parameters, the visual transition from "controller starting" to "home page" is seamless and never exposes any text or buttons that could be mistaken for an error page.

No behavior change in the failure path: once ensureDesktopControllerReady times out, controllerSurfaceState flips to failed and the original error card with Retry is still rendered. First-install setup animation path is untouched (it runs in the setup overlay layer above this branch).

Affected areas

  • Desktop app (Electron shell)
  • Controller (backend / API)
  • Web dashboard (React UI)
  • OpenClaw runtime
  • Skills
  • Shared schemas / packages
  • Build / CI / Tooling

Checklist

  • `pnpm typecheck` passes
  • `pnpm lint` passes
  • `pnpm test` passes (671 passed / 38 skipped)
  • `pnpm generate-types` run (if API routes/schemas changed) β€” N/A
  • No credentials or tokens in code or logs
  • No `any` types introduced (use `unknown` with narrowing)

Notes for reviewers

  • Manually verified on macOS by stopping and relaunching the desktop app in dev mode: the white "Retry controller" card is gone and users now see the NexuLoader 4-quadrant animation until the home page appears.
  • Dev-loop gotcha surfaced during testing (not fixed in this PR): `pnpm dev start` only rebuilds `dist-electron/` (main + preload) but does not rebuild the renderer bundle in `apps/desktop/dist/`. Since `apps/desktop/main/index.ts:957` always does `loadFile("../../dist/index.html")` regardless of dev / prod, any change to `apps/desktop/src/*.tsx` is invisible unless you manually run `pnpm --filter @nexu/desktop build` before `pnpm dev restart desktop`. Worth tracking as a separate follow-up (either loading the Vite dev server in dev mode, or auto-rebuilding the renderer in `scripts/dev/src/services/desktop.ts`).
  • Failure path preserved: breaking the ready endpoint URL temporarily showed that after the polling timeout, the original "Controller unavailable" card with the Retry button still renders correctly.

…is starting

On relaunch, users saw a white card titled "Starting controller..." with
"Retry controller" and "Open control plane" buttons during the short
window where the local controller was booting but not yet ready. It
looked like the app had landed on an error page instead of the home
page, which is what issue #876 reported.

The only difference between the "polling" and "failed" states in the
old fallback UI was the heading text β€” both rendered the same card and
the same Retry button, so users could not tell "still starting" from
"actually broken".

Split the fallback into three branches:
- desktopWebUrl ready -> SurfaceFrame with the webview (unchanged)
- controller failed / recovering -> the original error card with Retry
- normal polling -> SurfaceFrame with src=null, which reuses the built-in
  NexuLoader overlay that SurfaceFrame already shows before webview
  did-finish-load

The polling loader and the post-mount webview loader are now the same
NexuLoader component with the same animation, so the transition from
"controller starting" to "home page" is visually seamless and never
exposes any text or buttons that could be mistaken for an error page.

Closes #876
@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 7, 2026

Codecov Report

❌ Patch coverage is 90.90909% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
apps/desktop/src/main.tsx 90.90% 1 Missing ⚠️

πŸ“’ Thoughts on this report? Let us know!

@anthhub anthhub merged commit e980cca into shared/rewards-center-integration Apr 7, 2026
18 checks passed
lefarcen added a commit that referenced this pull request Apr 8, 2026
…esktop (#789)

* feat: add desktop rewards center

* feat(quota): add fallback and restore managed model endpoints

- Introduced new API endpoints for triggering automatic fallback to BYOK provider and restoring the default model to a managed (cloud) model.
- Updated OpenAPI schema to include new properties related to BYOK usage and fallback status.
- Implemented QuotaFallbackService to handle fallback logic and model restoration.
- Added BudgetWarningBanner component to notify users about credit status in the UI.
- Enhanced existing services and routes to integrate new quota management features.

* refactor(workspace-layout): enhance rewards link layout and styling

- Updated the rewards link to use a flex layout for better alignment and spacing.
- Increased the size of the Coins icon for improved visibility.
- Adjusted the display of claimed and total credits to enhance readability.
- Refined the progress bar styling for a more polished appearance.

* feat(rewards): integrate cloud balance feature into rewards system

- Updated OpenAPI schema to include cloud balance properties.
- Implemented CloudRewardService for managing rewards status and claims.
- Enhanced NexuConfigStore to fetch and convert cloud rewards status.
- Added tests for CloudRewardService and updated existing tests for NexuConfigStore.
- Updated UI components to display cloud balance information.
- Localized new cloud balance strings in English and Chinese.

* feat(budget-warning-banner): enhance budget warning banner functionality and localization

- Introduced a status configuration for the BudgetWarningBanner to manage different states (warning and depleted) with corresponding styles and texts.
- Updated the component to utilize the new configuration for rendering, improving code readability and maintainability.
- Added new localization strings for budget warning and depleted states in English and Chinese.
- Implemented a debug panel for development purposes to simulate budget banner states.
- Added tests for the BudgetWarningBanner to ensure correct rendering based on status.

* feat(desktop-rewards): implement auto-fallback logic and enhance rewards status handling

- Added logic to automatically trigger a fallback to BYOK when the managed model's cloud balance is depleted.
- Integrated logging for fallback failures to improve error tracking.
- Created unit tests for the new fallback functionality to ensure reliability.
- Updated the BudgetWarningBanner and related UI components to reflect changes in rewards status and actions.
- Enhanced localization strings for clarity in user messaging regarding rewards and credits.

* feat(desktop-rewards): implement auto-fallback logic and enhance rewards status handling

- Added logic to automatically trigger a fallback to BYOK when the managed model's cloud balance is depleted.
- Integrated logging for fallback failures to improve error tracking.
- Created a new test suite for desktop rewards routes to validate fallback behavior and status retrieval.
- Updated existing UI components and localization strings to reflect changes in rewards status messaging.

* refactor(web): extract shared cloud connect hook and fix state bug

- Remove 5 investigation markdown files accidentally committed to repo root
- Fix cloudConnecting state not reset in "Connection already in progress" path
- Extract handleCloudConnect + polling effects into shared useCloudConnect hook
- Deduplicate formatRewardAmount by exporting from home-rewards-teaser

* feat(rewards): add reward share assets and download functionality

- Introduced six new PNG images for reward sharing.
- Created a new module for managing reward share assets, including types and functions for selecting and downloading assets.
- Implemented tests to ensure the integrity of the reward share assets and their download functionality.

* chore: fix import sort order in home.tsx

* feat(rewards): add loading skeleton, auto-fallback toast, and 60s polling

- Show skeleton placeholders instead of fallback tasks during loading
- Add autoFallbackTriggered flag to rewards status schema
- Display warning toast when BYOK auto-fallback is triggered
- Add refetchInterval: 60s to detect balance changes in background

* feat(budget-warning-banner): update warning and depleted states with new headlines and actions

- Refactored the BudgetWarningBanner to use new headline keys for warning and depleted states.
- Updated the UI to include an upgrade action button and removed the previous earn credits button.
- Enhanced localization strings for better clarity in user messaging regarding budget status.
- Added a new RewardTaskIcon component for rendering task icons dynamically.

* feat(rewards): enhance rewards display and localization

- Updated the rewards status hook to disable retries for fetching data.
- Added new localization strings for balance details in English and Chinese.
- Improved the WorkspaceLayout to display rewards balance information with a detailed popup.
- Refactored HomePage to manage session and channel data more effectively, ensuring proper rendering based on session history.
- Added tests to verify the rendering of rewards components and their behavior during loading states.

* feat(rewards): implement virtual reward check and enhance localization

- Added a new module for handling virtual reward checks, including functions to manage task status and delay.
- Updated localization strings in English and Chinese for task checking and claiming processes.
- Refactored the RewardConfirmModal to utilize the new virtual check functionality, improving user feedback during reward processing.
- Introduced tests to validate the behavior of the virtual reward check and description key retrieval.

* fix(rewards): polish desktop credits flows

* feat(controller): implement controller readiness management and budget banner enhancements

- Introduced a new `ensureDesktopControllerReady` function to manage the readiness of the desktop controller, including polling and recovery logic.
- Refactored the `DesktopShell` and `useDesktopRuntimeConfig` to utilize the new controller readiness management.
- Enhanced the budget banner functionality in the `HomePage` to handle dismissal states more effectively, using session storage for tracking.
- Added a new `BudgetDepletedDialog` component to provide user feedback when budget is exhausted.
- Implemented tests for the controller readiness logic and budget banner dismissal behavior to ensure reliability.

* feat(rewards): implement GitHub star verification and proof URL handling

- Added a new `GithubStarVerificationService` to manage GitHub star session preparation and verification.
- Enhanced the desktop rewards routes to include a new endpoint for preparing GitHub star verification sessions.
- Updated the claim reward logic to validate proof URLs and GitHub session IDs before processing claims.
- Introduced new localization strings for handling proof URL inputs and GitHub session errors.
- Added tests to ensure the correct behavior of the new verification and proof handling features.

* fix(rewards): harden desktop reward validation

* chore: format skill watcher workspace test

* fix: unify low-credit prompts and byok fallback

* fix: move home budget banner below hero

* fix: avoid zero balance while rewards sync

* fix: route balance detail to environment billing

* build(desktop): stabilize Windows dev and distribution workflows (#449)

* fix(desktop): make local startup work on Windows

* fix(desktop): sanitize persisted startup logs

* test(desktop): add Feishu websocket smoke harness

* fix(smoke): improve Feishu websocket diagnostics

* build(desktop): add Windows smoke distribution flow

* build(desktop): align Windows sidecar archives with zip flow

* build(desktop): align Windows packaging with NSIS defaults

* build(desktop): refine Windows distribution commands

* build(desktop): improve Windows installer UX

Switch the NSIS flow to an assisted installer so Windows install and uninstall are visible and data cleanup can be opted into without silent surprises. Restore executable resource editing and use tombstone-based app-data cleanup so shortcuts show the Nexu icon and uninstall stays fast without flashing a console window.

* fix(desktop): stabilize Windows dist and runtime install (#538)

* build(desktop): skip exe editing for unsigned Windows dist

* fix(openclaw-runtime): stabilize Windows postinstall

Keep the full install path available for debugging while making pruned installs skip Windows-native optional dependencies that break postinstall flows.

* fix(desktop): align dev health check with runtime state

* fix(openclaw-runtime): refresh cache on installer changes

* build(desktop): add windows CI coverage

* fix(desktop): avoid sidecar copy symlink loops on windows

* fix(desktop): clean runtime plugin self-links on windows

* build(desktop): scope windows CI to build validation

* fix(desktop): restore mac dist executable discovery

---------

Co-authored-by: mRcfps <1402401442@qq.com>

* build(desktop): stabilize Windows startup and shell polish

* fix(web): refine Windows sidebar toggle layout

* fix(desktop): harden Windows packaged runtime startup

* feat: streamline desktop local dev workflow (#640)

* feat: add local dev supervisor workflow

* feat: refine local dev workflow and desktop runtime scaffolding

* docs: add dev workflow faq

* fix: remove nested controller tsx watcher

* refactor: share ensure guards across dev process helpers

* chore: remove stale task notes

* refactor: centralize local dev path resolution

* refactor: move dev orchestration into scripts/dev

Keep @nexu/dev-utils focused on atomic helpers so service-level controller and web flows stay easier to reason about and recover. Add lightweight session tracing so leaked dev processes can be correlated and cleaned up without heavy self-healing.

* refactor: clarify scripts dev module boundaries

* feat: externalize dev runtime ownership

* feat: split local dev into explicit service controls

* refactor: remove legacy desktop dev launchers

* fix: run desktop local dev through the Vite supervisor

* fix: harden local dev stack flow

* chore: sync workspace lockfiles

* fix: restore desktop dev auth session

* chore: unify scripts dev logging

* chore: stabilize launchd encapsulation and windows desktop smoke flow (#651)

* chore: introduce shared desktop lifecycle contract

* chore: move desktop platform lifecycle behind adapters

* chore: centralize desktop platform compatibility

* chore: stage patched OpenClaw runtime for local dev

* chore: log staged OpenClaw runtime usage

* chore: speed up windows desktop build iteration

* chore: disable win executable editing for local builds

* chore: ignore local build cache

* fix(desktop): support windows openclaw sidecar archives

* chore: unify desktop dev launch under scripts/dev (#654)

* chore(ci): add windows packaging to desktop dist full

* chore(ci): opt desktop dist full into node 24 actions

* Revert "chore(ci): opt desktop dist full into node 24 actions"

This reverts commit b19336e.

* fix(desktop): harden windows packaging and installer flow

* chore: remove task handoff note

* chore: align desktop tests with platform runtime changes

* chore: fix linux desktop test platform mocks

* chore: stabilize dev check service supervision

* chore: fix packaged launchd path test platform setup

* fix(desktop): preserve explicit dev runtime env overrides

* fix(dev): preserve openclaw supervisor pid lock

* chore(ci): track scripts dev changes in desktop dev checks

* chore(ci): support manual desktop workflow dispatch

* fix(dev): treat active openclaw port as running

* fix(dev): align desktop runtime ports with CI contract

* fix(dev): wait for openclaw gateway before controller start

* chore(ci): capture dev logs and read desktop pid lock

* fix(dev): wait for openclaw health before reporting ready

* fix(controller): debounce sync on all skill dir events

* fix(dev): derive openclaw status from runtime health

* fix(dev): disable bonjour in desktop CI smoke

* fix(desktop): harden Windows packaging and cleanup flows

* fix(dev): stabilize Windows desktop dev lifecycle

* fix(dev): allow overriding desktop dev ports

* fix(dev): require openclaw readiness for running status

* fix(dev): expose builtin openclaw extensions in local controller

* fix(desktop): stabilize windows packaging workflows

* fix: restore windows dev UI and openclaw startup compatibility

* fix(desktop): clear stale startup errors after runtime recovery

* fix(desktop): use packaged openclaw sidecars in windows builds

* chore: refresh lockfile for desktop dependency changes

* fix: make nexu runtime model plugin reload state by content

---------

Co-authored-by: Marc Chan <mrc@powerformer.com>
Co-authored-by: mRcfps <1402401442@qq.com>

* fix(i18n): update English and Chinese translations for rewards and balance tooltips; improve workspace layout with balance popup functionality

* feat(rewards): enhance GitHub star session handling and add token support for rate limiting; update rewards page to manage session preparation and error handling

* fix(rewards): simplify balance popup and enable github star auth token

- Drop plan/consumed rows from sidebar balance popup so only the cumulative
  reward credits row remains, matching the latest design.
- Update zh/en earned tooltip to spell out the consumption order
  (plan -> packs -> reward) and the funding sources.
- GithubStarVerificationService now reads NEXU_GITHUB_TOKEN from env to
  authenticate against api.github.com (5000 req/h) and reports rate-limit
  headers plus auth state in error messages for easier diagnosis.
- Remove the obsolete checkin total footer from the rewards page bottom strip.

* refactor(workspace-layout): enhance balance popup tooltip functionality

- Updated the balance popup to include a tooltip that appears on hover, providing additional context for earned rewards.
- Improved the styling and accessibility of the tooltip for better user experience.

* fix(i18n): update English and Chinese translations for balance popup earned credits

* feat(api): add new endpoint for retrieving desktop-local auth session and update reward sharing tasks

- Introduced the `/api/auth/get-session` endpoint to fetch the current desktop-local authentication session, including user and session details.
- Updated reward sharing tasks to replace specific platforms with a generalized "mobile_share" task, enhancing flexibility in sharing options.
- Revised English and Chinese translations for mobile sharing prompts and reward descriptions to reflect the new task structure.

* refactor(rewards): remove unused desktop cloud status and simplify QR code sharing

- Eliminated the use of desktop cloud status in the rewards page.
- Updated QR code sharing functionality to use a fixed mobile share URL instead of resolving from cloud URL.

* feat(rewards): add mobile QR code link hint and enhance mobile sharing UI

- Introduced a new hint for the mobile QR code that directs users to the Nexu GitHub repository.
- Improved the mobile sharing section's UI with enhanced styling and animations for better user experience.
- Updated English and Chinese translations to reflect the new mobile QR code link hint.

* fix(rewards): add mobile_share task id and surface silent cloud failures

The rewards UI silently failed to render the cloud balance popup
because the cloud API returned a `mobile_share` task that wasn't in
`rewardTaskIdSchema`. Zod parse failed β†’ cloud-reward-service returned
parse_error β†’ nexu-config-store fell through to the default branch
returning cloudBalance: null and tasks: []. The error was swallowed by
empty catches at every layer, so the only visible symptom was
"clicking the credits button does nothing".

- Add `mobile_share` to `rewardTaskIdSchema` enum and `rewardTasks`
  fallback array (group=social, icon=smartphone, image/weekly,
  requiresScreenshot, matching wechat/feishu/jike).
- Regenerate openapi.json + web SDK.
- Log cloud_rewards_status_{http,parse,network}_error in
  cloud-reward-service so future schema drift surfaces immediately.
- Log desktop_rewards_status_cloud_fallback in nexu-config-store when
  the non-auth_failed branch falls through, so the dropped reason is
  visible in controller logs.

* fix(rewards): wire credits popup detail button to cloud usage page

The "view usage detail" button in the sidebar credits popup was navigating
to the local /workspace/rewards page instead of the actual usage detail
page on the connected cloud profile. Open the cloud profile's
/workspace/usage URL in the system browser via openExternalUrl, mirroring
how the cloud bill links work elsewhere.

Also correct resolveCloudUsageUrl's fallback host from nexu.net to nexu.io
(nexu.net is not the production domain).

* feat(analytics): wire growth event tracking on PostHog base

Add the analytics events the activation funnel needs from the desktop
client side, on top of the PR #778 PostHog migration:

- workspace_growth_rewards_click β€” fired when the sidebar "share nexu /
  earn extra credits" growth banner is clicked.
- workspace_click_usage_detail β€” fired when the credits balance popup
  "view usage detail" button is clicked.
- user_message_sent.state β€” controller analytics service now tags each
  user_message_sent with state="Success" or state="false". Failure is
  detected by openclaw:prompt-error transcript entries arriving before
  the assistant response; success is the default and gets confirmed
  when the assistant message lands.
- AnalyticsAuthSource type now also accepts "home" so the home page
  can be the source of an auth click. The desktop side of the
  signup_success / login_success plumbing (source pass-through to
  cloud) is still pending β€” that side has to be done together with the
  cloud auth-init schema and is intentionally left out of this commit.

The user_message_sent.credit_charged property the funnel spec also
mentions cannot be implemented client-side: there is no per-message
credit consumption pipeline yet. cloud's credit_usages table is empty
of llm-call entries, link writes link.usage_events (USD cost) but
nothing transforms USD into credit and writes credit_usages, and link
does not record channel either. That work belongs to the cloud team
under the existing credit consumption track and is documented in
specs/current/credit.md.

* develop menu: set balance

* feat(auth): pass auth source from desktop to cloud through device-init

The cloud auth page (apps/web/src/pages/auth.tsx in nexu-io/cloud) is
already firing signup_success / login_success with the right method
field, but it sources `source` from a `?source=...` query string and
falls back to "welcome_page" otherwise. The desktop side never set
that query string, so every auth event from the desktop client showed
up as welcome_page regardless of where the user actually clicked.

This wires the desktop side end-to-end so the cloud event gets the
real source:

- packages/shared: cloudConnectBodySchema (new) and
  cloudProfileConnectBodySchema both accept an optional `source`
  string.
- controller (nexu-config-store / desktop-local-service / route):
  connect endpoints accept `source` in the request body and append it
  as `&source=…` to the cloud /auth browser URL.
- apps/web hook (use-cloud-connect): handleCloudConnect accepts an
  optional AnalyticsAuthSource parameter and forwards it.
- callers tagged with the page they fire from:
  - welcome.tsx β†’ "welcome_page" (both initial connect and the
    retry-after-error path).
  - workspace-layout.tsx sidebar growth-card β†’ derived from current
    route via existing isHomePage / isModelsPage flags
    (home / settings / fallback home).
  - rewards.tsx (task action and login CTA) β†’ "home", since the
    rewards page is reached from home and the funnel spec only lists
    welcome_page / settings / home as valid sources.

Cloud-side counterpart (still pending, separate PR in nexu-io/cloud):
add "home" to the validSources allowlist in apps/web/src/pages/auth.tsx
so the postApiV1MeAuthSource backfill accepts the new value.

* fix(rewards): resolve CI failures and address Codex review feedback

- rewards.tsx: guard i18n.language access with optional chaining to prevent
  SSR/test crash when i18n instance is not fully initialized
- models.tsx: treat 'Connection attempt already in progress' as pending and
  keep polling instead of dropping user into error state (Codex P2)
- controller-ready.ts: give final recovery attempt a bounded
  finalAttemptTimeoutMs (default 4x attemptTimeoutMs) so crash-looping
  controller eventually surfaces as false instead of leaking polling loops
  (Codex P2)
- tests: align reward-share-assets / home locale / controller-ready tests
  with consolidated mobile_share task and new docs URL format

* fix(desktop): stabilize packaged webview startup

* fix(ci): assert web surface origin instead of /workspace pathname

Verify packaged runtime unit health used to require that some embedded
webview have lastUrl.includes("/workspace"), but that asserts product
state ("the user has reached the workspace route") rather than runtime
health.

In fresh-state CI runs the renderer mounts /workspace, then the SPA's
AuthLayout (apps/web/src/layouts/auth-layout.tsx:14-16) and
WorkspaceLayout (apps/web/src/layouts/workspace-layout.tsx:346-349)
immediately Navigate('/') because there is no auth session and
SETUP_COMPLETE_KEY is unset. The desktop diagnostics reporter records
contents.getURL() at did-finish-load time
(apps/desktop/main/index.ts:1035-1056), so it captures the
post-redirect URL β€” webview ends up with lastUrl="http://127.0.0.1:50810/"
and the old check reported "workspace webview diagnostics are missing".

Local runs accidentally passed because the developer machine had a
persisted Local Storage with nexu_setup_complete=1 from earlier sessions,
which suppressed the redirect. CI is ephemeral and never had it.

The actual runtime health invariant is that an embedded webview
successfully loaded a page from the local web sidecar's origin β€” the
embed handshake worked, the renderer is alive, and there was no
fail-load. The path under that origin (/, /workspace, /welcome, ...)
is product state, not health.

- Add webOrigin to readinessUrls (parsed from the web URL).
- Replace getLatestWorkspaceWebview with getLatestWebSurfaceWebview that
  filters embedded entries by same-origin against webOrigin and ignores
  pathname entirely.
- Thread webOrigin through diagnosticsChecksPassed,
  collectDiagnosticsFailures, probesPassed, verifyRuntime.
- Failure message now reads "web surface webview diagnostics are
  missing (no embedded webview reported origin <origin>)".

Verified locally with two fixtures:
- {id:3, lastUrl:"http://127.0.0.1:50810/"}      β†’ matches (used to miss)
- {id:3, lastUrl:"http://127.0.0.1:50810/workspace"} β†’ still matches

* fix(desktop): show NexuLoader instead of error card while controller is starting (#893)

On relaunch, users saw a white card titled "Starting controller..." with
"Retry controller" and "Open control plane" buttons during the short
window where the local controller was booting but not yet ready. It
looked like the app had landed on an error page instead of the home
page, which is what issue #876 reported.

The only difference between the "polling" and "failed" states in the
old fallback UI was the heading text β€” both rendered the same card and
the same Retry button, so users could not tell "still starting" from
"actually broken".

Split the fallback into three branches:
- desktopWebUrl ready -> SurfaceFrame with the webview (unchanged)
- controller failed / recovering -> the original error card with Retry
- normal polling -> SurfaceFrame with src=null, which reuses the built-in
  NexuLoader overlay that SurfaceFrame already shows before webview
  did-finish-load

The polling loader and the post-mount webview loader are now the same
NexuLoader component with the same animation, so the transition from
"controller starting" to "home page" is visually seamless and never
exposes any text or buttons that could be mistaken for an error page.

Closes #876

* refresh after set balance

* perf(desktop): unblock event loop during packaged sidecar extraction

The async openclaw sidecar materializer was named "Async" and wrapped
in async/await, but it called execFileSync("/usr/bin/tar", …) and
rmSync(…) internally. execFileSync blocks the Node.js event loop until
the child process exits, so for the ~14s tar extraction on first
install / post-update the main process could not respond to anything,
including IPC handshake from the renderer's preload. As a result the
renderer's loadFile took ~28s to fire did-finish-load (measured locally:
12:13:42.805 dispatched β†’ 12:14:10.463 finished, while extraction ran
12:13:42 β†’ 12:13:56), and the setup-animation video did not start
playing until extraction was complete β€” defeating the entire point of
showing the window with a white background "before loadFile, before
React, before anything" (apps/desktop/main/index.ts:945-955).

Switch the tar invocation to promisify(execFile), which still spawns a
real /usr/bin/tar child process but waits for it via libuv asynchronously
so the event loop stays responsive. Also replace the matching rmSync of
the previous extracted sidecar root with the async fs/promises rm to
keep the function uniformly non-blocking.

Sync materializer (createSyncTarSidecarMaterializer) is left untouched
because it is intentionally synchronous for early-bootstrap call sites.

Effect: renderer mounts and the setup-animation video starts playing
within seconds of window.show(), in parallel with extraction, instead
of after extraction finishes.

* fix(desktop): refresh packaged runtime on build changes

Avoid reusing stale extracted runtime sidecars when a rebuilt app keeps the same app version but changes CFBundleVersion, and keep the desktop rewards balance popup reachable while cloud balance data is still loading.

* fix(desktop): tear down stale runtime before replace

When packaged runtime identity changes, boot out launchd services and clear surviving Nexu processes before replacing the extracted runner and controller sidecar so reinstall flows cannot keep stale runtime code alive.

* fix(desktop): sign openclaw native sidecar binaries

Sign all packaged OpenClaw native binary candidates directly during mac builds and invalidate stale archived sidecar payloads so notarization cannot fail on unsigned canvas/skia artifacts.

* fix(rewards): address remaining PR review feedback

Recover stale desktop cloud connect sessions, validate reward group IDs and profile renames more defensively, and route desktop test balance operations through the host bridge instead of renderer-origin fetches.

* test(desktop): make webview preload URL test portable

Build the expected file URL with Node path/url helpers so the desktop preload URL assertion passes on Windows as well as Unix runners.

---------

Co-authored-by: PerishFire <39043006+PerishCode@users.noreply.github.com>
Co-authored-by: Marc Chan <mrc@powerformer.com>
Co-authored-by: mRcfps <1402401442@qq.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: nettee <nettee.liu@gmail.com>
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.

1 participant