Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add CLAUDE.md
Rūdolfs Ošiņš committed 1 month ago
commit a6af849959b41984cd17e75e4ef52da4c42b6191
parent 0e38df1
1 file changed +279 -0
added CLAUDE.md
@@ -0,0 +1,279 @@
+
# 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/`).
+

+
## Plan mode
+

+
- Propose the minimal MVP first — no tests, no docs, no speculative extras
+
- Tests are added only when explicitly requested after MVP review
+
- Keep plans concise: short bullet points listing what changes and where
+
- Skip obvious details and boilerplate explanations
+

+
## Tech stack
+

+
- Svelte 5 with runes syntax (`$state`, `$derived`, `$props`, `$effect`)
+
- TypeScript, Vite, Vitest, Playwright
+
- Rust backend: Tauri commands in `crates/radicle-tauri/`
+
- Types generated by ts-rs: `crates/radicle-types/` → `crates/radicle-types/bindings/`
+
- Path aliases: `@app` (src), `@bindings` (crates/radicle-types/bindings), `@tests`
+

+
## Commands
+

+
### Development
+

+
```sh
+
npm run tauri dev       # full Tauri dev build with hot reload
+
npm run start:http      # Vite dev server backed by test-http-api (no Tauri runtime)
+
```
+

+
### Checks and linting (tsc, svelte-check, eslint, prettier)
+

+
```sh
+
npm run check
+
npm run format          # auto-fix prettier issues
+
```
+

+
### Unit tests
+

+
```sh
+
npm run test:unit
+
```
+

+
### E2E tests
+

+
```sh
+
npm run test:e2e -- --project webkit
+
npm run test:e2e -- --project webkit tests/e2e/<file>.spec.ts
+
```
+

+
`SKIP_SETUP=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.
+

+
```sh
+
SKIP_SETUP=true npm run test:e2e -- --project webkit
+
```
+

+
### Rust backend (`crates/`)
+

+
```sh
+
scripts/check-rs    # cargo fmt --check, clippy --workspace, check, test
+
```
+

+
### Regenerate TypeScript bindings
+

+
Run after changing any type annotated with `#[derive(TS)]` in `crates/radicle-types/`:
+

+
```sh
+
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
+
5. `src/` — call via `invoke<T>("command_name", args)`, import types from `@bindings`
+

+
### Pre-push checklist
+

+
These mirror what CI runs on every PR. Run them all before shipping a feature.
+

+
```sh
+
npm run check                            # tsc, svelte-check, eslint, prettier,
+
                                         # cargo fmt, cargo clippy, cargo test
+
npm run test:unit
+
npm run test:e2e -- --project webkit
+
```
+

+
If backend types changed, regenerate and commit the bindings first:
+

+
```sh
+
npm run generate-types
+
```
+

+
**E2E prerequisite** — run once after cloning or when Radicle binaries are updated:
+

+
```sh
+
./scripts/install-binaries
+
npm run build:http
+
```
+

+
## Backend architecture
+

+
The Rust backend follows a hexagonal (ports & adapters) architecture split across
+
three crates:
+

+
```
+
crates/
+
├── radicle-types/     # domain types, ports (traits), adapters, ts-rs bindings
+
├── radicle-tauri/     # Tauri driver — thin IPC wrappers around port methods
+
└── test-http-api/     # Axum HTTP driver — same ports, used by E2E tests
+
```
+

+
Both driver crates depend on `radicle-types` and share no logic between them.
+

+
### radicle-types — ports and adapters
+

+
**Ports** are Rust traits in `src/traits/`. Each trait has a full default
+
implementation that calls `self.profile()` to access the Radicle SDK directly.
+
`AppState` (the single concrete type shared by drivers) holds a
+
`radicle::Profile` and derives all behaviour from these default implementations:
+

+
```rust
+
pub struct AppState { pub profile: radicle::Profile }
+
impl Repo for AppState {}    // all methods come from the trait defaults
+
impl Issues for AppState {}
+
impl Patches for AppState {}
+
// ...
+
```
+

+
Current ports: `Profile`, `Repo`, `Cobs`, `Thread`, `Issues`, `IssuesMut`,
+
`Patches`, `PatchesMut`, `Jobs`.
+

+
**Domain modules** in `src/domain/` go one step further: they define their own
+
storage port + a generic `Service<T>` that wraps it, keeping domain logic
+
separate from the Radicle SDK. Currently implemented:
+

+
- `domain/inbox/` — `InboxStorage` port + `Service<I: InboxStorage>`
+
- `domain/patch/` — `PatchStorage` port + `Service<I: PatchStorage>`
+

+
**Outbound adapter** in `src/outbound/sqlite.rs`: a single `Sqlite` struct that
+
implements both `InboxStorage` and `PatchStorage` by querying the Radicle SQLite
+
COB cache and notifications DB (read-only, thread-safe).
+

+
### radicle-tauri — Tauri driver
+

+
Commands in `src/commands/` are thin wrappers: they receive
+
`tauri::State<AppState>`, call the matching port method (e.g. `ctx.list_repos()`),
+
and return the result. No business logic lives here.
+

+
Note: the domain `Service` types (`InboxService`, `PatchService`) are instantiated
+
in `startup.rs` but not yet wired into `AppState` — the Tauri commands currently
+
reach the same data through the trait default implementations. Completing this
+
wiring is future work.
+

+
### test-http-api — HTTP driver
+

+
Mirrors `radicle-tauri` as an Axum HTTP server. Used by Playwright E2E tests
+
(and `npm run start:http`) so the frontend can run without the Tauri runtime.
+
`Context` implements all the same port traits as `AppState`.
+

+
## Radicle ecosystem (sibling repos)
+

+
`crates/radicle-tauri` depends on crates from sibling repositories. Read
+
source from these paths when working on Rust code.
+

+
### heartwood (`../heartwood`)
+

+
Core Radicle protocol implementation. Key crates used by radicle-tauri:
+
- `radicle` — standard library (storage, identity, COBs, git, node)
+
- `radicle-cob` — collaborative objects (issues, patches as CRDTs)
+
- `radicle-crypto` — Ed25519 signing, SSH key handling
+
- `radicle-core` — fundamental types (`RepoId`, etc.)
+

+
Key files:
+
- `../heartwood/HACKING.md` — development guide, environment variables
+
- `../heartwood/ARCHITECTURE.md` — high-level architecture
+
- `../heartwood/crates/radicle/src/lib.rs` — main library entry point
+

+
### radicle-git (`../radicle-git`)
+

+
Git library wrappers. Key crate:
+
- `radicle-surf` — code browsing (files, diffs, commits, branches,
+
  tags). This is what radicle-tauri uses to serve repository content.
+

+
Key file: `../radicle-git/radicle-surf/src/lib.rs`
+

+
### radicle-job (`../radicle-job`)
+

+
Decentralized job execution (CI/CD) on the Radicle network.
+
Key file: `../radicle-job/README.md`
+

+
### RIPs — protocol specs (`../rips`)
+

+
- `../rips/0001-heartwood.md` — protocol overview
+
- `../rips/0002-identity.md` — identity system (DIDs, Ed25519)
+
- `../rips/0003-storage-layout.md` — git storage layout
+

+
### Radicle documentation (`../radicle.xyz`)
+

+
Read these when you need domain context for UI work:
+
- `../radicle.xyz/_guides/user.md` — end-to-end user workflows
+
  (init, clone, seed, issues, patches, code review, private repos)
+
- `../radicle.xyz/_guides/protocol.md` — protocol internals
+
  (gossip, replication, identity documents, COB data model, trust)
+
- `../radicle.xyz/_guides/seeder.md` — seed node operation
+
  (seeding policies, httpd setup, DNS-SD)
+
- `../radicle.xyz/_posts/2025-07-23-using-radicle-ci-for-development.md` — CI integration
+
- `../radicle.xyz/_posts/2025-08-12-canonical-references.md` — canonical refs design
+

+
## Domain glossary
+

+
- **RID** — Repository ID (`rad:z3gqc...`)
+
- **DID** — Decentralized Identifier, user identity (`did:key:z6Mk...`)
+
- **NID** — Node ID, public key suffix of a DID
+
- **COB** — Collaborative Object (issue, patch, or identity as a Git DAG)
+
- **Delegate** — authorized repo maintainer; signatures determine canonical state
+
- **Patch** — pull-request equivalent with immutable revisions and reviews
+
- **Seed** — hosting/replicating a repo; seed nodes are always-on servers
+
- **Canonical refs** — branches/tags resolved by delegate quorum
+

+
## Code conventions
+

+
- Prefer `undefined` over `null`
+
- Do not add comments unless explicitly asked. When writing comments,
+
  use proper English sentences
+
- Ask before adding new dependencies
+

+
### Svelte components
+

+
- Script order (enforced by prettier): `<script lang="ts" module>`,
+
  `<script lang="ts">`, `<style>`, markup
+
- Props use `$props()`: `const { foo, bar = undefined }: Props = $props()`
+
- CSS: scoped styles with design tokens (`var(--color-text-primary)`,
+
  `var(--txt-body-m-regular)`, `var(--border-radius-sm)`); use `:global()`
+
  for styling slotted or `{@html}` content
+
- Loading states: `{#await promise}` blocks for inline async;
+
  local `loading` boolean with `try/catch` for imperative fetches
+

+
### TypeScript
+

+
- Import backend types from `@bindings/*` — these are generated by ts-rs, never
+
  hand-write interfaces that duplicate them
+
- Use ES private fields (`#field`), not the TypeScript `private` keyword
+
- Call Tauri commands via `invoke<T>("command_name", { arg })` from `@app/lib/invoke`
+

+
### Rust backend
+

+
- Business logic belongs in trait default implementations (`crates/radicle-types/src/traits/`),
+
  not in command handlers
+
- Tauri command signature: `pub fn cmd(ctx: tauri::State<AppState>, ...) -> Result<T, Error>`
+
- Commands are registered in `crates/radicle-tauri/src/lib.rs` via `tauri::generate_handler![...]`
+
- Mirror every new command in `crates/test-http-api/src/` so E2E tests continue to work
+
- 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`
+

+
## Commit messages
+

+
- Imperative mood: "Add feature" not "Added feature"
+
- Capitalize subject, no trailing period, max 50 chars
+

+
## Do NOT
+

+
- Do not use `npm test` — no default test script exists
+
- Do not use `yarn` or `pnpm` — use npm
+
- Do not use `npx vitest` or `npx playwright test` — use the `npm run` scripts
+
- Do not mix legacy Svelte syntax (`export let`, `$:`) with runes in the same component