Files
radicle-explorer/desktop/AGENTS.md
T

6.4 KiB

radicle-desktop

Tauri desktop app for Radicle. Svelte 5 frontend communicates with a Rust backend via Tauri IPC commands. TypeScript types are generated from Rust structs using ts-rs (crates/radicle-types/).

The app lives in desktop/ of the radicle-gui monorepo. It is a package in the root npm workspace and its own Cargo workspace: install npm dependencies at the repository root with npm ci -w desktop, and run every other command here from desktop/. All paths in this file are relative to it. Conventions shared with explorer are in the root AGENTS.md, and UI code shared with it lives in shared/ui/.

Svelte syntax and aliases

  • Svelte 5 with runes syntax ($state, $derived, $props, $effect)
  • Path aliases: @app (src), @bindings (crates/radicle-types/bindings), @tests

Commands

Development

npm run dev             # full Tauri dev build with hot reload and devtools
npm run start:http      # test-http-api backend on :8081 (no Tauri runtime)
npm start               # Vite dev server; in a browser it talks to test-http-api

Set VITE_LOG_INVOKE_TIMINGS=true to log each command's duration ([invoke] <command> <ms>ms) to the browser console. It is off by default.

Checks and linting

npm run check           # svelte-check, eslint, prettier, then scripts/check-rs
npm run format          # auto-fix prettier issues

Unit tests

npm run test:unit

DOMPurify does not sanitize under happy-dom (the unit-test environment): <script> and onerror= pass through, and setConfig allowlists are ignored. Don't assert sanitization in tests/unit/; verify it in the real app or in an E2E test.

E2E tests

npm run test:e2e
npm run test:e2e -- tests/e2e/<file>.spec.ts

Run ./scripts/install-binaries once after cloning and whenever the Radicle binaries are updated.

Peer keys have no passphrase, so the app authenticates without an ssh-agent. Tests that need a running node use the peer fixture; tests that need an ssh-agent use the sshAuthSock fixture and create a peer with a passphrase. When a fixture depends on one peer seeing another's changes, wait for the node event with waitForEvent instead of sleeping. Reload pages with reload(page) from @tests/support/fixtures.js, not page.reload(), and navigate a page that already shows the app with goto(page, url): the app polls the backend, and the harness would otherwise fail the test on the request the navigation cancels.

Tests run against a production build in tests/tmp/build, served by vite preview; the build takes a few seconds at startup. Set E2E_DEV_SERVER=1 to run against the Vite dev server instead, e.g. for hot reload while writing tests.

Each run writes tests/artifacts/results.json (Playwright's JSON report) and per-test logs in tests/artifacts/<test>/. Read those to see what failed in a run someone else started.

SKIP_FIXTURE_CREATION=true skips fixture creation for faster iteration. Only use it when you are solely editing .spec.ts files and fixtures already exist from a previous full run. Any change to app code, the Rust backend, or test fixtures requires a full run.

SKIP_FIXTURE_CREATION=true npm run test:e2e

Rust backend (crates/)

scripts/check-rs    # cargo fmt --check, clippy on all targets, test

Regenerate TypeScript bindings

Run after changing any type annotated with #[derive(TS)] in crates/radicle-types/, and commit the regenerated bindings:

npm run generate-types

Changes across layers

Adding or changing a Tauri command touches these layers in order:

  1. crates/radicle-types/src/ — add/update Rust types with #[derive(TS)] and #[ts(export)], run npm run generate-types to update @bindings
  2. crates/radicle-types/src/traits/ — add the method to the relevant port trait; default implementation goes here, not in the command handler
  3. crates/radicle-tauri/src/commands/ — add the #[tauri::command] handler (thin wrapper: ctx.method()), register in crates/radicle-tauri/src/lib.rs
  4. crates/test-http-api/src/ — mirror the same route so E2E tests keep working; cargo test -p test-http-api --test parity fails until you do. A command that can't work over HTTP goes in TAURI_ONLY in crates/test-http-api/tests/parity.rs, with the reason
  5. src/ — call via invoke<T>("command_name", args), import types from @bindings

Tests for a feature

When adding tests for a feature, go through docs/testing-checklist.md and report which items are covered and which don't apply. The testing strategy is in docs/adr/0001-testing-strategy.md.

Pre-push checklist

npm run check:ci runs everything CI runs on every PR (checks, unit tests, install-binaries, E2E tests) and stops at the first failure. It writes the full output, with each step's duration and exit code, to tests/tmp/check-ci.log.

Backend architecture

Three crates in a hexagonal architecture; the reasoning is in docs/adr/0002-backend-architecture.md.

  • radicle-types — domain types, ports (src/traits/), domain services (src/domain/), the SQLite adapter (src/outbound/), ts-rs bindings
  • radicle-tauri — Tauri driver; AppState implements the ports
  • test-http-api — HTTP driver for E2E tests and npm run start:http; Context implements the same ports

Ports have full default implementations, so a driver only implements Profile. Paginated list views (issues, patches, notifications) go through a domain Service backed by the SQLite adapter instead of the ports; new lists over large data sets should do the same.

Code conventions

Svelte components

  • Props use $props(): const { foo, bar = undefined }: Props = $props()

TypeScript

  • Import backend types from @bindings/* — these are generated by ts-rs, never hand-write interfaces that duplicate them
  • Call Tauri commands via invoke<T>("command_name", { arg }) from @app/lib/invoke

Rust backend

  • All serialized types live in crates/radicle-types/src/; annotate new types with #[derive(Serialize, TS)], #[serde(rename_all = "camelCase")], #[ts(export)], and #[ts(export_to = "<dir>/")]
  • Optional fields: #[serde(skip_serializing_if = "Option::is_none")]
  • Error type: radicle_types::error::Error
  • rust-toolchain.toml tracks heartwood's. When bumping the radicle crate, check heartwood's toolchain too: an older rustc fails inside the dependency with a confusing error (e.g. E0382 in cob/identity.rs for radicle 0.25)