Files
radicle-explorer/desktop/AGENTS.md
T
Brandon OxendineandRūdolfs Ošiņš 3f32a750b8 desktop: Add the board COB
A board arranges a repository's issues and patches in columns and stores
only where each card sits, as `works.monadic.board` in its own crate so
other clients can reuse it. Moves follow the issue and patch access
rules, ops that fail them are ignored rather than rejected, and positions
are fractional keys so concurrent moves converge. ADR 0005 records the
decisions.

Co-authored-by: Rūdolfs Ošiņš <rudolfs@osins.org>
2026-11-06 18:21:33 +01:00

6.5 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.

Boards

Read docs/adr/0005-boards.md before working on boards.

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)