| Dimension | Shared | Only in A | Only in B | Overlap |
|---|---|---|---|---|
| Sections | 0 | 1 | 13 | 0% |
| Commands | 0 | 2 | 5 | 0% |
| Section tags | 0 | 1 | 6 | 0% |
What each file covers
Sections
0 shared · 1 only in A · 13 only in B- − CLAUDE.md
- + AGENTS.md
- + Repository layout
- + Flutter version
- + Commands
- + Core crate features
- + Architecture
- + State management
- + Isolates
- + Networking (Rust)
- + Multicast discovery (Rust)
- + i18n
- + FOSS build
- + Release notes
Commands
0 shared · 2 only in A · 5 only in B- − flutter
- − dart
- + cargo test --features full
- + cargo clippy --features full
- + cargo check
- + flutter pub get
- + cargo build
Section tags
0 shared · 1 only in A · 6 only in B- − agent-behaviour
- + build
- + test
- + lint-format
- + code-style
- + architecture
- + deployment
Line diff
localsend/localsend · CLAUDE.md
@@ −1 @@
1# CLAUDE.md
2
3Always use **`fvm flutter`** / **`fvm dart`**, never the bare `flutter` / `dart` binaries — this repo pins its
4Flutter version in `.fvmrc` and the system-wide toolchain will not match it.
5
6Full guidance for this repository lives in AGENTS.md:
7
8@AGENTS.md
9
localsend/localsend · AGENTS.md
@@ +1 @@
1# AGENTS.md
2
3LocalSend disallows AI generated contributions unless:
4
5- they are bug fixes or
6- very small or
7- you prove your expertise in your field
8
9This file provides guidance to LLMs when working with code in this repository.
10
11## Repository layout
12
13This is a multi-language monorepo: a Flutter app on top of a Rust protocol implementation.
14
15| Path | What it is |
16|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
17| `app/` | The Flutter app (`localsend_app`). UI, providers, persistence, platform channels. |
18| `packages/localsend_isolates/` | Dart isolate layer + `flutter_rust_bridge` (FRB) bindings. Owns `rust/` (the Flutter plugin crate `rust_lib_localsend_app`) and `rust_builder/` (cargokit). |
19| `packages/core/` | Rust crate `localsend`: protocol, HTTP server/client, crypto, WebRTC. No Flutter dependency. |
20| `packages/typed_isolates/` | Small standalone package wrapping Dart `Isolate` with typed send/receive channels. |
21| `server/` | Axum WebSocket signaling server for WebRTC (`/v1/ws`). Deployed separately, see `server/Dockerfile`. |
22| `cli/` | Rust CLI crate (`localsend-cli`): interactive terminal client on top of `packages/core` (v2 HTTP + multicast). |
23| `support/scripts/` | Release/packaging scripts (per-platform builds, MSIX, Inno Setup, FOSS stripping). |
24
25The four Rust crates (`packages/core`, `packages/localsend_isolates/rust`, `server`, `cli`) form a single Cargo workspace rooted at the repository root: one shared `Cargo.lock` and `target/`, and `[profile.*]` settings live only in the root `Cargo.toml` (member profiles would be ignored). Cargokit still builds the plugin crate into its own target dir during Flutter builds.
26
27Dependency direction: `app` → `localsend_isolates` → (`typed_isolates`, `rust_lib_localsend_app` → `localsend` core).
28The app depends on **only** `localsend_isolates` — not on `flutter_rust_bridge`, `typed_isolates`, or the plugin crate directly.
29
30## Flutter version
31
32Pinned to the version in `.fvmrc` (also mirrored in `.github/workflows/ci.yml` and `app/pubspec.yaml`, plus the `support/submodules/flutter` git submodule). Use **`fvm flutter` / `fvm dart`** instead of the system-wide toolchain.
33Bumping the version means updating all four places — see the "Bump Flutter" section of `CONTRIBUTING.md`.
34
35## Commands
36
37Run from `app/` unless stated otherwise.
38
39```bash
40fvm flutter pub get
41fvm dart run build_runner build # dart_mappable, freezed, flutter_gen, mockito
42fvm dart run slang # i18n codegen (slang_build_runner is disabled in build.yaml)
43fvm flutter run
44```
45
46Checks (what CI runs):
47
48```bash
49fvm dart format --set-exit-if-changed lib test # CI deletes lib/gen first; generated code is not format-checked
50fvm flutter analyze
51fvm flutter test
52fvm flutter test test/unit/util/security_helper_test.dart # single file
53fvm flutter test --plain-name 'some test name' # single test
54```
55
56Formatting is **150 columns** (`page_width: 150` in `analysis_options.yaml`, `trailing_commas: preserve`). Any tool that reformats generated Dart at 80 columns creates pure noise — reformat with `fvm dart format` afterwards.
57
58Rust:
59
60```bash
61cargo test --features full # in packages/core — see "Core crate features" below
62cargo clippy --features full
63cargo check # in packages/localsend_isolates/rust, server, cli
64```
65
66FRB codegen — run from `packages/localsend_isolates/`:
67
68```bash
69flutter_rust_bridge_codegen generate # config in flutter_rust_bridge.yaml (dart_format_line_length: 150)
70```
71
72Codegen has a habit of rewriting `app/test/mocks.mocks.dart` at 80 columns; revert that file if it shows up in the diff.
73
74`packages/localsend_isolates` has its own `build.yaml`/`pubspec.yaml` and needs its own `pub get` + `build_runner` run when its models change. CI additionally runs `flutter pub get` in `packages/localsend_isolates/rust_builder/cargokit/build_tool`.
75
76## Core crate features
77
78`packages/core` gates almost everything behind Cargo features (`crypto`, `http`, `multicast`, `webrtc`, `webrtc-signaling`, `full`), and `default = []`. **Always build and test it with `--features full`.** A bare `cargo check`/`cargo build` fails because modules are declared unconditionally while their dependencies are optional — that is pre-existing and expected, not a regression.
79
80## Architecture
81
82### State management
83
84Refena (`refena_flutter`), not Riverpod. Providers live in `app/lib/provider/`; `NotifierProvider` for plain state, `ReduxProvider` + dispatched action classes for anything the isolate layer touches. `app/lib/config/init.dart` (`preInit`) is the bootstrap: it initialises logging, `RustLib.init()`, persistence, the isolate container, tray/window, and returns the `RefenaContainer` that `main.dart` mounts.
85
86Models are `dart_mappable` (`@MappableClass`, `.mapper.dart` parts) with renamed methods — `fromJson`/`toJson` are the **Map** converters and `deserialize`/`serialize` are the string ones (configured in both `build.yaml` files). Freezed is used for FRB-adjacent unions.
87
88### Isolates
89
90The heavy networking never runs on the main isolate. `packages/localsend_isolates/lib/src/isolate/`:
91
92- `parent/parent_isolate_provider.dart` — `ParentIsolateState` holds one `IsolateConnector` per child (http scan discovery, multicast discovery, http upload, http server) plus a `SyncState` mirrored into every child. `IsolateSetupAction` spawns them.
93- `parent/actions.dart`, `parent/actions_sync.dart` — the only supported way for the app to talk to the children.
94- `child/*_isolate.dart` — child entry points; they translate typed task messages into calls on `lib/src/task/`.
95- `lib/src/task/` — pure helpers only; **isolate logic is prohibited there** (see its `README.md`).
96
97State that children need (alias, port, protocol, whether the server runs, whether web send is on) is pushed via `IsolateSyncServerStateAction`. Children read `syncState` at start, so sync **before** starting the server.
98
99### Networking (Rust)
100
101The HTTP server and client are Rust, not Dart. `packages/core/src/http/server/` implements protocol v2 (v1 endpoints are not served) plus the "web send" download flow (`server/web.rs`) and an internal `show` endpoint used to foreground an already-running instance.
102
103Integration is channel-based: `start_with_port` takes a `ServerConfigV2 { pin, event_tx, web_send }` and emits `ServerEventV2` events (`Register`, `PrepareUpload` with a `decision_tx` oneshot, `FileUpload` with a byte stream + `result_tx`, `PrepareDownload`, `SessionEnd`, `PrepareUploadAborted`, `CancelReceived`). Only **one upload session is active at a time**; cancellation safety comes from drop guards (`PendingSessionGuard`, `UploadGuard`, `PendingWebSessionGuard`). There is deliberately no `auto_accept` in core — the app auto-accepts by answering `decision_tx` immediately. New server→app interactions should extend `ServerEventV2` rather than adding side channels.
104
105The FRB layer (`packages/localsend_isolates/rust/src/api/server.rs`) exposes `start_server` + an opaque `RsHttpServer` whose `listen` merges the v2, web-send and internal channels into one `RsServerEvent` stream; responder oneshots stay on the Rust side. On the Dart side `child/server_isolate.dart` turns those into `HttpServerEvent`s, which `app/lib/provider/network/server/server_provider.dart` routes to `ReceiveController` / `SendController` — these are **event handlers, not route handlers**.
106
107Save targets are decided in Dart (`prepareFileSaveTarget`) and written by Rust: a path, or an Android SAF file descriptor obtained through the `org.localsend.localsend_app/localsend` method channel. Gallery saves go through a cache file first.
108
109Server event `ip`s are `PeerIp` (IP + IPv6 scope): a link-local peer renders as `fe80::1%3`, which the HTTP client accepts back as a host, so event ips stay dialable.
110
111TLS uses per-device on-the-fly certificates with **mandatory client certificates** (optional while the web pages are served, so browsers can connect); the peer identity is the uppercase-hex SHA-256 of the client cert DER, and `Register` is simply not emitted when a payload's claimed fingerprint disagrees with the cert. Prefer `event.certFingerprint ?? event.info.fingerprint` — the payload fallback only exists for encryption-off mode.
112
113Both the receive pin and the web-send pin are fixed at server start, so changing either restarts the server.
114
115Web assets for the browser download page are embedded from `packages/core/assets/web/`.
116
117### Multicast discovery (Rust)
118
119`packages/core/src/multicast/` (feature `multicast`, independent of `http`) implements UDP multicast discovery for protocol v2.1 — v1 messages are not parsed.
120Integration mirrors the HTTP server: `multicast::start` takes a `MulticastConfig { group, group_v6, port, interface_filter, device, event_tx }` and emits `MulticastEvent::Discovered { ip, message }`; the returned `MulticastHandle` offers `announce` (the announcement burst) and `wait_stopped`.
121
122UDP is **announce-only**: responses go back over HTTP as a unicast register request to the announcing device.
123
124One socket is bound per interface IPv4 address (`SO_REUSEPORT`/`SO_REUSEADDR` + `IP_MULTICAST_IF`), because a single socket only sends on one interface. Multicast loopback stays on so that instances on one host see each other; own messages are dropped by fingerprint. IPv6 is a LocalSend extension (group `ff12::fd3a:e420`, `DEFAULT_MULTICAST_GROUP_V6`), enabled by setting `group_v6`: one `IPV6_V6ONLY` socket per interface, joined by interface index. `Discovered` carries the source's scope ID (interface index), which link-local IPv6 sources need for the HTTP answer.
125
126### i18n
127
128Slang, source files in `app/assets/i18n/` (`<locale>.json` plus `_missing_translations_<locale>.json`), generated output in `app/lib/gen/`. Translations are managed on Weblate; fields prefixed with `@` are metadata for translators and are not used by the app. `app/test/unit/i18n_test.dart` guards the locale set.
129
130### FOSS build
131
132`in_app_purchase` and the donation UI are stripped for F-Droid by `support/scripts/remove_proprietary_dependencies.sh`, which relies on the `# [FOSS_REMOVE]` pubspec marker and `// [FOSS_REMOVE_START]` / `// [FOSS_REMOVE_END]` comment pairs. Preserve those markers when editing `lib/config/init.dart`, `lib/pages/donation/*`, or `lib/provider/purchase_provider.dart`.
133
134## Release notes
135
136`app/pubspec.yaml`'s version must match `#define MyAppVersion` in `support/scripts/compile_windows_exe-inno.iss` and the `version` in `cli/Cargo.toml` (the CLI prints it in its start banner) — CI fails on a mismatch. Platform build commands and release steps are documented in `README.md` ("Building") and `CONTRIBUTING.md` ("Release").
@@ −1 +1 @@
1−# CLAUDE.md
1+# AGENTS.md
22
3−Always use **`fvm flutter`** / **`fvm dart`**, never the bare `flutter` / `dart` binaries — this repo pins its
4−Flutter version in `.fvmrc` and the system-wide toolchain will not match it.
3+LocalSend disallows AI generated contributions unless:
54
6−Full guidance for this repository lives in AGENTS.md:
5+- they are bug fixes or
6+- very small or
7+- you prove your expertise in your field
78
8−@AGENTS.md
9+This file provides guidance to LLMs when working with code in this repository.
910
11+## Repository layout
12+
13+This is a multi-language monorepo: a Flutter app on top of a Rust protocol implementation.
14+
15+| Path | What it is |
16+|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
17+| `app/` | The Flutter app (`localsend_app`). UI, providers, persistence, platform channels. |
18+| `packages/localsend_isolates/` | Dart isolate layer + `flutter_rust_bridge` (FRB) bindings. Owns `rust/` (the Flutter plugin crate `rust_lib_localsend_app`) and `rust_builder/` (cargokit). |
19+| `packages/core/` | Rust crate `localsend`: protocol, HTTP server/client, crypto, WebRTC. No Flutter dependency. |
20+| `packages/typed_isolates/` | Small standalone package wrapping Dart `Isolate` with typed send/receive channels. |
21+| `server/` | Axum WebSocket signaling server for WebRTC (`/v1/ws`). Deployed separately, see `server/Dockerfile`. |
22+| `cli/` | Rust CLI crate (`localsend-cli`): interactive terminal client on top of `packages/core` (v2 HTTP + multicast). |
23+| `support/scripts/` | Release/packaging scripts (per-platform builds, MSIX, Inno Setup, FOSS stripping). |
24+
25+The four Rust crates (`packages/core`, `packages/localsend_isolates/rust`, `server`, `cli`) form a single Cargo workspace rooted at the repository root: one shared `Cargo.lock` and `target/`, and `[profile.*]` settings live only in the root `Cargo.toml` (member profiles would be ignored). Cargokit still builds the plugin crate into its own target dir during Flutter builds.
26+
27+Dependency direction: `app` → `localsend_isolates` → (`typed_isolates`, `rust_lib_localsend_app` → `localsend` core).
28+The app depends on **only** `localsend_isolates` — not on `flutter_rust_bridge`, `typed_isolates`, or the plugin crate directly.
29+
30+## Flutter version
31+
32+Pinned to the version in `.fvmrc` (also mirrored in `.github/workflows/ci.yml` and `app/pubspec.yaml`, plus the `support/submodules/flutter` git submodule). Use **`fvm flutter` / `fvm dart`** instead of the system-wide toolchain.
33+Bumping the version means updating all four places — see the "Bump Flutter" section of `CONTRIBUTING.md`.
34+
35+## Commands
36+
37+Run from `app/` unless stated otherwise.
38+
39+```bash
40+fvm flutter pub get
41+fvm dart run build_runner build # dart_mappable, freezed, flutter_gen, mockito
42+fvm dart run slang # i18n codegen (slang_build_runner is disabled in build.yaml)
43+fvm flutter run
44+```
45+
46+Checks (what CI runs):
47+
48+```bash
49+fvm dart format --set-exit-if-changed lib test # CI deletes lib/gen first; generated code is not format-checked
50+fvm flutter analyze
51+fvm flutter test
52+fvm flutter test test/unit/util/security_helper_test.dart # single file
53+fvm flutter test --plain-name 'some test name' # single test
54+```
55+
56+Formatting is **150 columns** (`page_width: 150` in `analysis_options.yaml`, `trailing_commas: preserve`). Any tool that reformats generated Dart at 80 columns creates pure noise — reformat with `fvm dart format` afterwards.
57+
58+Rust:
59+
60+```bash
61+cargo test --features full # in packages/core — see "Core crate features" below
62+cargo clippy --features full
63+cargo check # in packages/localsend_isolates/rust, server, cli
64+```
65+
66+FRB codegen — run from `packages/localsend_isolates/`:
67+
68+```bash
69+flutter_rust_bridge_codegen generate # config in flutter_rust_bridge.yaml (dart_format_line_length: 150)
70+```
71+
72+Codegen has a habit of rewriting `app/test/mocks.mocks.dart` at 80 columns; revert that file if it shows up in the diff.
73+
74+`packages/localsend_isolates` has its own `build.yaml`/`pubspec.yaml` and needs its own `pub get` + `build_runner` run when its models change. CI additionally runs `flutter pub get` in `packages/localsend_isolates/rust_builder/cargokit/build_tool`.
75+
76+## Core crate features
77+
78+`packages/core` gates almost everything behind Cargo features (`crypto`, `http`, `multicast`, `webrtc`, `webrtc-signaling`, `full`), and `default = []`. **Always build and test it with `--features full`.** A bare `cargo check`/`cargo build` fails because modules are declared unconditionally while their dependencies are optional — that is pre-existing and expected, not a regression.
79+
80+## Architecture
81+
82+### State management
83+
84+Refena (`refena_flutter`), not Riverpod. Providers live in `app/lib/provider/`; `NotifierProvider` for plain state, `ReduxProvider` + dispatched action classes for anything the isolate layer touches. `app/lib/config/init.dart` (`preInit`) is the bootstrap: it initialises logging, `RustLib.init()`, persistence, the isolate container, tray/window, and returns the `RefenaContainer` that `main.dart` mounts.
85+
86+Models are `dart_mappable` (`@MappableClass`, `.mapper.dart` parts) with renamed methods — `fromJson`/`toJson` are the **Map** converters and `deserialize`/`serialize` are the string ones (configured in both `build.yaml` files). Freezed is used for FRB-adjacent unions.
87+
88+### Isolates
89+
90+The heavy networking never runs on the main isolate. `packages/localsend_isolates/lib/src/isolate/`:
91+
92+- `parent/parent_isolate_provider.dart` — `ParentIsolateState` holds one `IsolateConnector` per child (http scan discovery, multicast discovery, http upload, http server) plus a `SyncState` mirrored into every child. `IsolateSetupAction` spawns them.
93+- `parent/actions.dart`, `parent/actions_sync.dart` — the only supported way for the app to talk to the children.
94+- `child/*_isolate.dart` — child entry points; they translate typed task messages into calls on `lib/src/task/`.
95+- `lib/src/task/` — pure helpers only; **isolate logic is prohibited there** (see its `README.md`).
96+
97+State that children need (alias, port, protocol, whether the server runs, whether web send is on) is pushed via `IsolateSyncServerStateAction`. Children read `syncState` at start, so sync **before** starting the server.
98+
99+### Networking (Rust)
100+
101+The HTTP server and client are Rust, not Dart. `packages/core/src/http/server/` implements protocol v2 (v1 endpoints are not served) plus the "web send" download flow (`server/web.rs`) and an internal `show` endpoint used to foreground an already-running instance.
102+
103+Integration is channel-based: `start_with_port` takes a `ServerConfigV2 { pin, event_tx, web_send }` and emits `ServerEventV2` events (`Register`, `PrepareUpload` with a `decision_tx` oneshot, `FileUpload` with a byte stream + `result_tx`, `PrepareDownload`, `SessionEnd`, `PrepareUploadAborted`, `CancelReceived`). Only **one upload session is active at a time**; cancellation safety comes from drop guards (`PendingSessionGuard`, `UploadGuard`, `PendingWebSessionGuard`). There is deliberately no `auto_accept` in core — the app auto-accepts by answering `decision_tx` immediately. New server→app interactions should extend `ServerEventV2` rather than adding side channels.
104+
105+The FRB layer (`packages/localsend_isolates/rust/src/api/server.rs`) exposes `start_server` + an opaque `RsHttpServer` whose `listen` merges the v2, web-send and internal channels into one `RsServerEvent` stream; responder oneshots stay on the Rust side. On the Dart side `child/server_isolate.dart` turns those into `HttpServerEvent`s, which `app/lib/provider/network/server/server_provider.dart` routes to `ReceiveController` / `SendController` — these are **event handlers, not route handlers**.
106+
107+Save targets are decided in Dart (`prepareFileSaveTarget`) and written by Rust: a path, or an Android SAF file descriptor obtained through the `org.localsend.localsend_app/localsend` method channel. Gallery saves go through a cache file first.
108+
109+Server event `ip`s are `PeerIp` (IP + IPv6 scope): a link-local peer renders as `fe80::1%3`, which the HTTP client accepts back as a host, so event ips stay dialable.
110+
111+TLS uses per-device on-the-fly certificates with **mandatory client certificates** (optional while the web pages are served, so browsers can connect); the peer identity is the uppercase-hex SHA-256 of the client cert DER, and `Register` is simply not emitted when a payload's claimed fingerprint disagrees with the cert. Prefer `event.certFingerprint ?? event.info.fingerprint` — the payload fallback only exists for encryption-off mode.
112+
113+Both the receive pin and the web-send pin are fixed at server start, so changing either restarts the server.
114+
115+Web assets for the browser download page are embedded from `packages/core/assets/web/`.
116+
117+### Multicast discovery (Rust)
118+
119+`packages/core/src/multicast/` (feature `multicast`, independent of `http`) implements UDP multicast discovery for protocol v2.1 — v1 messages are not parsed.
120+Integration mirrors the HTTP server: `multicast::start` takes a `MulticastConfig { group, group_v6, port, interface_filter, device, event_tx }` and emits `MulticastEvent::Discovered { ip, message }`; the returned `MulticastHandle` offers `announce` (the announcement burst) and `wait_stopped`.
121+
122+UDP is **announce-only**: responses go back over HTTP as a unicast register request to the announcing device.
123+
124+One socket is bound per interface IPv4 address (`SO_REUSEPORT`/`SO_REUSEADDR` + `IP_MULTICAST_IF`), because a single socket only sends on one interface. Multicast loopback stays on so that instances on one host see each other; own messages are dropped by fingerprint. IPv6 is a LocalSend extension (group `ff12::fd3a:e420`, `DEFAULT_MULTICAST_GROUP_V6`), enabled by setting `group_v6`: one `IPV6_V6ONLY` socket per interface, joined by interface index. `Discovered` carries the source's scope ID (interface index), which link-local IPv6 sources need for the HTTP answer.
125+
126+### i18n
127+
128+Slang, source files in `app/assets/i18n/` (`<locale>.json` plus `_missing_translations_<locale>.json`), generated output in `app/lib/gen/`. Translations are managed on Weblate; fields prefixed with `@` are metadata for translators and are not used by the app. `app/test/unit/i18n_test.dart` guards the locale set.
129+
130+### FOSS build
131+
132+`in_app_purchase` and the donation UI are stripped for F-Droid by `support/scripts/remove_proprietary_dependencies.sh`, which relies on the `# [FOSS_REMOVE]` pubspec marker and `// [FOSS_REMOVE_START]` / `// [FOSS_REMOVE_END]` comment pairs. Preserve those markers when editing `lib/config/init.dart`, `lib/pages/donation/*`, or `lib/provider/purchase_provider.dart`.
133+
134+## Release notes
135+
136+`app/pubspec.yaml`'s version must match `#define MyAppVersion` in `support/scripts/compile_windows_exe-inno.iss` and the `version` in `cli/Cargo.toml` (the CLI prints it in its start banner) — CI fails on a mismatch. Platform build commands and release steps are documented in `README.md` ("Building") and `CONTRIBUTING.md` ("Release").
