feat(desktop): setup animation on first install and post-update#596
feat(desktop): setup animation on first install and post-update#596
Conversation
…ate extraction Defer the synchronous openclaw sidecar extraction so the window can appear immediately. When extraction is needed (first install or update), a full-screen video overlay plays once, then fades out to the existing four-color NexuLoader which stays visible until the cold start completes. - Split ensurePackagedOpenclawSidecar into resolvePackagedOpenclawSidecar (sync path check) + extractOpenclawSidecarAsync (async extraction with retries) - createRuntimeUnitManifests no longer blocks on tar extraction - Add needsSetupAnimation to HostBootstrap, forwarded via env var to preload - Add setup:complete HostDesktopCommand type for future use - Compressed design video from 33 MB to 1.4 MB (720p CRF 26)
📝 WalkthroughWalkthroughThis PR introduces a setup animation feature triggered on first application startup. It adds detection logic to determine if openclaw sidecar extraction is needed, conditionally performs async extraction during cold-start, exposes setup state through the IPC bridge, and renders a full-screen setup video overlay before transitioning to the main application UI. Changes
Sequence Diagram(s)sequenceDiagram
participant Main as Main Process
participant Preload
participant Renderer
participant Host as Host Bridge
Main->>Main: Check if sidecar<br/>extraction needed
alt Extraction Needed
Main->>Main: Set NEXU_NEEDS_SETUP_<br/>ANIMATION=1
Main->>Main: Async extract openclaw<br/>sidecar with retries
end
Main->>Preload: Create main window<br/>(env var set)
Preload->>Preload: Read NEXU_NEEDS_SETUP_<br/>ANIMATION from process.env
Preload->>Host: Expose needsSetupAnimation<br/>via contextBridge
Preload->>Renderer: Provide hostBridge with<br/>bootstrap.needsSetupAnimation
Renderer->>Renderer: Initialize setupPhase state<br/>based on needsSetupAnimation
alt needsSetupAnimation === true
Renderer->>Renderer: Render full-screen overlay<br/>with setup-animation.mp4
Renderer->>Renderer: Play video, fade on end,<br/>set phase to "done"
Renderer->>Host: Send setup:complete<br/>command
Host->>Renderer: Command acknowledged<br/>(short-circuit in handler)
end
Renderer->>Renderer: Show main UI when<br/>setupPhase === "done"
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/desktop/main/runtime/manifests.ts (1)
239-258: Async retry delay usessetTimeoutinstead of shellsleep– good improvement over sync version.The async extraction correctly uses
setTimeoutfor the retry delay (Line 256), which is an improvement over the deprecated sync version that shells out tosleep(Line 293). However, Line 243 and Line 246 still invokermandtarvia PATH lookup.While these are standard POSIX utilities (not npm/pnpm/npx), the coding guideline states to "never use any shell command that relies on the user's PATH." Consider whether these utilities could be unavailable or aliased in edge cases on macOS.
💡 Alternative: Use Node.js fs APIs for rm
- if (existsSync(extractedSidecarRoot)) { - await execFileAsync("rm", ["-rf", extractedSidecarRoot]); - } + if (existsSync(extractedSidecarRoot)) { + const { rm } = await import("node:fs/promises"); + await rm(extractedSidecarRoot, { recursive: true, force: true }); + }For
tar, there's no pure-Node equivalent without adding a dependency, so continuing to use the systemtaris reasonable.As per coding guidelines: "Desktop packaged app: never use
npx,npm,pnpm, or any shell command that relies on the user's PATH."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/runtime/manifests.ts` around lines 239 - 258, Replace the PATH-dependent "rm" exec with the Node fs API: instead of execFileAsync("rm", ["-rf", extractedSidecarRoot]) in the retry loop, use fs.rm or fs.promises.rm (or fs.rmSync) with { recursive: true, force: true } to remove extractedSidecarRoot; keep using execFileAsync("tar", ...) for archive extraction since there's no pure-Node tar in scope. Update imports/usages accordingly and preserve the surrounding logic that checks existsSync(extractedSidecarRoot), mkdirSync(extractedSidecarRoot, { recursive: true }), writeFileSync(stampPath, archiveStamp), and the MAX_RETRIES retry behavior.apps/desktop/main/index.ts (1)
30-33: Synchronous extraction inlaunchd-bootstrap.tsshould be tracked for future migration.
launchd-bootstrap.tsat line 885 still usesensurePackagedOpenclawSidecar, the synchronous extraction method. While this is intentional for backward compatibility, consider adding a TODO comment or opening a tracking issue to migrate this to the async pattern (extractOpenclawSidecarAsync) in a future PR.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/index.ts` around lines 30 - 33, The launchd-bootstrap usage of the synchronous extraction function ensurePackagedOpenclawSidecar should be annotated for future migration to the async API extractOpenclawSidecarAsync: add a clear TODO comment next to the call to ensurePackagedOpenclawSidecar indicating this is intentionally synchronous for backward compatibility and reference extractOpenclawSidecarAsync as the target migration, and optionally create/open a tracking issue ID in the comment so maintainers can follow up in a later PR.
🤖 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/src/main.tsx`:
- Around line 1208-1219: The video element using src={setupVideoUrl} lacks error
handling so a load failure will never trigger onEnded; add an onError handler on
the same video element (next to onEnded) that calls setSetupPhase("fading") (and
optionally logs the error or sends telemetry) to gracefully skip the animation
when the video cannot load; ensure the handler references the existing
setupVideoUrl and setSetupPhase symbols so behavior matches the successful end
path.
---
Nitpick comments:
In `@apps/desktop/main/index.ts`:
- Around line 30-33: The launchd-bootstrap usage of the synchronous extraction
function ensurePackagedOpenclawSidecar should be annotated for future migration
to the async API extractOpenclawSidecarAsync: add a clear TODO comment next to
the call to ensurePackagedOpenclawSidecar indicating this is intentionally
synchronous for backward compatibility and reference extractOpenclawSidecarAsync
as the target migration, and optionally create/open a tracking issue ID in the
comment so maintainers can follow up in a later PR.
In `@apps/desktop/main/runtime/manifests.ts`:
- Around line 239-258: Replace the PATH-dependent "rm" exec with the Node fs
API: instead of execFileAsync("rm", ["-rf", extractedSidecarRoot]) in the retry
loop, use fs.rm or fs.promises.rm (or fs.rmSync) with { recursive: true, force:
true } to remove extractedSidecarRoot; keep using execFileAsync("tar", ...) for
archive extraction since there's no pure-Node tar in scope. Update
imports/usages accordingly and preserve the surrounding logic that checks
existsSync(extractedSidecarRoot), mkdirSync(extractedSidecarRoot, { recursive:
true }), writeFileSync(stampPath, archiveStamp), and the MAX_RETRIES retry
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3573a7a5-f3b2-4ff5-b93c-bf0eb81c1ea9
⛔ Files ignored due to path filters (1)
apps/desktop/assets/setup-animation.mp4is excluded by!**/*.mp4
📒 Files selected for processing (7)
apps/desktop/main/index.tsapps/desktop/main/runtime/manifests.tsapps/desktop/preload/index.tsapps/desktop/preload/webview-preload.tsapps/desktop/shared/host.tsapps/desktop/src/components/desktop-shell.tsxapps/desktop/src/main.tsx
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 39429f6bfa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
# Conflicts: # apps/desktop/main/runtime/manifests.ts
…ded after main merge
Deploying nexu-docs with
|
| Latest commit: |
0ae134c
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://499cf501.nexu-docs.pages.dev |
| Branch Preview URL: | https://feat-setup-animation.nexu-docs.pages.dev |
- Swap progress bar and percent order (bar on top, percent right-aligned below) - Replace "Later"/"Dismiss" buttons with "Changelog" linking to GitHub releases - Add "Retry" button to error state - Update progress fill color to #1c1f23
Switch to "downloading" phase with 0% as soon as the user clicks Download, instead of waiting for the first update:progress event from electron-updater. This eliminates the perceived lag where the button appears unresponsive.
Replace single-play animation with two-phase design from design team: 1. Main video (23s, 720p, 1.1MB) plays once on first install / post-update 2. If cold-start isn't done when main video ends, seamlessly switch to short loop video (4s, 720p, 45KB) that repeats until ready 3. When getRuntimeConfig() resolves (cold-start done), fade out overlay Phase flow: playing → looping → fading → done - If cold-start finishes during main video: playing → fading → done (no loop) - If cold-start takes longer: playing → looping → fading → done
- Show window as soon as webContents starts loading (did-start-loading) instead of waiting for ready-to-show, so the user sees the white background → animation ASAP instead of staring at a bouncing dock icon - Set white background (#ffffff) during setup to match animation overlay - Defer vibrancy/transparent background until animation completes - Add setup:animation-complete IPC: renderer notifies main process when animation overlay is removed, main process restores vibrancy
Root cause: createRuntimeUnitManifests() called ensurePackagedOpenclawSidecar() synchronously at module-level (before app.whenReady), which does a blocking tar extraction of ~250MB on first install. Fix: - Add resolveOpenclawSidecarRoot() that returns the extraction target path without actually extracting — used by manifest creation for path setup - Actual extraction stays in extractOpenclawSidecarAsync() during cold start - Show window BEFORE loadFile (not on did-start-loading which was too late) - White background matches animation overlay for seamless transition - Restore vibrancy via setup:animation-complete IPC after animation ends
# Conflicts: # apps/desktop/src/components/update-banner.tsx # apps/desktop/src/main.tsx
Previously: 60s initial delay + 4h interval. Now: immediate check on cold-start completion + 15m interval.
When launchd has stale state for a service label (e.g. after repeated bootout/bootstrap during port conflict recovery), bootstrap fails with 'Input/output error (code 5)'. Now detects this error, bootout to clear the stale registration, waits 1s, and retries once.
# Conflicts: # .gitignore
What
Play a branded setup animation video during first install and post-update openclaw sidecar extraction.
Why
Users see a blank screen or frozen window during the initial tar extraction + cold start on first launch or after an update. A setup animation gives clear visual feedback that the app is bootstrapping.
How
ensurePackagedOpenclawSidecarintoresolvePackagedOpenclawSidecar(fast sync path check) +extractOpenclawSidecarAsync(async extraction with retries).createRuntimeUnitManifestsno longer blocks on tar extraction — the window can appear immediately.checkOpenclawExtractionNeeded()compares the.archive-stampfile (storedsize:mtimeMsofpayload.tar.gz) against the current archive. Stamp mismatch → first install or update → show animation.needsSetupAnimationis passed via env varNEXU_NEEDS_SETUP_ANIMATIONto the preload, exposed inHostBootstrap. The renderer shows a full-screen video overlay on mount when the flag is set.onEndedtriggers fade-out (0.6s) → existing four-color NexuLoader takes over until cold start completes.Affected areas
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpassespnpm generate-typesrun (if API routes/schemas changed)anytypes introduced (useunknownwith narrowing)Notes for reviewers
setup:completecommand type inHostDesktopCommandis added but currently unused — the video dismisses itself viaonEnded. Kept for potential future use (e.g., aborting the animation early if cold start finishes before the video ends).ensurePackagedOpenclawSidecaris kept for backward compatibility withlaunchd-bootstrap.tswhich still calls it directly.Summary by CodeRabbit