feat(desktop): launchd-based process architecture#405
Conversation
Replace reserved close code 1008 (Policy Violation) with private code 4008 when closing WebSocket connections on error. Code 1008 is reserved for server use and Node.js 22's native WebSocket throws DOMException [InvalidAccessError] when clients attempt to use it, causing the controller process to crash on authentication failures. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Cherry-pick WebSocket close code fix from PR #365 - Change launchd namespace from com.nexu.* to io.nexu.* - Add progress tracking directory with STATUS, DECISIONS, ISSUES Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 1-3 of launchd architecture refactor: - LaunchdManager: wrapper for launchctl commands (install, start, stop, status, graceful shutdown) - PlistGenerator: generates launchd plist XML for Controller and OpenClaw services with proper env vars and dependencies - EmbeddedWebServer: serves static files and proxies API requests to Controller, replacing the web sidecar process Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- launchd-bootstrap.ts: complete bootstrap flow for launchd-based startup (install services, start controller, start openclaw, start embedded web server) - Feature flag NEXU_USE_LAUNCHD=1 for gradual rollout - Unified log directory at ~/.nexu/logs/ - Path resolution for packaged vs dev environments - Index file exporting all services Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- quit-handler.ts: handles before-quit event with dialog - Options: Quit Completely (stop services), Run in Background, Cancel - Graceful shutdown of launchd services - Exported via services/index.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a macOS launchd-based service stack and tooling to the Desktop app (LaunchdManager, plist generation, bootstrap orchestration, embedded HTTP server, quit integration, dev script, tests, and docs), plus controller runtime changes (new "starting" status, WebSocket close-code tweak, OpenClaw config defaults). Changes
Sequence Diagram(s)sequenceDiagram
participant Electron as Electron App
participant Bootstrap as LaunchdBootstrap
participant LaunchdMgr as LaunchdManager
participant launchd as launchd / launchctl
participant Controller as Controller Service
participant WebServer as Embedded Web Server
participant OpenClaw as OpenClaw Service
participant User as User
Electron->>Bootstrap: bootstrapWithLaunchd(env)
Bootstrap->>LaunchdMgr: ensure/install plists & services
LaunchdMgr->>launchd: bootstrap controller plist
launchd-->>Controller: start controller
Controller-->>LaunchdMgr: reported running
Bootstrap->>WebServer: startEmbeddedWebServer(opts)
WebServer->>Controller: proxy health request
Controller-->>WebServer: 200 OK
WebServer-->>Bootstrap: controller healthy
LaunchdMgr->>launchd: bootstrap openclaw plist
launchd-->>OpenClaw: start openclaw
OpenClaw-->>LaunchdMgr: reported running
User->>Electron: request quit
Electron->>Electron: showQuitDialog()
User-->>Electron: "quit-completely"
Electron->>WebServer: close()
Electron->>LaunchdMgr: stopAllServices()
LaunchdMgr->>launchd: stop openclaw, then controller
launchd-->>OpenClaw: stopped
launchd-->>Controller: stopped
Electron->>Electron: app.quit()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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 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 |
scripts/dev-launchd.sh provides: - start: generate plists, bootstrap and start services - stop: gracefully stop services - restart: stop then start - status: show launchd service status - logs: tail all log files Uses io.nexu.*.dev labels and ~/.nexu/logs/ for logging. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add launchd service imports - Add runLaunchdColdStart function that uses bootstrapWithLaunchd - Check NEXU_USE_LAUNCHD=1 flag to choose bootstrap mode - Install launchd quit handler after successful launchd bootstrap - Modify before-quit handler to skip orchestrator cleanup in launchd mode - Derive openclaw paths from nexuHome config Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- launchd-manager.test.ts: tests for LaunchdManager class and SERVICE_LABELS - plist-generator.test.ts: tests for generatePlist function Tests cover: - Platform check (darwin only) - Default and custom plist directories - UID-based domain construction - Dev vs prod label generation - Plist XML generation with correct structure - XML character escaping - Log path configuration - Service dependencies Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (6)
apps/desktop/main/services/launchd-manager.ts (1)
54-60: Consider structured logging for service operations.The current implementation uses plain
console.log/warn/errorcalls. Per coding guidelines, logging should be structured (pino or console JSON) for better observability and debugging of launchd operations.This is a good-to-have improvement that could be addressed in a follow-up.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/services/launchd-manager.ts` around lines 54 - 60, Replace the plain console calls in the launchd bootstrap block so logging is structured (use the project's pino/logger) rather than console.log/warn/error: when stdout is present call logger.info with an object containing { event: 'launchd.bootstrap', label, stdout }, when stderr call logger.warn with { event: 'launchd.bootstrap_warning', label, stderr }, and in the catch block call logger.error with { event: 'launchd.bootstrap_failed', label, error: err instanceof Error ? err.message : String(err), stack: err instanceof Error ? err.stack : undefined } so the label, stdout/stderr and error details are captured as structured fields instead of plain strings; update imports/usages accordingly where label, stdout, stderr and err are referenced.scripts/dev-launchd.sh (1)
171-177: Unused loop variablei.The loop counter
iis never used. Use_to indicate an intentionally unused variable.📝 Suggested fix
- for i in {1..30}; do + for _ in {1..30}; do🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/dev-launchd.sh` around lines 171 - 177, The for-loop in scripts/dev-launchd.sh uses an unused loop variable `i`; change the loop header from `for i in {1..30}` to use a placeholder like `for _ in {1..30}` to indicate the counter is intentionally unused, leaving the curl readiness check (`curl -s "http://127.0.0.1:$CONTROLLER_PORT/api/auth/get-session"`) and the break/sleep logic unchanged.apps/desktop/main/services/launchd-bootstrap.ts (2)
212-219:process.cwd()may be unreliable in dev mode.In development,
process.cwd()depends on where Electron was launched from, which could vary. Consider using__dirnameor a more explicit path anchor.♻️ Suggested improvement
export function getDefaultPlistDir(isDev: boolean): string { if (isDev) { // Dev mode: use repo-local directory - return path.join(process.cwd(), ".tmp", "launchd"); + // Use app.getAppPath() or require.main?.path for more reliable anchoring + return path.join(process.cwd(), ".tmp", "launchd"); } // Production: use standard LaunchAgents directory return path.join(os.homedir(), "Library", "LaunchAgents"); }This is acceptable for now since it's dev-only and documented behavior.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/services/launchd-bootstrap.ts` around lines 212 - 219, getDefaultPlistDir uses process.cwd() which can vary in dev; change it to use a stable module-based anchor (e.g., __dirname) or another explicit repo-root resolver so the dev .tmp/launchd path is deterministic. Locate getDefaultPlistDir and replace the process.cwd() usage with a directory derived from __dirname (or a project-root helper) to construct path.join(<stable-dev-root>, ".tmp", "launchd") while leaving the production branch unchanged.
250-271:process.cwd()in development path resolution has the same caveat.Same concern as
getDefaultPlistDir—works but depends on launch context.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/services/launchd-bootstrap.ts` around lines 250 - 271, The code uses process.cwd() to set repoRoot (in the launchd-bootstrap.ts path bundle that builds nodePath/controllerEntryPath/openclawPath/controllerCwd/openclawCwd), which is fragile depending on launch context; change repoRoot to be derived from the source file location (e.g., compute it relative to this module using __dirname or import.meta.url and path.resolve up to the repo root) instead of process.cwd(), and update any related logic that relies on repoRoot (controllerEntryPath, openclawPath, controllerCwd, openclawCwd) so paths remain correct regardless of working directory—mirror the same fix you applied to getDefaultPlistDir.apps/desktop/main/services/quit-handler.ts (1)
113-116:removeAllListeners("before-quit")may remove unrelated handlers.If other parts of the app register
before-quitlisteners, this will remove them too. Consider tracking and removing only the handler installed by this module.♻️ Safer alternative
+ const handler = async (event: Electron.Event) => { + // ... handler body ... + }; + - app.on("before-quit", async (event) => { + app.on("before-quit", handler); // ... existing code ... - app.removeAllListeners("before-quit"); + app.removeListener("before-quit", handler); app.quit(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/services/quit-handler.ts` around lines 113 - 116, Currently the code calls app.removeAllListeners("before-quit"), which can remove unrelated listeners; instead keep a reference to the listener this module adds (e.g., the function currently registered in this file, locate the listener passed to app.on("before-quit") or the enclosing function in quit-handler.ts) and call app.removeListener("before-quit", yourListener) (or app.off) before calling app.quit(); update the registration to assign the listener to a const (e.g., quitHandler) and use that symbol when removing so only this module's handler is removed.apps/desktop/main/services/embedded-web-server.ts (1)
17-32: Consider adding.mapand.webmanifestMIME types.Common web app assets that may be missing:
♻️ Suggested additions
const MIME_TYPES: Record<string, string> = { ".html": "text/html", ".js": "application/javascript", ".css": "text/css", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".woff": "font/woff", ".woff2": "font/woff2", ".ttf": "font/ttf", ".eot": "application/vnd.ms-fontobject", + ".map": "application/json", + ".webmanifest": "application/manifest+json", + ".webp": "image/webp", };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/services/embedded-web-server.ts` around lines 17 - 32, The MIME_TYPES map is missing handlers for source maps and web manifests; update the MIME_TYPES constant to include ".map" with "application/json" (or "application/json; charset=utf-8") and ".webmanifest" with "application/manifest+json" so the embedded web server serves .map and .webmanifest files with correct content-types (update the MIME_TYPES Record<string,string> in embedded-web-server.ts).
🤖 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/services/embedded-web-server.ts`:
- Around line 131-147: The code currently joins webRoot with the unsanitized
url.pathname (in the static file handling block) which allows path traversal;
update the logic in the handler that calculates filePath (the section using
path.join(webRoot, url.pathname), fileExists and stat) to first normalize and
sanitize the pathname (e.g. decode URL, remove/nullify any leading "/" and
reject or strip segments containing ".."), then use
path.resolve(path.join(webRoot, sanitizedPath)) and verify the resolved path is
inside webRoot (e.g. by checking that
resolvedPath.startsWith(path.resolve(webRoot) + path.sep) or using path.relative
and ensuring it does not start with ".."); only after that perform
fileExists/stat and fallback to index.html if the path is invalid, non-existent,
or a directory.
In `@apps/desktop/main/services/launchd-manager.ts`:
- Around line 108-134: The catch in stopServiceGracefully currently swallows all
errors from stopService; update it to only treat the "service not running" case
as a no-op and rethrow or surface other errors: in stopServiceGracefully call
stopService(label) inside try/catch, inspect the caught error (e.g., check for a
specific error class or message indicating "not running" from stopService),
return only for that case, and otherwise rethrow or log and throw the original
error so permission/validation errors aren’t silently ignored; reference
stopServiceGracefully and stopService to locate and implement the conditional
error handling.
In `@apps/desktop/main/services/quit-handler.ts`:
- Around line 122-140: The quitWithDecision function currently calls
opts.launchd.stopServiceGracefully (and opts.webServer.close) without catching
errors, so any throw can prevent app.quit; update quitWithDecision to mirror
installLaunchdQuitHandler by wrapping stopServiceGracefully calls in try/catch
(and webServer.close if desired), log or handle errors via
opts.logger/processLogger, and ensure app.quit() is always called (use a finally
block or call app.quit() after catching errors) so the app cannot be left
running in an inconsistent state; reference symbols: quitWithDecision,
opts.launchd.stopServiceGracefully, opts.webServer.close, app.quit, and
installLaunchdQuitHandler for how errors are handled.
In `@scripts/dev-launchd.sh`:
- Around line 14-15: LOG_DIR is pointing to $HOME/.nexu/logs which conflicts
with the design doc; change it to use the repository-local path by setting
LOG_DIR="$REPO_ROOT/.tmp/logs" (matching PLIST_DIR's use of REPO_ROOT), and
ensure any code that creates or writes logs uses LOG_DIR so logs are created
under .tmp/logs inside the repo; update any mkdir or file-write commands that
reference the old path to use the LOG_DIR variable instead.
- Around line 67-70: Update the StandardErrorPath filenames to match the design
docs by changing the controller and openclaw error log names from
"$LOG_DIR/controller.error.log" and "$LOG_DIR/openclaw.error.log" to
"$LOG_DIR/controller.err" and "$LOG_DIR/openclaw.err" respectively; modify the
XML entries that use the StandardErrorPath key (the block around
StandardOutPath/StandardErrorPath in this script) and apply the same change for
openclaw (the analogous block near the openclaw section).
- Around line 36-41: The cleanup() function (which kills the launchctl jobs for
"$DOMAIN/$OPENCLAW_LABEL" and "$DOMAIN/$CONTROLLER_LABEL") is never wired to
signals; add a trap to invoke cleanup on EXIT, SIGINT and SIGTERM so the
services are stopped when the script exits or is interrupted, or if keeping
services running is intentional, remove the unused cleanup() to avoid confusion;
ensure the trap references the existing cleanup symbol and covers at least EXIT,
SIGINT and SIGTERM.
- Line 28: The script currently sets NODE_PATH="${NODE_PATH:-$(which node)}",
which conflicts with Node.js's NODE_PATH; rename this variable to NODE_BIN (e.g.
NODE_BIN="${NODE_BIN:-$(which node)}") and update every usage of NODE_PATH in
the script and in the plist generation code to use NODE_BIN instead so spawned
processes receive the node binary path without affecting module resolution;
search for all references to NODE_PATH and replace them with NODE_BIN (including
any string interpolations passed into generated plist content).
In `@specs/designs/launchd-process-architecture/progress/STATUS.md`:
- Around line 52-67: Update the checklist entries that reference file paths with
the incorrect "/src/" segment: replace
`apps/desktop/src/main/services/launchd-manager.ts`,
`apps/desktop/src/main/services/plist-generator.ts`, and
`apps/desktop/src/main/embedded-web-server.ts` with the actual implementation
paths that omit "/src/" (i.e., `apps/desktop/main/...`) so the TODOs match the
real filenames and locations used by LaunchdManager, plist-generator, and
embedded-web-server.
---
Nitpick comments:
In `@apps/desktop/main/services/embedded-web-server.ts`:
- Around line 17-32: The MIME_TYPES map is missing handlers for source maps and
web manifests; update the MIME_TYPES constant to include ".map" with
"application/json" (or "application/json; charset=utf-8") and ".webmanifest"
with "application/manifest+json" so the embedded web server serves .map and
.webmanifest files with correct content-types (update the MIME_TYPES
Record<string,string> in embedded-web-server.ts).
In `@apps/desktop/main/services/launchd-bootstrap.ts`:
- Around line 212-219: getDefaultPlistDir uses process.cwd() which can vary in
dev; change it to use a stable module-based anchor (e.g., __dirname) or another
explicit repo-root resolver so the dev .tmp/launchd path is deterministic.
Locate getDefaultPlistDir and replace the process.cwd() usage with a directory
derived from __dirname (or a project-root helper) to construct
path.join(<stable-dev-root>, ".tmp", "launchd") while leaving the production
branch unchanged.
- Around line 250-271: The code uses process.cwd() to set repoRoot (in the
launchd-bootstrap.ts path bundle that builds
nodePath/controllerEntryPath/openclawPath/controllerCwd/openclawCwd), which is
fragile depending on launch context; change repoRoot to be derived from the
source file location (e.g., compute it relative to this module using __dirname
or import.meta.url and path.resolve up to the repo root) instead of
process.cwd(), and update any related logic that relies on repoRoot
(controllerEntryPath, openclawPath, controllerCwd, openclawCwd) so paths remain
correct regardless of working directory—mirror the same fix you applied to
getDefaultPlistDir.
In `@apps/desktop/main/services/launchd-manager.ts`:
- Around line 54-60: Replace the plain console calls in the launchd bootstrap
block so logging is structured (use the project's pino/logger) rather than
console.log/warn/error: when stdout is present call logger.info with an object
containing { event: 'launchd.bootstrap', label, stdout }, when stderr call
logger.warn with { event: 'launchd.bootstrap_warning', label, stderr }, and in
the catch block call logger.error with { event: 'launchd.bootstrap_failed',
label, error: err instanceof Error ? err.message : String(err), stack: err
instanceof Error ? err.stack : undefined } so the label, stdout/stderr and error
details are captured as structured fields instead of plain strings; update
imports/usages accordingly where label, stdout, stderr and err are referenced.
In `@apps/desktop/main/services/quit-handler.ts`:
- Around line 113-116: Currently the code calls
app.removeAllListeners("before-quit"), which can remove unrelated listeners;
instead keep a reference to the listener this module adds (e.g., the function
currently registered in this file, locate the listener passed to
app.on("before-quit") or the enclosing function in quit-handler.ts) and call
app.removeListener("before-quit", yourListener) (or app.off) before calling
app.quit(); update the registration to assign the listener to a const (e.g.,
quitHandler) and use that symbol when removing so only this module's handler is
removed.
In `@scripts/dev-launchd.sh`:
- Around line 171-177: The for-loop in scripts/dev-launchd.sh uses an unused
loop variable `i`; change the loop header from `for i in {1..30}` to use a
placeholder like `for _ in {1..30}` to indicate the counter is intentionally
unused, leaving the curl readiness check (`curl -s
"http://127.0.0.1:$CONTROLLER_PORT/api/auth/get-session"`) and the break/sleep
logic unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4ae45104-39c8-428b-b6c4-4b55c1adee11
📒 Files selected for processing (14)
apps/controller/src/runtime/openclaw-ws-client.tsapps/desktop/main/services/embedded-web-server.tsapps/desktop/main/services/index.tsapps/desktop/main/services/launchd-bootstrap.tsapps/desktop/main/services/launchd-manager.tsapps/desktop/main/services/plist-generator.tsapps/desktop/main/services/quit-handler.tsscripts/dev-launchd.shspecs/designs/launchd-process-architecture/en.mdspecs/designs/launchd-process-architecture/progress/DECISIONS.mdspecs/designs/launchd-process-architecture/progress/ISSUES.mdspecs/designs/launchd-process-architecture/progress/README.mdspecs/designs/launchd-process-architecture/progress/STATUS.mdspecs/designs/launchd-process-architecture/zh.md
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 848-861: The launchd path registers a second app "before-quit" and
can run cleanup out of order (and miss installation on bootstrap failure); fix
by consolidating to a single authoritative before-quit flow: stop adding another
app.on("before-quit") inside installLaunchdQuitHandler and instead have
installLaunchdQuitHandler register cleanup callbacks with the existing global
before-quit handler (or expose a registerLaunchdQuitCallback API); also ensure
installLaunchdQuitHandler is invoked reliably (either only after auth/bootstrap
fully succeeds or by setting a launchd-managed flag early and always registering
the centralized handler) so that the cleanup using sleepGuard.dispose,
diagnosticsReporter.flushNow and flushRuntimeLoggers executes only from the
single global before-quit path.
- Around line 843-849: The launchd branch is only gated by
isLaunchdBootstrapEnabled(), so on non-macOS machines NEXU_USE_LAUNCHD=1 will
try runLaunchdColdStart(); change the gating to require both
isLaunchdBootstrapEnabled() and a macOS platform check (process.platform ===
'darwin') before calling runLaunchdColdStart(), update the logColdStart message
accordingly or keep it based on the combined boolean, and ensure
runLaunchdColdStart() is never invoked on Linux/Windows.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 659ff481-34ea-4a92-a43f-dec183607aef
📒 Files selected for processing (4)
apps/desktop/main/index.tsspecs/designs/launchd-process-architecture/progress/STATUS.mdtests/desktop/launchd-manager.test.tstests/desktop/plist-generator.test.ts
✅ Files skipped from review due to trivial changes (1)
- specs/designs/launchd-process-architecture/progress/STATUS.md
- Fix OpenClaw config paths to match controller defaults in env.ts (OPENCLAW_STATE_DIR=~/.nexu/runtime/openclaw/state) - Add `gateway` subcommand to OpenClaw plist generation - Use OPENCLAW_CONFIG_PATH env var instead of --config argument - Add --auth none for dev mode to simplify local development - Update tests to verify OPENCLAW_CONFIG_PATH env var presence Tested with ./scripts/dev-launchd.sh - Controller and OpenClaw WebSocket connection verified working. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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 856-861: Duplicate cleanup is running both in the onBeforeQuit
callback passed to installLaunchdQuitHandler and in the global 'before-quit'
handler; remove the double-run by centralizing cleanup in one place: either move
sleepGuard.dispose("launchd-quit"), await diagnosticsReporter?.flushNow(), and
flushRuntimeLoggers() exclusively into the installLaunchdQuitHandler's
onBeforeQuit callback, or keep them only in the global 'before-quit' handler and
wrap that cleanup with if (!launchdResult) (the variable set by
installLaunchdQuitHandler) so the global handler skips cleanup when the launchd
flow handled the quit; update references to sleepGuard.dispose,
diagnosticsReporter.flushNow, flushRuntimeLoggers, installLaunchdQuitHandler,
and the global 'before-quit' handler accordingly.
- Around line 848-862: The issue is that launchdResult may be set by
runLaunchdColdStart()/bootstrapWithLaunchd but if ensureDesktopAuthSession
throws the quit handler never gets installed; fix by installing the launchd quit
handler immediately after bootstrapWithLaunchd/runLaunchdColdStart returns
(i.e., right after launchdResult is assigned and before calling
ensureDesktopAuthSession) using installLaunchdQuitHandler with the same {
launchd: launchdResult.launchd, labels: launchdResult.labels, webServer:
launchdResult.webServer, onBeforeQuit: (...) } block so the handler is present
even if auth fails; alternatively, if you prefer not to install early, ensure
the catch block clears launchdResult = null when the quit handler wasn’t
installed so the before-quit handler won’t assume a handler exists.
In `@scripts/dev-launchd.sh`:
- Around line 222-227: The tail_logs function can call tail with a glob that
fails when no *.log files exist; update tail_logs to defensively check for logs
before calling tail by testing the glob expansion (e.g., set positional params
or an array from "$LOG_DIR"/*.log and verify a file exists) and print a friendly
message or wait/exit if none found; reference the tail_logs function and the
LOG_DIR/*.log glob when making this change.
- Around line 173-181: The loop uses an unused variable and calls curl without a
timeout; change the loop header from using the unused "i" to a throwaway
variable (e.g., "_" in the for loop) to silence SC2034, and add explicit curl
timeout options to the probe (for example --connect-timeout and/or --max-time
and --fail or --silent flags) when calling curl against
"http://127.0.0.1:$CONTROLLER_PORT/api/auth/get-session" so each iteration
cannot hang indefinitely.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3cbe7789-8869-49fe-9e3a-8c665a37e417
📒 Files selected for processing (5)
apps/desktop/main/index.tsapps/desktop/main/services/plist-generator.tsscripts/dev-launchd.shspecs/designs/launchd-process-architecture/progress/STATUS.mdtests/desktop/plist-generator.test.ts
✅ Files skipped from review due to trivial changes (2)
- specs/designs/launchd-process-architecture/progress/STATUS.md
- tests/desktop/plist-generator.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/desktop/main/services/plist-generator.ts
… workflow - Add "starting" RuntimeStatus: when OpenClaw gateway is unreachable but process is alive, show "启动中" instead of "已离线" - Parallelize launchd service install/start + web server (Promise.all) - Use adaptive readiness polling (50ms→250ms) instead of fixed 250ms - Fix dev-launchd.sh stop: use bootout directly instead of SIGTERM+bootout race with KeepAlive; use SIGKILL for Electron to bypass quit handler - Dev quit handler keeps services running (run-in-background) so vite HMR restarts don't kill launchd services - Add tool progress prompt to nexu-platform-bootstrap plugin - Disable humanDelay in config compiler - Cold start time reduced from ~5s to ~2s Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Stop: wait for ports to free after bootout, SIGKILL orphans including chrome_crashpad_handler - Fix resolveLaunchdPaths for packaged mode: OpenClaw is at runtime/openclaw/node_modules/openclaw/openclaw.mjs, not runtime/openclaw-runtime/openclaw.mjs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (5)
apps/desktop/main/index.ts (3)
857-863:⚠️ Potential issue | 🟠 MajorGate launchd bootstrap to macOS.
Line 857 only checks the feature flag. On Linux/Windows,
NEXU_USE_LAUNCHD=1still sends startup throughrunLaunchdColdStart(), where thelaunchctlpath fails instead of falling back to the orchestrator.🛠️ Minimal fix
- const useLaunchd = isLaunchdBootstrapEnabled(); + const useLaunchd = + process.platform === "darwin" && isLaunchdBootstrapEnabled();🤖 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 857 - 863, The code currently calls runLaunchdColdStart() based solely on isLaunchdBootstrapEnabled(); update the gating logic to also require macOS by checking process.platform === "darwin" (or an equivalent platform check) before invoking runLaunchdColdStart(), so non-macOS systems fall back to the orchestrator path; modify the condition around useLaunchd and the call to runLaunchdColdStart() (and adjust the logColdStart message if needed) to include the platform check.
511-530:⚠️ Potential issue | 🟠 MajorInstall the launchd quit handler before auth bootstrap can fail.
launchdResultis assigned at Line 511, butinstallLaunchdQuitHandler()is only reached afterrunLaunchdColdStart()finishes. IfensureDesktopAuthSession()rejects at Line 530, the globalbefore-quitpath still treats the app as launchd-managed, but there is no launchd quit handler to show the dialog or stop services gracefully.Also applies to: 862-876
🤖 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 511 - 530, After obtaining launchdResult from bootstrapWithLaunchd, call installLaunchdQuitHandler(...) immediately (using the launchdResult or its relevant fields) before awaiting ensureDesktopAuthSession so the quit handler is registered even if ensureDesktopAuthSession rejects; update the runLaunchdColdStart / launchd startup flow (where launchdResult is created and where ensureDesktopAuthSession is awaited) to invoke installLaunchdQuitHandler(launchdResult) right after launchdResult is set and before any awaits that can throw, and apply the same change in the other launchd startup block that mirrors this flow.
866-875:⚠️ Potential issue | 🟠 MajorKeep launchd quit on one authoritative
before-quitpath.This adds a second handler, but the existing listeners at Lines 920-934 still run first. On a canceled quit, diagnostics are already unsubscribed and
sleepGuard/log flushing have already run, so the app stays open with launchd services intact but local runtime cleanup already torn down.Also applies to: 920-942
🤖 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 866 - 875, There are two before-quit paths: the installLaunchdQuitHandler call is adding an onBeforeQuit that duplicates teardown (sleepGuard.dispose, diagnosticsReporter.flushNow, flushRuntimeLoggers) which races with the file's existing centralized before-quit listeners; remove the onBeforeQuit callback from installLaunchdQuitHandler and instead invoke the same cleanup (sleepGuard?.dispose("launchd-quit"); await diagnosticsReporter?.flushNow().catch(() => undefined); flushRuntimeLoggers();) from the single authoritative before-quit handler already defined in this file so launchd teardown always follows the central quit flow.apps/desktop/main/services/quit-handler.ts (1)
128-145:⚠️ Potential issue | 🟡 MinorMake
app.quit()best-effort, not conditional on cleanup succeeding.Lines 132-142 await several shutdown steps without catching. If
onBeforeQuit,webServer.close(), orstopServiceGracefully()rejects,quitWithDecision()exits early and never reachesapp.quit(), unlikeinstallLaunchdQuitHandler()above.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/services/quit-handler.ts` around lines 128 - 145, quitWithDecision currently awaits opts.onBeforeQuit, opts.webServer.close, and opts.launchd.stopServiceGracefully calls directly so any rejection prevents reaching app.quit(); wrap the cleanup calls in a try/catch (or a try/finally) so failures are caught/logged but do not stop execution, ensuring app.quit() is always invoked (similar behavior to installLaunchdQuitHandler). Specifically, handle errors from opts.onBeforeQuit(), opts.webServer.close(), and opts.launchd.stopServiceGracefully(opts.labels.openclaw/opts.labels.controller) and then call app.quit() regardless.scripts/dev-launchd.sh (1)
181-185:⚠️ Potential issue | 🟡 MinorHandle the empty-log case before tailing.
On a fresh run,
$LOG_DIRmay not contain any*.logfiles yet. Then the glob stays literal andtail -fexits immediately with a missing-file error.🛠️ Defensive fix
tail_logs() { echo "Tailing logs from $LOG_DIR..." echo "(Press Ctrl+C to stop)" echo "" - tail -f "$LOG_DIR"/*.log + local files=("$LOG_DIR"/*.log) + if [ ! -e "${files[0]}" ]; then + echo "No log files found yet in $LOG_DIR" + return 0 + fi + tail -f "${files[@]}" }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/dev-launchd.sh` around lines 181 - 185, The tail_logs function calls tail -f "$LOG_DIR"/*.log which fails if no .log files exist; update tail_logs to check for matching log files before invoking tail: test for any files (e.g. using a glob check like set -- "$LOG_DIR"/*.log && [ -e "$1" ] or ls "$LOG_DIR"/*.log >/dev/null 2>&1) and if none are found print a friendly "No logs yet" message (or wait/exit) instead of calling tail; keep references to tail_logs, LOG_DIR and the tail -f invocation so the check is placed immediately before that command.
🧹 Nitpick comments (1)
apps/desktop/main/services/quit-handler.ts (1)
80-117: Use the desktop logger here instead of rawconsole.*.These shutdown events bypass the unified lifecycle logs and are harder to correlate once the app is packaged. Please route them through the existing runtime logging helper or inject a logger into this service.
As per coding guidelines "Logging must be structured (pino or console JSON) and never log credentials".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/main/services/quit-handler.ts` around lines 80 - 117, Replace all raw console.log/console.error calls in quit-handler.ts with the structured runtime logger used elsewhere: accept or use the injected logger (e.g., opts.logger or the runtime logging helper) and call logger.info/logger.error with the same messages and error objects for the onBeforeQuit, webServer.close and launchd.stopServiceGracefully blocks (including the "Stopping launchd services..." / "Keeping services running in background" and the per-label stop messages). Ensure the logger is present on opts (or import the shared runtime logger), update calls around opts.onBeforeQuit, opts.webServer.close, and opts.launchd.stopServiceGracefully to pass the error object as a structured field (not concatenated strings) and do not log any sensitive data or credentials.
🤖 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/controller/src/runtime/loops.ts`:
- Around line 69-84: The gateway health logic currently never triggers restarts
when the process has a PID or when HTTP errors occur; update the branch handling
result.status !== null and the branch where processAlive is true to increment a
failure counter on params.state (e.g., params.state.gatewayFailureCount) and
call params.processManager?.restartForHealth() once that counter exceeds a short
threshold (or after a timeout), while resetting the counter on success; ensure
you update params.state.gatewayStatus and params.state.lastGatewayError as now,
but add/clear gatewayFailureCount so a wedged-but-alive gateway (HTTP 5xx or
never binding) will be restarted instead of staying stuck in
"starting"/"degraded".
In `@apps/controller/src/runtime/openclaw-process.ts`:
- Around line 73-77: The isAlive() method currently falls back to PID 0 when
child.pid is undefined, causing isProcessAlive(0) to probe the wrong process;
update isAlive() to validate child.pid is a defined positive integer (e.g., pid
> 0) before calling isProcessAlive(this.child.pid), and return false if pid is
missing or non-positive so that non-started/spawn-failed children aren't
reported alive; reference the isAlive method, the child property, child.pid, and
isProcessAlive when making the change.
In `@scripts/dev-launchd.sh`:
- Around line 24-26: The script currently hard-codes CONTROLLER_PORT to 50800;
update it to read the controller port dynamically (the same value desktop sets
at runtime) instead of a fixed default so cleanup/status use the real socket.
Replace the fixed CONTROLLER_PORT="${CONTROLLER_PORT:-50800}" with logic that
loads the runtime configuration or an exported environment variable (e.g. parse
runtimeConfig.ports.controller from the runtime config file or accept
CONTROLLER_PORT from the caller) and propagate that value to the locations
mentioned (lines around the CONTROLLER_PORT assignment, the blocks referenced at
64-68 and in full_cleanup()/status handlers at ~150-166). Ensure full_cleanup()
and status reference this dynamic CONTROLLER_PORT variable so they wait/check
the actual controller socket.
---
Duplicate comments:
In `@apps/desktop/main/index.ts`:
- Around line 857-863: The code currently calls runLaunchdColdStart() based
solely on isLaunchdBootstrapEnabled(); update the gating logic to also require
macOS by checking process.platform === "darwin" (or an equivalent platform
check) before invoking runLaunchdColdStart(), so non-macOS systems fall back to
the orchestrator path; modify the condition around useLaunchd and the call to
runLaunchdColdStart() (and adjust the logColdStart message if needed) to include
the platform check.
- Around line 511-530: After obtaining launchdResult from bootstrapWithLaunchd,
call installLaunchdQuitHandler(...) immediately (using the launchdResult or its
relevant fields) before awaiting ensureDesktopAuthSession so the quit handler is
registered even if ensureDesktopAuthSession rejects; update the
runLaunchdColdStart / launchd startup flow (where launchdResult is created and
where ensureDesktopAuthSession is awaited) to invoke
installLaunchdQuitHandler(launchdResult) right after launchdResult is set and
before any awaits that can throw, and apply the same change in the other launchd
startup block that mirrors this flow.
- Around line 866-875: There are two before-quit paths: the
installLaunchdQuitHandler call is adding an onBeforeQuit that duplicates
teardown (sleepGuard.dispose, diagnosticsReporter.flushNow, flushRuntimeLoggers)
which races with the file's existing centralized before-quit listeners; remove
the onBeforeQuit callback from installLaunchdQuitHandler and instead invoke the
same cleanup (sleepGuard?.dispose("launchd-quit"); await
diagnosticsReporter?.flushNow().catch(() => undefined); flushRuntimeLoggers();)
from the single authoritative before-quit handler already defined in this file
so launchd teardown always follows the central quit flow.
In `@apps/desktop/main/services/quit-handler.ts`:
- Around line 128-145: quitWithDecision currently awaits opts.onBeforeQuit,
opts.webServer.close, and opts.launchd.stopServiceGracefully calls directly so
any rejection prevents reaching app.quit(); wrap the cleanup calls in a
try/catch (or a try/finally) so failures are caught/logged but do not stop
execution, ensuring app.quit() is always invoked (similar behavior to
installLaunchdQuitHandler). Specifically, handle errors from
opts.onBeforeQuit(), opts.webServer.close(), and
opts.launchd.stopServiceGracefully(opts.labels.openclaw/opts.labels.controller)
and then call app.quit() regardless.
In `@scripts/dev-launchd.sh`:
- Around line 181-185: The tail_logs function calls tail -f "$LOG_DIR"/*.log
which fails if no .log files exist; update tail_logs to check for matching log
files before invoking tail: test for any files (e.g. using a glob check like set
-- "$LOG_DIR"/*.log && [ -e "$1" ] or ls "$LOG_DIR"/*.log >/dev/null 2>&1) and
if none are found print a friendly "No logs yet" message (or wait/exit) instead
of calling tail; keep references to tail_logs, LOG_DIR and the tail -f
invocation so the check is placed immediately before that command.
---
Nitpick comments:
In `@apps/desktop/main/services/quit-handler.ts`:
- Around line 80-117: Replace all raw console.log/console.error calls in
quit-handler.ts with the structured runtime logger used elsewhere: accept or use
the injected logger (e.g., opts.logger or the runtime logging helper) and call
logger.info/logger.error with the same messages and error objects for the
onBeforeQuit, webServer.close and launchd.stopServiceGracefully blocks
(including the "Stopping launchd services..." / "Keeping services running in
background" and the per-label stop messages). Ensure the logger is present on
opts (or import the shared runtime logger), update calls around
opts.onBeforeQuit, opts.webServer.close, and opts.launchd.stopServiceGracefully
to pass the error object as a structured field (not concatenated strings) and do
not log any sensitive data or credentials.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 01cc76f5-b5e7-4346-b077-c2a5ec8d5a18
📒 Files selected for processing (13)
apps/controller/openapi.jsonapps/controller/src/lib/openclaw-config-compiler.tsapps/controller/src/routes/desktop-routes.tsapps/controller/src/runtime/loops.tsapps/controller/src/runtime/openclaw-process.tsapps/controller/src/runtime/state.tsapps/controller/static/runtime-plugins/nexu-platform-bootstrap/index.jsapps/desktop/main/index.tsapps/desktop/main/services/launchd-bootstrap.tsapps/desktop/main/services/quit-handler.tsapps/web/lib/api/types.gen.tsapps/web/src/pages/home.tsxscripts/dev-launchd.sh
✅ Files skipped from review due to trivial changes (3)
- apps/controller/src/lib/openclaw-config-compiler.ts
- apps/controller/src/routes/desktop-routes.ts
- apps/controller/openapi.json
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/desktop/main/services/launchd-bootstrap.ts
There was a problem hiding this comment.
♻️ Duplicate comments (2)
scripts/dev-launchd.sh (2)
193-198:⚠️ Potential issue | 🟡 MinorGuard
tail_logswhen no log files exist.Line 197 can fail on first run because
"$LOG_DIR"/*.logmay not expand to any files.Suggested fix
tail_logs() { echo "Tailing logs from $LOG_DIR..." echo "(Press Ctrl+C to stop)" echo "" - tail -f "$LOG_DIR"/*.log + if compgen -G "$LOG_DIR"/*.log >/dev/null 2>&1; then + tail -f "$LOG_DIR"/*.log + else + echo "No log files found yet in $LOG_DIR" + exit 0 + fi }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/dev-launchd.sh` around lines 193 - 198, The tail_logs function currently calls tail -f "$LOG_DIR"/*.log which will fail if the glob matches nothing; update tail_logs (referencing tail_logs and LOG_DIR) to first expand the glob into an array or check for existence (e.g., files=("$LOG_DIR"/*.log) and test [ -e "${files[0]}" ] or similar) and only call tail -f with the file list when at least one log exists, otherwise print a clear "No logs found in $LOG_DIR" message and return gracefully.
25-26:⚠️ Potential issue | 🟠 MajorUse the runtime-selected controller port instead of a fixed
50800.Line 25 hard-codes
CONTROLLER_PORT, but launchd bootstrap uses dynamic ports. This can make cleanup/status target the wrong socket (Line 63-66, Line 162-165), causing false “not listening” and incomplete waits.Suggested fix
-CONTROLLER_PORT="${CONTROLLER_PORT:-50800}" +# Prefer caller-provided value; otherwise derive from runtime config if available. +# Fallback only as last resort. +CONTROLLER_PORT="${CONTROLLER_PORT:-}" +if [ -z "$CONTROLLER_PORT" ] && [ -f "$HOME/.nexu/runtime/runtime-config.json" ]; then + CONTROLLER_PORT="$(sed -nE 's/.*"controller"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$HOME/.nexu/runtime/runtime-config.json" | head -1)" +fi +CONTROLLER_PORT="${CONTROLLER_PORT:-50800}"Also applies to: 63-66, 158-166
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/dev-launchd.sh` around lines 25 - 26, The script currently hard-codes CONTROLLER_PORT to 50800; remove that fixed default so the script uses the runtime-selected CONTROLLER_PORT provided by launchd (i.e. change CONTROLLER_PORT="${CONTROLLER_PORT:-50800}" to just use the existing CONTROLLER_PORT environment value or an empty/default-less expansion), and ensure all references that check/cleanup sockets (the places using CONTROLLER_PORT and OPENCLAW_PORT later in the script) use that variable rather than a fixed port; update any socket checks/cleanup/status logic to read from CONTROLLER_PORT so they target the dynamic launchd-assigned port.
🧹 Nitpick comments (1)
scripts/dev-launchd.sh (1)
29-35: Remove unused path variables or wire them into plist generation.Line 29-35 defines values that are never consumed in this script (
NODE_PATH,CONTROLLER_ENTRY,OPENCLAW_PATH,OPENCLAW_CONFIG). This is easy to drift and confuses source-of-truth.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@scripts/dev-launchd.sh` around lines 29 - 35, The variables NODE_PATH, CONTROLLER_ENTRY, OPENCLAW_PATH, OPENCLAW_STATE_DIR and OPENCLAW_CONFIG are defined but never used; either remove these unused definitions from the script or wire them into the launchd plist generation so they become authoritative values. Locate the assignments for NODE_PATH, CONTROLLER_ENTRY, OPENCLAW_PATH, OPENCLAW_STATE_DIR and OPENCLAW_CONFIG in scripts/dev-launchd.sh and either (A) delete those unused lines to avoid drift, or (B) reference them when building the plist (e.g., populate EnvironmentVariables/<key> or program arguments with CONTROLLER_ENTRY/OPENCLAW_PATH and set OPENCLAW_CONFIG/OPENCLAW_STATE_DIR where the runtime expects them) to ensure the script is the single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@scripts/dev-launchd.sh`:
- Around line 193-198: The tail_logs function currently calls tail -f
"$LOG_DIR"/*.log which will fail if the glob matches nothing; update tail_logs
(referencing tail_logs and LOG_DIR) to first expand the glob into an array or
check for existence (e.g., files=("$LOG_DIR"/*.log) and test [ -e "${files[0]}"
] or similar) and only call tail -f with the file list when at least one log
exists, otherwise print a clear "No logs found in $LOG_DIR" message and return
gracefully.
- Around line 25-26: The script currently hard-codes CONTROLLER_PORT to 50800;
remove that fixed default so the script uses the runtime-selected
CONTROLLER_PORT provided by launchd (i.e. change
CONTROLLER_PORT="${CONTROLLER_PORT:-50800}" to just use the existing
CONTROLLER_PORT environment value or an empty/default-less expansion), and
ensure all references that check/cleanup sockets (the places using
CONTROLLER_PORT and OPENCLAW_PORT later in the script) use that variable rather
than a fixed port; update any socket checks/cleanup/status logic to read from
CONTROLLER_PORT so they target the dynamic launchd-assigned port.
---
Nitpick comments:
In `@scripts/dev-launchd.sh`:
- Around line 29-35: The variables NODE_PATH, CONTROLLER_ENTRY, OPENCLAW_PATH,
OPENCLAW_STATE_DIR and OPENCLAW_CONFIG are defined but never used; either remove
these unused definitions from the script or wire them into the launchd plist
generation so they become authoritative values. Locate the assignments for
NODE_PATH, CONTROLLER_ENTRY, OPENCLAW_PATH, OPENCLAW_STATE_DIR and
OPENCLAW_CONFIG in scripts/dev-launchd.sh and either (A) delete those unused
lines to avoid drift, or (B) reference them when building the plist (e.g.,
populate EnvironmentVariables/<key> or program arguments with
CONTROLLER_ENTRY/OPENCLAW_PATH and set OPENCLAW_CONFIG/OPENCLAW_STATE_DIR where
the runtime expects them) to ensure the script is the single source of truth.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9b4a2183-4cf7-4ab6-91c9-f0f8188ba4fa
📒 Files selected for processing (2)
apps/desktop/main/services/launchd-bootstrap.tsscripts/dev-launchd.sh
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/desktop/main/services/launchd-bootstrap.ts
…tartup When the WebSocket to OpenClaw gateway isn't connected yet (during startup), channels were shown as "disconnected" (red). Now they show as "connecting" (yellow pulse) when the runtime is still starting, giving users a much less alarming startup experience. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add explicit "booting" → "ready" lifecycle to ControllerRuntimeState. During boot, gateway-unreachable is always treated as "starting" (not "unhealthy"), regardless of whether the process manager owns the OpenClaw process (fixes launchd mode where processManager.isAlive() returns false). Channel live status also uses bootPhase to show "connecting" during startup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The embedded web server serves static files from apps/web/dist, so code changes to the web app require a build step before starting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bootPhase was set to "ready" immediately after wsClient.connect(), but the WS handshake hadn't completed yet. Health loop then saw gateway-unreachable + bootPhase=ready → "unhealthy" → UI showed "已离线" during startup. Now bootPhase transitions to "ready" inside the onConnected callback, so the entire startup shows "starting" → "active" cleanly. Also adds temporary debug logs to home.tsx for startup diagnostics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Channel error status now shows translated lastError (e.g. "会话已过期" instead of generic "错误") - Controller maps WeChat "not configured" + not running to "session expired" for better UX - Add i18n keys for common channel errors (session expired, not configured, disabled) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use warning color (orange) instead of danger (red) for known recoverable errors like session expired, with actionable label "请重新连接" / "Reconnect required". Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The exponential backoff for OpenClaw WebSocket reconnection could reach 16s+ during startup, causing the UI to stay in "starting" state for 20+ seconds. Cap at 4s so retry sequence is 1→2→4→4→4s. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…way up When the health loop detects the gateway HTTP endpoint becomes reachable, it calls wsClient.retryNow() to cancel the backoff timer and connect immediately. This eliminates the 4-16s gap between gateway ready and WS connected during startup. Also replaces the ugly "Starting local services..." loading screen with a minimal Nexu logo pulse animation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- main.tsx had a duplicate SurfaceFrame with old loading text; replaced with Nexu logo pulse animation matching surface-frame.tsx - dev-launchd.sh now checks for dist/index.html and rebuilds desktop if missing, preventing blank screen after accidental dist deletion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace plain loading screen with animated Nexu logo matching the design system prototype (NexuLoader.tsx). Four quadrants light up sequentially in brand colors: orange, green, pink, gold. Pure CSS animation, no framer-motion dependency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
main.tsx had its own SurfaceFrame copy with the old loading screen. Now imports from components/surface-frame.tsx so both Runtime Console and Desktop Shell views use the same 4-color Nexu loader. Background updated to dark radial gradient matching desktop theme. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…urface - Move webview load event listener into the ref callback to avoid race where dom-ready fires before useEffect binds. Use did-finish-load (fires after navigation complete) instead of dom-ready. - Default activeSurface to "web" in both dev and packaged mode so the Nexu loader and web UI are visible immediately. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Launchd mode uses an embedded web server (no web sidecar process), but better-auth's /api/auth/get-session endpoint was missing, causing AuthLayout to block rendering. Add a mock desktop session response so the web app proceeds past auth in desktop local mode. Also removes temporary DevTools debug code. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In dev mode, load the renderer from vite dev server (VITE_DEV_SERVER_URL) instead of pre-built dist/index.html. This fixes blank pages after vite HMR restarts Electron, since the dev server always serves the latest code. Production still uses loadFile for the static dist. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When renderer loads from vite dev server (localhost:5180), fetch to embedded web server (127.0.0.1:50810) is cross-origin. Add CORS headers to allow dev mode requests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dev mode was hiding the window on close (for vite HMR), but this made Dock right-click quit feel broken. Now dev mode lets the window close normally — services are managed by pnpm stop. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add trap to dev-launchd.sh so launchd services are cleaned up when Electron quits, Ctrl+C is pressed, or the script exits for any reason. Previously services stayed running after Dock quit. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The before-quit handler was calling preventDefault() even in dev mode, preventing Dock right-click quit from working. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add aria-label to loader SVG to fix biome a11y/noSvgWithoutTitle - Use pnpm build (all packages) instead of individual filter builds in dev-launchd.sh to ensure @nexu/shared builds before web Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Development progress files no longer needed after implementation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI runners may not support launchd properly. Now launchd is only enabled via explicit NEXU_USE_LAUNCHD=1 (dev scripts) or in packaged macOS apps. CI and plain `pnpm dev` use orchestrator mode. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pnpm start must be non-blocking for CI compatibility (desktop-check-dev.sh). Restore dev.sh (tmux orchestrator) as default. Launchd mode available via: - pnpm start:launchd / stop:launchd / restart:launchd Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Set NEXU_USE_LAUNCHD=0 so desktop-ci uses the tmux orchestrator mode which is non-blocking and CI-compatible. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove vite dev server loadURL (causes CORS + preload issues) - dev-launchd.sh launches electron directly after build (no vite watch) - Restore loadFile for all modes (file:// protocol, no CORS) - CI desktop-check uses dev.sh directly (non-blocking tmux mode) - package.json: pnpm start uses dev-launchd.sh (launchd mode) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve conflicts by keeping our branch's versions (all changes are incremental improvements after #405). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Migrate desktop process management from Electron child-process tree to macOS launchd LaunchAgents. Services (Controller, OpenClaw) run as independent system-managed processes with automatic crash recovery.
Process Management
Startup Performance
Startup UX
Status Display
startingRuntimeStatus withbootPhaselifecyclePackaged App
Dev Workflow
pnpm start/pnpm stop/pnpm restartfor launchd modepnpm --filter @nexu/desktop devfor orchestrator mode (vite HMR)Closes #442
Related to #400, #401
Test plan
pnpm typecheckpassespnpm testpassespnpm start→ services start, UI shows Starting → Runningpnpm stop→ all services cleanly stoppedpnpm restart→ clean restart cycle🤖 Generated with Claude Code