Electron + SvelteKit + TypeScript desktop frontend for Intent that talks to
the intentd daemon. It is consumed as the packages/cloudlands-fe git
submodule of intent-hq/intent.
PR updates run Linux metadata, static consumer contracts, workflow policy checks, type/lint checks, and unit tests. The merge queue reruns type/lint and unit tests and adds builds, integration tests, and relevant browser/native fixture suites against the merged tree. Browser suites also retain complete nightly/manual runs. Release-only diffs keep their validated exemptions; the required CI Gate rejects unexpected PR or queue skips and failed jobs.
The renderer is a SvelteKit application running inside Electron. It never talks to the backend directly. Instead, all backend access flows through a single seam:
SvelteKit renderer <-> AppClient (JSON-RPC boundary) <-> intentd daemon
- AppClient (
src/lib/client) is the only boundary the renderer uses to reach "the backend". Each domain exposes async query methods, reactivesubscribe()streams, and mutation methods. It ships with two implementations behind the same contract:- a live implementation (
src/lib/client/live) that speaks JSON-RPC to theintentddaemon over the IPC bridge, and - a mock implementation (
src/lib/client/mock) backed by in-memory fixtures, so the app runs standalone without a daemon.
- a live implementation (
- Shared / domain state uses Redux + redux-saga under
src/store/renderer. Svelte 5 runes are used for UI rendering only; they are not a home for shared or durable application state. - Node-bound code lives under feature
main/subtrees. The renderer and the Electron main process are a strict boundary: renderer code never imports from a feature'smain/subtree, and vice versa.
cloudlands-fe is normally developed as the packages/cloudlands-fe submodule
of the monorepo:
git clone https://github.com/intent-hq/intent.git
cd intent
# Initialize this submodule (some sibling submodules are still private,
# so initialize selectively rather than cloning with --recurse-submodules)
git submodule update --init --recursive packages/cloudlands-fe
# Fast browser-only component work
make dev-ui
# Full app with an intentd sidecar
make devmake dev-ui installs the locked frontend dependencies when needed and opens no
Electron or daemon process. make dev-ui DEV_PORT=5290 gives a concurrent workspace
its own strict Vite port. For the full stack, make dev builds intentd and launches
the app with it as a sidecar (see intentd sidecar pin).
You can also clone and run this repository on its own (the app falls back to the mock AppClient when no daemon is present):
git clone https://github.com/intent-hq/cloudlands-fe.git
cd cloudlands-fe
corepack pnpm install --frozen-lockfile
corepack pnpm run dev:uiUse corepack pnpm run dev instead when you need the complete Electron app. Without a
staged intentd sidecar, it falls back to the mock AppClient.
Use the repository-pinned pnpm through Corepack, not npm. The following scripts are
defined in package.json:
corepack pnpm install --frozen-lockfile # Install locked dependencies
corepack pnpm run dev:ui # Fast named-state component preview
corepack pnpm run dev:web # Complete plain-browser renderer
corepack pnpm run dev # Complete Vite + Electron app
corepack pnpm run dev:cdp # Electron app with CDP support
corepack pnpm run build # Production build (renderer, main, preload)
corepack pnpm run build:web # Build static plain-browser output in dist/web
corepack pnpm run check # Svelte + TypeScript diagnostics
corepack pnpm run lint # ESLint
corepack pnpm run format # Prettier write pass
corepack pnpm run test:unit # Vitest unit suite
corepack pnpm run test:playwright # Playwright testsUse dev:ui for isolated component work, dev:web for the complete browser renderer
and its client connection, and dev:cdp for Electron main, preload, native, window, or
shell work. A direct preview URL controls state, theme, width, and motion:
http://127.0.0.1:5190/sandbox/button?state=loading&theme=dark&width=420&motion=reduced
http://127.0.0.1:5190/sandbox/mention-agent-avatar?state=waiting&theme=dark&width=420&motion=reduced
Button provides default, loading, disabled, and destructive. Mention agent
avatar provides idle, waiting, and error. See the monorepo
Developer Guide for every
working URL, browser discovery and readiness calls, HMR and hidden-tab guidance, and a
targeted component-test command.
dev:web accepts VITE_INTENTD_WS_URL as a local-development convenience.
build:web deliberately does not compile that value into static JavaScript,
because a full WebSocket URL can contain userinfo, query credentials, or a
fragment. Production hosting should replace /runtime-config.js at response or
deployment time and set globalThis.__INTENT_RUNTIME_CONFIG__.intentdWsUrl to a
wss:// URL. Serve that asset over authenticated HTTPS with Cache-Control: no-store, and use a short-lived, per-user credential rather than a shared
secret. The URL is necessarily visible to that browser session; this separation
prevents it from leaking through versioned bundles, source maps, and long-lived
CDN caches. The committed empty runtime config preserves the standalone mock
fallback when no live daemon is configured.
The bundled intentd sidecar is pinned to an exact released version in
intentd.version: a bare semver with no leading v,
matching a vX.Y.Z / vX.Y.Z-beta.N tag on
intent-hq/intentd.
- Bumping the pin: edit
intentd.versionin a normal reviewable PR, then runnode scripts/fetch-sidecar.cjsto confirm the release assets exist and verify, andnode scripts/sync-intentd-protocol-version.mjsto regenerate the tests' defaultCURRENT_PROTOCOL_VERSION(src/test/current-protocol-version.ts); commit both files.auto-pin-intentddoes all three, andlint:intentd-protocol-pinfails a pin that skipped the sync. - Fetching the pinned sidecar:
node scripts/fetch-sidecar.cjsdownloads the cargo-dist release asset for the current platform/arch, verifies its sha256 against the release's.sha256asset, and stages the binary atresources/sidecar/intentd[.exe]. A GitHub token with read access (INTENTD_READ_PAT, orGH_TOKEN/GITHUB_TOKEN) is only needed while the intentd repo is private. The script is idempotent; use--forceto re-fetch. - Local dev builds:
scripts/copy-sidecar.cjsstill stages a locally built binary frompackages/intentd/target/release(make build-sidecar/make devin the monorepo).
The platform/arch → release-asset mapping lives in
scripts/fetch-sidecar-lib.mjs (unit-tested in
scripts/fetch-sidecar-lib.test.ts); update it there if the intentd release
target list changes.
Desktop builds are distributed through three rolling releases (alpha, beta,
stable) on
intent-hq/cloudlands-releases,
each pointing the auto-updater at the latest build for that channel:
- alpha — every
vX.Y.Ztag (cut by merging the release-please PR) builds the app and publishes both an immutable versioned release and the rollingalpharelease (.github/workflows/release-alpha.yml). - beta — a manual promotion of an existing versioned release (no new
build): dispatch
.github/workflows/promote-beta.ymlwith theversioninput to copy that release's assets and updater feeds into the rollingbetarelease. - stable — same promotion model via
.github/workflows/release-stable.yml; it additionally marks the promoted version as Latest.
Mac desktop builds support Apple Silicon only; new Intel Mac builds are no
longer published. Each channel publishes the selected release's ARM64 metadata
under latest-mac.yml, restoring automatic discovery for older native Apple
Silicon installations. Beta and stable feeds
advance only when that version is promoted to the respective channel.
Alpha builds using the retired latest-arm64-mac.yml feed need a one-time Apple
Silicon reinstall to return to the standard feed. That feed never reached beta
or stable.
Intel Macs are unsupported and cannot use the Apple Silicon installer.
The pinned prerelease cleanup command protects versions referenced by the Mac feed in both the source and release-mirror repositories.
src/
├── shared/ # Cross-process utilities (main + renderer)
├── lib/ # Renderer-only shared utilities & components
│ └── client/ # AppClient JSON-RPC boundary (live + mock)
├── main/ # Electron main-process code & utilities
├── preload/ # Electron preload scripts
├── features/ # Feature-first renderer modules
│ └── <name>/
│ ├── utils/ # Feature-local utilities
│ └── main/ # Feature's main-process (Node-bound) code
├── routes/ # SvelteKit pages
├── store/
│ └── renderer/ # Redux slices + sagas (shared/domain state)
└── test/
See AGENTS.md for the full conventions and routing guide.
Frontend documentation lives in the monorepo's docs/fe/
(the relative links resolve in a monorepo checkout, where this repo mounts at
packages/cloudlands-fe/). Key references:
- STATE_MANAGEMENT.md — Redux + saga state policy and the Svelte-store migration.
- MODULE_BOUNDARY_GUIDE.md — renderer↔main and feature boundaries.
- TYPE_SYSTEM_GUIDE.md — TypeScript and type safety conventions.
- EVENT_SYSTEM.md — the unified event system.
Bug reports and feature requests for all Intent components — including this frontend — are tracked centrally on the intent-hq/intent issue tracker, not on this repository.
The desktop app is local-first and ships no telemetry or analytics. The only network calls the app itself makes go to public GitHub Releases:
- Auto-updates & release notes — the built app checks for and downloads
updates from GitHub Releases on
intent-hq/cloudlands-releases,
and fetches release notes for the installed version from the same repo's
GitHub Releases API
(
src/features/release-notes/main/release-notes.service.ts).AUTO_UPDATE_URLinsrc/shared/constants.tsis the release-download base URL; thepublishURL inelectron-builder.ymlappends the release channel (e.g./stable).
Everything else goes through the intentd daemon the app is connected to —
local by default; connecting to a remote daemon makes that daemon traffic go
to the host you chose. If you connect the GitHub, Linear, or Sentry
integrations, the daemon calls those services' APIs on your behalf — nothing
is sent to them unless you configure the integration — and the daemon is
likewise what probes an MCP server when you test its connection. Daemon-side
network calls — provider OAuth sign-ins, the user-configured integrations
(GitHub, Linear, Sentry), user-configured MCP servers, and the sitter
self-update — are documented in the
monorepo README's Network & privacy section.
This repository is the frontend ported from the prior Electron app and
wired to the intentd daemon through the AppClient JSON-RPC boundary. It
replaces the earlier Tauri v2 prototype that previously occupied this repo.
Licensed under the Apache License 2.0. See NOTICE for attribution and lineage details.
Jobs that ask for a free standard GitHub runner (ubuntu-24.04, ubuntu-latest)
always run on GitHub: the repository is public, so those runners cost nothing.
Jobs that ask for a paid size go through runner-route.yml (the same probe as
intent and intentd; intent-pr.yml's route job is an inline copy). GitHub's
larger runners are off: a paid size runs on a self-hosted slot or WarpBuild,
never on GitHub.
- Linux
gh-linux-8xjobs take a free intent-cache slot first, then burst to WarpBuildwarp-ubuntu-2404-x64-8x;gh-linux-arm64-8xtakes a self-hosted ARM64 runner first, thenwarp-ubuntu-2404-arm64-8x. - Integration Tests and the Playwright (root) and Electron browser suites run
on the free
ubuntu-24.04runner: they use about 2 of 8 CPUs. - Component Tests take a free intent-cache slot first, else the
cloudburst runner forgh-linux-8x. - Every Mac and Windows build reads the route's
cloudoutput: straight to WarpBuild, never a self-hosted runner. Windows builds usewarp-windows-2022-x64-8x(gh-windows-8x: they use about a third of 16 CPUs; Server 2022, because node-gyp fails on 2025), Apple Silicon jobswarp-macos-26-arm64-12x, and signed desktop Mac buildswarp-macos-15-arm64-12x(electron-builder's keychain step fails on macOS 26). - Build smoke and the manual packaged Mac smoke and isolated-package check need
a fresh VM for the job: they run on WarpBuild through
cloud, and their scripts accept GitHub-hosted or WarpBuild VMs but refuse an intent-cache slot. Only the one-use B76 diagnostics keep their frozen GitHub labels.
A job that falls back because the route job failed uses the WarpBuild label
(scripts/test_runner_policy.py rejects a paid GitHub label in any runs-on).
Fork pull requests run paid sizes on the free standard runner of the same OS
instead of WarpBuild. A WarpBuild platform with a job queued over 10 minutes is
reported as a warning; bursts still go to WarpBuild.
Mac desktop release and manual packaging builds target ARM64 only.
From-source Rust sidecars use the self-hosted sccache without Rust cache actions.
The setup script installs a pinned, checksum-verified binary. When the
INTENT_SCCACHE_ENDPOINT Actions variable is set, it uses the self-hosted
cache with the INTENT_SCCACHE_TOKEN secret (read-write unless
INTENT_SCCACHE_MODE says otherwise). Without the variable or a token (for
example fork pull requests), compilation proceeds uncached with a notice.
pnpm dependency caching remains unchanged.