* Translations * Hooks * Routes * Screens * Tests * Update CHANGELOG.md * tapping close button navigates back * find.byKey * New design * 'Create group' * Pass selectedUsers as param to hook * Add missing spacing for user list in set up group * use back button for headers in create group flow
11 KiB
AGENTS.md
Project Overview
This is a secure messaging app that uses the whitenoise Rust crate, which implements secure messaging using the Marmot Protocol with MLS (Messaging Layer Security) and Nostr.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Flutter UI Layer │
│ (screens/, widgets/, hooks/, providers/) │
├─────────────────────────────────────────────────────────────┤
│ Flutter-Rust Bridge Layer │
│ (lib/src/rust/ - auto-generated bindings) │
├─────────────────────────────────────────────────────────────┤
│ Rust API Layer │
│ (rust/src/api/ - thin wrapper around whitenoise) │
├─────────────────────────────────────────────────────────────┤
│ Whitenoise Rust Crate │
│ (external dependency - core messaging logic) │
└─────────────────────────────────────────────────────────────┘
Tech Stack
- Flutter/Dart - UI and application logic
- Rust - Core messaging/crypto functionality via FFI
- flutter_rust_bridge - Dart-Rust FFI bindings
- Riverpod - State management (shared state)
- flutter_hooks - Ephemeral widget state
- go_router - Navigation/routing
Git Worktrees
IMPORTANT: When starting work on a new feature, bug fix, or issue, use the /create-git-worktree command to create an isolated development environment.
/create-git-worktree <branch-name>
This creates a worktree in the trees/ directory at the repository root. Worktrees allow parallel development without affecting the main working directory.
Example: /create-git-worktree issue-42-fix-login
After running, you'll be working in trees/issue-42-fix-login/ on that branch.
Directory Structure
whitenoise/
├── lib/ # Flutter/Dart source code
│ ├── main.dart # App entry point
│ ├── routes.dart # Route definitions (go_router)
│ ├── theme.dart # Theme colors and styles
│ ├── providers/ # Riverpod providers (shared state)
│ ├── hooks/ # Flutter hooks (ephemeral state)
│ ├── screens/ # Full-page UI components
│ ├── widgets/ # Reusable components (see Widget Naming)
│ ├── services/ # Stateless operations (API calls)
│ ├── extensions/ # Dart extensions
│ ├── utils/ # Utility functions
│ ├── constants/ # Shared constants (fixed, related sets or reused elsewhere only)
│ └── src/rust/ # Auto-generated Rust bridge code (DO NOT EDIT)
├── rust/ # Rust source code
│ └── src/api/ # API modules exposed to Flutter
├── test/ # Flutter tests (mirrors lib/ structure)
├── trees/ # Git worktrees for parallel development
├── assets/ # Images, SVGs, fonts
└── scripts/ # Build/CI scripts
Use constants/ only for fixed, related sets (e.g. NIP kinds) or constants repeated in multiple places; otherwise keep constants next to the code that uses them.
Setup Commands
# Install all dependencies
just deps
# Install Flutter dependencies only
just deps-flutter
# Install Rust dependencies only
just deps-rust
Development Commands
# Format all code (Rust + Dart)
just format
# Lint all code
just lint
# Run all tests (verbose output)
just test-flutter
just test-rust
# Run tests with coverage (80% minimum)
just coverage
# Generate coverage HTML report
just coverage-report
# Pre-commit checks (REQUIRED before every commit)
just precommit
# Pre-commit with verbose output (for debugging failures)
just precommit-verbose
# Regenerate flutter_rust_bridge code
just generate
# Rebuild Android native libraries (after Rust code/dependency changes)
just build-android-quiet
Quiet Commands for Agents
IMPORTANT: When verifying that code works, agents should ALWAYS use the quiet variants. These produce minimal output that is easy to parse while still showing errors on failure.
# Quiet test commands - USE THESE for verification
just test-flutter-quiet # Output: "+1093: All tests passed!" or error details
just test-rust-quiet # Output: "....... test result: ok" or error details
just build-android-quiet # Output: "✅ Android build complete" or error details
# Quiet pre-commit - USE THIS before committing
just precommit # Shows step names + ✓/✗, errors only on failure
Why quiet variants?
- Minimal output reduces context window usage
- Clear pass/fail indicators are easy to parse
- Full error details are still shown when something fails
- No noisy progress indicators or dependency resolution messages
Example quiet precommit output:
flutter deps... ✓
rust deps... ✓
l10n generation... ✓
l10n validation... ✓
auto-fix... ✓
formatting... ✓
linting... ✓
flutter tests... ✓
rust tests... ✓
✅ PRECOMMIT PASSED
Code Style
Dart/Flutter
- Single quotes for strings
prefer_const_constructorsenabledprefer_final_localsenabled- Line width: 100 characters
- Trailing commas: preserve
Widget Naming
There are three categories of widgets with different naming rules:
-
Design system widgets — Simple, presentational widgets that match the Figma design system. They have Widgetbook stories, no translations, and no Rust API calls.
- File prefixed with
wn_(e.g.,wn_filled_button.dart) - Class prefixed with
Wn(e.g.,WnFilledButton)
- File prefixed with
-
Complex reusable widgets — Used across multiple screens but contain translations, hooks with Rust API calls, or other complex logic.
- No
wn_/Wnprefix (e.g.,onboarding_carousel.dart/OnboardingCarousel)
- No
-
Screen-scoped widgets — Extracted from a single screen for simplicity, only used in that one screen.
- Prefixed with the screen name (e.g.,
ChatListTilefor a widget only used in the chat list screen)
- Prefixed with the screen name (e.g.,
Hook Naming
- Hook files prefixed with
use_(e.g.,use_chat_list.dart) - Hook functions start with
use(e.g.,useChatList())
Provider Naming
- Files end with
_provider.dart - Provider variables end with
Provider(e.g.,authProvider)
Comments
- DO NOT add comments except for code that is really complex or hard to understand.
Responsive Sizing with flutter_screenutil
- Use
flutter_screenutilfor all size values to ensure responsive layouts across devices - Use
.wfor width values, e.g.20.w - Use
.hfor height values, e.g.16.h - Use
.spfor font size and letter spacing, e.g.14.sp - Use
.rfor radius values, e.g.8.r - Apply to: padding, margins, gaps, icon sizes, font sizes, border radius, container dimensions
Avoid StatefulWidget
- In line with rules number 6 & 7 below in the Development philosophy, we should avoid the use of StatefulWidgets. Prefer to use providers (shared app-wide state) or hooks (widget-local state) instead.
Testing
IMPORTANT: Test coverage is of utmost importance. Never submit a PR that reduces test coverage.
- Test files mirror source structure with
_test.dartsuffix - Minimum coverage requirement: 80%
- Use helpers from
test/test_helpers.dart:setUpTestView(tester)- Configure test view dimensionsmountTestApp(tester, overrides)- Mount full app with provider overridesmountHook(tester, useHook)- Test individual hooksmountWidget(child, tester)- Mount single widgetmountStackedWidget(child, tester)- Mount widget in Stack
- Mock Rust API using
RustLib.initMock(api: mockApi) - Always extend
MockWnApifromtest/mocks/mock_wn_api.dartinstead of implementingRustLibApidirectly - this ensures consistent mock behavior and reuses common mock implementations - Prefer
find.byKey()overfind.byIcon()- add keys to icons in widgets and usefind.byKey(const Key('icon_name'))in tests - Use valid 64-char hex strings for pubkeys in tests (see
test_helpers.dartfor examples), not dummy values like'abc'or'test-pubkey' - Avoid
// coverage:ignore- Do not use// coverage:ignore-line,// coverage:ignore-start, or// coverage:ignore-endto bypass coverage requirements. Write tests for the code instead. The only acceptable exception is truly unreachable code (e.g., adefaultcase in a switch that is exhaustive but required by the compiler).
Development philosophy
Follow these principles when writing code:
- Simplicity over complexity - Keep the app thin
- Test all code - No untested code
- No dead code - Delete commented/unused code
- Whitenoise is source of truth - Don't duplicate logic from the Rust crate
- No caching in Flutter - Whitenoise persists data in local DB
- Shared state in providers - Use Riverpod for app-wide state
- Ephemeral state in hooks - Use flutter_hooks for widget-local state
- Pass data to hooks, not refs - Hooks receive data, not widget references
- Screens watch providers - Screens observe providers and pass data to hooks
- Self-explanatory code - Avoid comments; write clear, readable code
State Management Pattern
Screen (watches providers)
│
├── Providers (shared/persistent state)
│ └── Auth, account pubkey, etc.
│
└── Hooks (ephemeral/local state)
└── Chat list, messages, form inputs, etc.
Rust API Guidelines
- Modules in
rust/src/api/are exposed to Flutter - Functions use
#[frb]attribute for bridge generation - Structs use
#[frb(non_opaque)]for Flutter compatibility - Errors wrapped in
ApiErrorenum usingthiserror - Files in
lib/src/rust/are auto-generated - DO NOT EDIT manually
Commit Checklist
CRITICAL: You MUST run just precommit before EVERY commit. No exceptions.
- Run
just precommitand ensure it passes completely - Coverage meets 95% minimum
- Update
CHANGELOG.mdfor any user-facing changes - Follow existing code patterns and naming conventions
The precommit command runs all checks: formatting, linting, and tests. If it passes, you're good to commit. If it fails, fix the issues before committing.
Fixing Bugs
When I report a bug, don't start by trying to fix it. Instead, start by writing a test that reproduces the bug. Then, have subagents try to fix the bug and prove it with a passing test.