项目文件夹

文件
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

20 KiB

title, sidebarTitle, description
title sidebarTitle description
Desktop local development Local development Why and how the Eliza desktop dev orchestrator (eliza/packages/app-core/scripts/dev-platform.mjs) runs Vite, the API, and Electrobun together — environment variables, signals, and shutdown behavior.

The desktop dev stack is not a single binary. bun run dev:desktop and bun run dev:desktop:watch run eliza/packages/app-core/scripts/dev-platform.mjs, which orchestrates separate processes: optional one-off vite build, optional repo-root tsdown, then long-lived Vite (when ELIZA_DESKTOP_VITE_WATCH=1), bun --watch API, and Electrobun.

Why orchestrate? Electrobun needs (a) a renderer URL, (b) often a running dashboard API, and (c) in dev, a root dist/ bundle for the embedded Eliza runtime. Doing that manually is error-prone; one script keeps ports, env vars, and shutdown consistent.

Commands

CLI flags (preferred for ad-hoc use; bun run dev:desktop -- --help lists them): --no-api, --force-renderer, --rollup-watch, --vite-force.

Command What starts Typical use
bun run dev:desktop API (unless --no-api) + Electrobun; skips vite build when packages/app/dist is fresher than sources Fast iteration against built renderer assets
bun run dev:desktop:watch Same orchestrator with ELIZA_DESKTOP_VITE_WATCH=1Vite dev server + HMR Desktop UI workflow
bun run dev Browser dashboard stack only (API + Vite) Headless-friendly dashboard iteration

Startup tables: the orchestrator, Vite, API, and Electrobun each print a plain-text settings table (columns Setting / Effective / Source / Change) so you can see defaults vs env and how to change a knob. Run without --help to see them in the terminal.

Startup tables and terminal banners

On a TTY, tables may use a Unicode box frame and a large figlet-style title for the subsystem name (orchestrator, Vite, API, Electrobun), with ANSI color (magenta title, cyan frame) unless NO_COLOR is set (FORCE_COLOR can opt in for piped output).

Why: Desktop dev runs four processes with overlapping env (ports, URLs, feature flags). The goal is fast visual scanning of effective values for humans and IDE agents — the same rationale as port pre-allocation and prefixed logs. This is not companion or dashboard UI; it does not ship to end users as product chrome.

Docs: this page is the current developer diagnostics and desktop workspace reference.

Why separate commands? A full production Vite build is still useful when you want parity with shipped assets or when you are not touching the desktop shell UI. bun run dev:desktop:watch points Electrobun at the Vite dev server for HMR, while bun run dev stays on the browser dashboard stack.

Legacy: Rollup vite build --watch

If you explicitly need file output on every save (e.g. debugging Rollup behavior):

ELIZA_DESKTOP_VITE_WATCH=1 bun eliza/packages/app-core/scripts/dev-platform.mjs -- --rollup-watch
# or env-only:
ELIZA_DESKTOP_VITE_WATCH=1 ELIZA_DESKTOP_VITE_BUILD_WATCH=1 bun eliza/packages/app-core/scripts/dev-platform.mjs

Why this is opt-in: vite build --watch still runs Rollup production emits; “3 modules transformed” can still mean seconds rewriting multiMB chunks. The default watch path uses the Vite dev server instead.

Environment variables

Variable Purpose
ELIZA_DESKTOP_VITE_WATCH=1 Enables watch workflow (dev server by default; see below)
ELIZA_DESKTOP_VITE_BUILD_WATCH=1 With VITE_WATCH, use vite build --watch instead of vite dev
ELIZA_PORT Vite / expected UI port (default 2138)
ELIZA_API_PORT API port (default 31337); forwarded to Vite proxy env and Electrobun
ELIZA_RENDERER_URL Set by the orchestrator when using Vite dev — Electrobun’s resolveRendererUrl() prefers this over the built-in static server (why: HMR only works against the dev server)
ELIZA_DESKTOP_RENDERER_BUILD=always Force vite build even when dist/ looks fresh
--force-renderer Same as always rebuilding the renderer
--vite-force Pass vite --force when the Vite dev server starts (clear dep optimization cache)
--rollup-watch With ELIZA_DESKTOP_VITE_WATCH=1, use vite build --watch instead of vite dev
--no-api Electrobun only; no dev-server.ts child
ELIZA_DEFER_LOCAL_EMBEDDING_WARMUP Deferred by default everywhere (desktop dev included): GGUF embedding prefetch runs after the API runtime is ready. Set =0/false/no/off for the eager process-entry prefetch.
ELIZA_SKIP_LOCAL_EMBEDDING_WARMUP Skips GGUF embedding prefetch entirely. Desktop dev sets this by default only when CI is truthy; explicit values are preserved.
ELIZA_ENABLE_STARTUP_LOCAL_EMBEDDING_WARMUP=1 Opt in to starting GGUF embedding warmup during runtime bootstrap when neither defer nor skip is set.
ELIZA_DISABLE_LOCAL_EMBEDDINGS=1 Stronger switch: disables local TEXT_EMBEDDING registration entirely. Use this when another provider handles embeddings or local embeddings must be unavailable.
ELIZA_DESKTOP_SCREENSHOT_SERVER Default on for dev:desktop / bun run dev: Electrobun listens on 127.0.0.1:ELIZA_SCREENSHOT_SERVER_PORT (default 31339); the Eliza API proxies GET /api/dev/cursor-screenshot (loopback) as a full-screen PNG for agents/tools (macOS needs Screen Recording permission). Set to 0, false, no, or off to disable.
ELIZA_DESKTOP_DEV_LOG Default on: child logs (vite / api / electrobun) are mirrored to <stateDir>/desktop-dev-console.log at the repo root. GET /api/dev/console-log on the API (loopback) returns a tail (?maxLines=, ?maxBytes=). Set to 0 / false / no / off to disable.

When default ports are busy

The dev-platform orchestrator runs dev:desktop and bun run dev. Before starting long-lived children it probes loopback TCP starting at:

Env Role Default
ELIZA_API_PORT Eliza API (dev-server.ts) 31337
ELIZA_PORT Vite dev server (watch mode only) 2138

If the preferred port is already bound, the orchestrator tries preferred + 1, then +2, … (capped), and passes the resolved values into every child (ELIZA_DESKTOP_API_BASE, ELIZA_RENDERER_URL, Vite’s ELIZA_PORT, etc.).

Why pre-allocate in the parent (not only inside the API process): Vite reads vite.config.ts once at startup; the proxy’s target must match the API port before the first request. If only the API shifted ports after bind, the UI would still proxy to the old default until someone restarted Vite. Resolving ports once in dev-platform.mjs keeps orchestrator logs, env, proxy, and Electrobun on the same numbers.

Packaged desktop (local embedded agent): the Electrobun main process calls findFirstAvailableLoopbackPort (packages/app-core/platforms/electrobun/src/native/loopback-port.ts) from the preferred ELIZA_PORT (default 2138), passes that to the entry.js start child, and after a healthy start updates process.env.ELIZA_PORT / ELIZA_API_PORT in the shell. Why we stopped default lsof + SIGKILL: a second Eliza (or any app) on the same default port is valid when state dirs differ; killing PIDs from the shell is surprising and can terminate unrelated work. Opt-in reclaim: ELIZA_AGENT_RECLAIM_STALE_PORT=1 runs the old “free this port first” behavior for developers who want single-instance takeover.

Detached windows: when the embedded API port is finalized or changes, injectApiBase runs for the main window and all SurfaceWindowManager windows (why: chat/settings/etc. must not keep polling a stale http://127.0.0.1:…).

Related: Desktop app — Port configuration; GET /api/dev/stack overwrites api.listenPort from the accepted socket when possible (why: truth beats env if something else retargets the server).

macOS: frameless window chrome (native dylib)

On macOS, Electrobun only copies libMacWindowEffects.dylib into the dev bundle when that file exists (see packages/app-core/platforms/electrobun/electrobun.config.ts). Without it, traffic-light layout, drag regions, and inner-edge resize can be missing or wrong — easy to mistake for a generic Electrobun bug.

After cloning the repo, or whenever you change native/macos/window-effects.mm, build the dylib from the Electrobun package:

bun run --cwd packages/app-core/platforms/electrobun build:native-effects

More detail: Electrobun shell package (README: macOS window chrome), and Electrobun macOS window chrome.

macOS: Local Network permission (gateway discovery)

The desktop shell uses Bonjour/mDNS to discover Eliza gateways on your LAN. macOS may show a Local Network privacy dialog — choose Allow if you rely on local discovery.

Eliza’s pinned Electrobun config types (as of the version in this repo) do not expose an Info.plist merge for NSLocalNetworkUsageDescription, so the OS may show a generic prompt. If upstream adds that hook later, we can set clearer copy; behavior does not depend on it.

Why vite build is sometimes skipped

Before starting services, the script checks viteRendererBuildNeeded() (scripts/lib/vite-renderer-dist-stale.mjs): compare packages/app/dist/index.html mtime against packages/app/src, vite.config.ts, shared packages (eliza/packages/ui, eliza/packages/app-core), etc.

Why mtime, not a full dependency graph? It is a cheap, local-first heuristic so restarts do not pay 10–30s for a redundant production build when sources did not change. Override when you need a clean bundle.

Signals, Ctrl-C, and detached children (Unix)

On macOS/Linux, long-lived children are spawned with detached: true so they live in a separate session from the orchestrator.

Why: A TTY Ctrl-C is delivered to the foreground process group. Without detached, Electrobun, Vite, and the API all receive SIGINT together. Electrobun then handles the first interrupt (“press Ctrl+C again…”) while Vite and the API keep running; the parent stays alive because stdio pipes are still open — it feels like the first Ctrl-C “did nothing.”

With detached, only the orchestrator gets TTY SIGINT; it runs a single shutdown path: SIGTERM each known subtree, short grace, then SIGKILL, then process.exit.

Second Ctrl-C while shutting down force-exits immediately (exit 1) so you are never stuck behind a grace timer.

Windows: detached is not used the same way (stdio + process model differ); port cleanup uses netstat/taskkill instead of only lsof.

Quitting from the app (Electrobun exits)

If you Quit from the native menu, Electrobun exits with code 0 while Vite and the API may still be running. The orchestrator watches the electrobun child: on exit, it stops the remaining services and exits.

Why: Otherwise the terminal session hangs after “App quitting…” because the parent process is still holding pipes to Vite/API — same underlying issue as a partial Ctrl-C shutdown.

Port cleanup before Vite (killUiListenPort)

Before binding the UI port, the script tries to kill whatever is already listening (why: stale Vite or a crashed run leaves EADDRINUSE). Implementation: scripts/lib/kill-ui-listen-port.mjs (Unix: lsof; Windows: netstat + taskkill).

Process trees and kill-process-tree

Shutdown uses signalSpawnedProcessTreeonly the PID tree rooted at each spawned child (why: avoid pkill bun style nukes that would kill unrelated Bun workspaces on the machine).

Seeing many bun processes

Expected. You typically have: the orchestrator, the Vite dev server, bun --watch API, bun run dev under Electrobun (preload build + bunx electrobun dev), plus Bun/Vite/Electrobun internals. Worry if counts grow without bound or processes survive after the dev session fully exits.

IDE and agent observability (Cursor, scripts)

Editors and coding agents do not see the native Electrobun window, hear audio, or auto-discover localhost. Eliza adds explicit, machine-readable hooks so tools can still reason about “what is running” and approximate “what the user sees.”

Why this exists

  1. Multi-process truth — Health is not one PID. Vite, the API, and Electrobun can disagree on ports; logs are interleaved. A single JSON endpoint and one log file avoid “grep five terminals.”
  2. Security vs convenience — Screenshot and log tail endpoints are loopback-only; the screenshot path uses a session token between Electrobun and the API proxy; the log API only tails a file named desktop-dev-console.log. Why: local-first does not mean “any process on the LAN may pull your screen.”
  3. Opt-out defaults — Screenshot and aggregated logging are on for dev:desktop / bun run dev because agents and humans debugging together benefit; both disable with ELIZA_DESKTOP_SCREENSHOT_SERVER=0 and ELIZA_DESKTOP_DEV_LOG=0 so you can shrink attack surface or disk I/O.
  4. Cursor does not auto-poll — Discovery is documentation + .cursor/rules (see repo) plus you asking the agent to run curl or read a file. Why: the product does not silently scan your machine; hooks are there when instructed.

GET /api/dev/stack (Eliza API)

Returns stable JSON (schema: eliza.dev.stack/v1): API listen port (from the socket when available), desktop URLs/ports from env (ELIZA_RENDERER_URL, ELIZA_PORT, …), cursorScreenshot / desktopDevLog availability and paths, and short hints (e.g. Electrobun’s internal RPC port in launcher logs).

Why on the API: agents often already probe /api/health; one extra GET reuses the same host and avoids parsing Electrobun’s ephemeral port.

node packages/app-core/scripts/desktop-stack-status.mjs --json

Script: packages/app-core/scripts/desktop-stack-status.mjs (with packages/app-core/scripts/lib/desktop-stack-status.mjs). Probes UI/API ports, fetches /api/dev/stack, /api/health, and /api/status.

Why a CLI: agents and CI can run it without loading the dashboard; JSON exit code reflects API health for simple automation.

Full-screen PNG — GET /api/dev/cursor-screenshot

Loopback only. Proxies Electrobun’s dev server (default 127.0.0.1:31339) which uses the same OS-level capture as ScreenCaptureManager.takeScreenshot() (e.g. macOS screencapture). Not webview-only pixels.

Why proxy through the API: one URL on the familiar API port; token stays in env between orchestrator-spawned children. Why full screen first: window-ID capture is platform-specific; this path reuses existing, tested code.

Aggregated console — file + GET /api/dev/console-log

Prefixed vite / api / electrobun lines are mirrored to <stateDir>/desktop-dev-console.log (session banner on each orchestrator start). GET /api/dev/console-log (loopback) returns a text tail; query maxLines (default 400, cap 5000) and maxBytes (default 256000).

Why a file: agents can read_file the path from desktopDevLog.filePath without HTTP. Why HTTP tail: avoids reading multi-megabyte logs into context; caps prevent OOM. Why basename allow-list: ELIZA_DESKTOP_DEV_LOG_PATH could otherwise be pointed at arbitrary files.

UI E2E (Playwright)

Browser smoke tests target the same renderer URL Electrobun loads in watch mode (http://localhost:<ELIZA_PORT>, default 2138). They do not drive the native Electrobun webview; tray, native menus, and packaged-only behaviors stay covered by bun run --cwd packages/app test:desktop:packaged (where applicable) and the release regression checklist.

Why Playwright: the app already ships Playwright for renderer and packaged checks, so the browser smoke flows now use the same supported stack instead of a separate TestCafe toolchain. This removes the vulnerable replicator dependency entirely and keeps the UI E2E surface on one runner.

Dependency: Playwright lives in @elizaai/app and the smoke specs live in packages/app/test/ui-smoke/. A normal root bun install still hoists workspace packages; these browser checks are opt-in through the app package scripts.

Browser runtime: the suite uses Playwright Chromium. Install the browser once with cd packages/app && bunx playwright install chromium if it is not already present on the machine.

Command Purpose
bun run --cwd packages/app test:e2e Run packages/app/test/ui-smoke/ui-smoke.spec.ts; auto-starts the Vite renderer on :2138 when needed.
bun run --cwd packages/app test:e2e test/ui-smoke/settings-chat-control.spec.ts Runs the companion media settings persistence smoke.
bun run --cwd packages/app test:desktop:packaged Runs the packaged renderer smoke against packages/app/dist/index.html; skips if dist is missing.

Full test matrix: bun run test does not run Playwright UI smoke by default. Set ELIZA_TEST_UI_PLAYWRIGHT=1 to append the UI suite to test/scripts/test-parallel.mjs (serial, after Vitest e2e). ELIZA_TEST_UI_TESTCAFE=1 is still accepted as a legacy alias.

Path A vs native webview (Phase B): These specs still target the renderer URL, not the embedded Electrobun webview. Packaged/native behaviors remain covered by bun run --cwd packages/app test:desktop:packaged, bun run --cwd packages/app test:e2e, and the release regression checklist.

Piece Role
.cursor/rules/eliza-desktop-dev-observability.mdc Cursor: when to use stack / screenshot / console hooks (why: product does not auto-scan localhost)
scripts/dev-platform.mjs Orchestrator; sets env for stack / screenshot / log path
scripts/lib/vite-renderer-dist-stale.mjs When vite build is needed
scripts/lib/kill-ui-listen-port.mjs Free UI port
scripts/lib/kill-process-tree.mjs Scoped tree kill
packages/app-core/scripts/lib/desktop-stack-status.mjs Port + HTTP probes for desktop:stack-status
packages/app-core/scripts/desktop-stack-status.mjs CLI entry for agents (--json)
eliza/packages/app-core/src/api/dev-stack.ts Payload for GET /api/dev/stack
eliza/packages/app-core/src/api/dev-console-log.ts Safe tail read for GET /api/dev/console-log
packages/app-core/platforms/electrobun/src/index.ts resolveRendererUrl(); starts screenshot dev server when enabled
packages/app-core/platforms/electrobun/src/screenshot-dev-server.ts Loopback PNG server (proxied as /api/dev/cursor-screenshot)
packages/app/playwright.ui-smoke.config.ts Playwright config for renderer smoke specs
packages/app/playwright.ui-packaged.config.ts Playwright config for packaged file:// smoke
packages/app/test/ui-smoke/ui-smoke.spec.ts Main UI traversal + TAB_PATHS parity (e.g. /apps disabled)
packages/app/test/ui-smoke/settings-chat-companion.spec.ts Companion media settings persistence
packages/app/test/ui-smoke/packaged-hash.spec.ts file:// + hash routing parity

See also