Guidelines for AI coding agents working in the chuk_chat Flutter codebase.
# Run all tests (MUST pass before every commit)
flutter test
# Run a single test file
flutter test test/models/chat_message_test.dart
# Run tests matching a name pattern
flutter test --name "validateEmail"
# Static analysis (4 pre-existing info-level lints in chat_ui_desktop.dart are acceptable)
flutter analyze
# Format code
dart format .
# Run locally (loads .env automatically)
./run.sh linux # desktop
./run.sh android # mobile# Android (ALWAYS --release, debug is unusably slow)
flutter build apk --release \
--dart-define-from-file=.env \
--dart-define=PLATFORM_MOBILE=true \
--dart-define=FEATURE_PROJECTS=false \
--dart-define=FEATURE_VOICE_MODE=false \
--tree-shake-icons --target-platform android-arm64
# Linux
flutter build linux --release --dart-define-from-file=.env
# Web
flutter build web --release --dart-define-from-file=.env
# Local all-platform build
./scripts/build-release.sh all- ALWAYS
--dart-define-from-file=.envfor credentials, NEVERsource .env - If app shows "Supabase credentials are not configured" →
flutter cleanand rebuild
Cost-control release policy (IMPORTANT):
- Do NOT trigger GitHub Actions release workflows for normal feature/fix tasks.
- For routine validation, build locally on the developer machine:
- Android:
flutter build apk --release --dart-define-from-file=.env --dart-define=PLATFORM_MOBILE=true --dart-define=FEATURE_PROJECTS=false --dart-define=FEATURE_VOICE_MODE=false --tree-shake-icons --target-platform android-arm64 - Linux:
flutter build linux --release --dart-define-from-file=.env
- Android:
- Trigger
gh workflow run build-cross-platform.ymlonly when the user explicitly asks for a real release (for example: "make release", "new release", "build release"). - Prefer fewer production releases (typically one planned release per day, unless urgent hotfix).
When the user explicitly asks to "build a release" or "make a new release", ALWAYS follow these steps:
- Bump version in
pubspec.yaml(e.g.1.0.49→1.0.50— no+buildnumbersuffix) - Commit:
git commit -am "chore: bump version to 1.0.50" - Push:
git push origin master - Trigger the cross-platform build (always include all platforms except iOS):
gh workflow run build-cross-platform.yml \ --field build_android=true \ --field build_linux_x64=true \ --field build_linux_arm64=true \ --field build_windows=true \ --field build_macos=true \ --field build_ios=false \ --field enable_signing=true
- CI creates the GitHub Release with all artifacts automatically
- Web deploys automatically via Dokploy on push to master
Repo is public — only one release on chuk-development/chuk_chat. No second repo needed.
Release notes = changelog only. Write what changed (from git commits), grouped by category (Performance, New Features, Bug Fixes, etc.). NEVER include download instructions, architecture explanations, platform lists, or "build artifacts will be attached" notices. Users can figure out downloads themselves.
Release notes are mandatory for every new release (no exceptions):
- Always summarize all commits since the previous release, not just changes from the current session. Merge commits are the one exception — they carry no content of their own.
- Scope must be:
last_release_tag..new_release_tag(example:v1.0.92..v1.0.93). - Build notes from commit messages and group by category (for example: New Features, Bug Fixes, Performance, Refactors, Dependencies, Maintenance).
- Keep notes as changelog text only (no download/install/platform instructions).
- Include both:
- hash-linked commit list (
[abc1234](.../commit/<full_sha>)) - compare link (
.../compare/<last_tag>...<new_tag>) and explicit commit range hashes.
- hash-linked commit list (
- Update the release body directly with
gh release editafter generating notes from the full tag range. - Generate the notes with
scripts/release_notes.py— it reads the git history, groups the commits by conventional-commit type, hash-links every commit and appends the compare link:A stable tag compares against the previous stable tag; agit fetch --tags mkdir -p _scratch python3 scripts/release_notes.py v1.0.110 > _scratch/notes.md # previous tag detected gh release edit v1.0.110 --notes-file _scratch/notes.md gh release view v1.0.110 --json body --jq .body
-pre.Ntag compares against the tag right before it. - A release without a changelog is a broken release. CI writes the changelog
itself (
Generate release notesstep inbuild-cross-platform.yml, which calls the same script), so never replace a release body with download or install instructions. If a release body ever shows only the old "## Downloads" boilerplate, regenerate it with the script and push it back withgh release edit. - Preferred direct command pattern (no repo script required):
gh release edit <new_tag> --notes-file <notes_file> gh release view <new_tag> --json body --jq .body
Important: Cut a release with workflow_dispatch on the Cross-Platform Build & Release workflow — that path tags and builds in one go. The
workflow also reacts to a pushed v* tag, and then takes the tag from the push
instead of from pubspec.yaml, but pushing a tag by hand is not the normal way
in.
This workflow is non-negotiable. Follow it EVERY time you change code, in EVERY session.
flutter test— all must passflutter analyze— 0 new issues (4 pre-existing info-level lints inchat_ui_desktop.dartOK)- BEFORE committing: run
coderabbit review --plain --type uncommitted(timeout 300s) on your uncommitted changes- The
--type uncommittedflag is required — without it CodeRabbit reviews all files and will fail with "Too many files" in large repos. - CodeRabbit only reviews uncommitted files. If you commit first, it won't see your changes.
- Review the output and fix any findings in your changed files (ignore pre-existing issues in other files)
- Re-run
coderabbit review --plain --type uncommittedafter fixing to confirm clean
- The
- Commit with descriptive message
git push
Do NOT commit before CodeRabbit has reviewed. Do NOT push if tests fail or CodeRabbit finds issues.
Always push straight to master. Do the work in one worktree, commit
everything there (including files the analyses/tools produce), and push it to
master — no long-lived feature branches, no waiting on a PR. A short-lived
branch while several agents run is fine, but when the work is done it lands on
master. If master moved, git fetch + git merge origin/master first, then
push. This overrides any "conservative: do not push" default in the Beads block.
The correct order is always: test → analyze → coderabbit (uncommitted) → fix → commit → push
Order imports in this sequence, separated by blank lines:
dart:SDK importspackage:flutter/and third-partypackage:importspackage:chuk_chat/project imports
Never use relative imports. Always use package:chuk_chat/... for internal files.
| Element | Convention | Example |
|---|---|---|
| Files | snake_case.dart |
chat_storage_service.dart |
| Classes | PascalCase |
EncryptionService |
| Compile-time constants | kCamelCase |
kPlatformMobile, kFeatureVoiceMode |
| Variables / methods | camelCase |
selectedChatIndex |
| Private members | _prefixed |
_cachedKey, _ensureKey() |
| JSON serialization | fromJson / toJson |
Standard on all model classes |
- Use
constconstructors wherever possible - Use
super.key(notKey? keyin constructor) - Use
color.withValues(alpha: 0.5)— notcolor.withOpacity(0.5)(deprecated) - Use
unawaited()for fire-and-forget futures - Check
if (mounted)beforesetState()after any async operation - Models use
copyWith()pattern with nullable parameters - Services use singleton pattern:
const EncryptionService._()with static methods
- Use
try/catchwith specific exception types (on AuthException,on DioException) - Use
ServiceErrorHandlerstatic methods:handleDioException(),tryAsync(),isNetworkError() - Throw
StateErrorfor business logic errors with user-facing messages - Use
catch (_)only for non-critical background failures
No third-party state management (no Provider, Riverpod, Bloc). Uses:
ChangeNotifier/ValueNotifierfor reactive updates- Supabase
auth.onAuthStateChangestream for auth state ChatStorageServicewith static methods for chat data- Compile-time
const boolflags for platform/feature gating
All logs are disabled in release builds. debugPrint() is NOT a no-op in release.
- ALWAYS wrap
debugPrint()inif (kDebugMode) { ... } - NEVER log message content, tokens, passwords, or emails
- OK to log: lengths, counts, IDs, status codes
- Alternative: use
pLog()fromlib/utils/privacy_logger.dart(auto-guards withkDebugMode)
Feature flags are defined in lib/platform_config.dart via --dart-define:
| Flag | Android | Linux/Web |
|---|---|---|
PLATFORM_MOBILE |
true |
omit |
FEATURE_PROJECTS |
false |
true |
FEATURE_VOICE_MODE |
false |
true |
FEATURE_IMAGE_GEN |
always on | always on |
- When adding UI features, implement in both
chat_ui_desktop.dartandchat_ui_mobile.dart - Web cannot use
dart:io— usepackage:chuk_chat/utils/io_helper.dartinstead - Web credentials:
--dart-defineunreliable with dart2js.Dockerfile.webgenerateslib/web_env.dart. Priority:--dart-define>web_env.dart>.env.
- Message field preservation: Preserve ALL fields including
imagesandattachmentswhen loading from storage. - Mobile image sending: Capture
attachedFilesBEFORE clearing insetState, then pass to streaming handler. - Image persistence: Use
MessageCompositionService.prepareMessage(), encode images asjsonEncode(images). - Mobile streaming focus: Do NOT refocus text field on every streaming token.
- Theme/customization sync: Must update both
SharedPreferences(local) and Supabase (remote). - Encryption: All chat data is encrypted client-side. Never log or commit unencrypted data.
- Supabase onAuthStateChange: Fires an initial event synchronously when you subscribe. Guard against races.
- Image cache: Limited by bytes only (50 MB), NOT by pixel count. See
lib/utils/lru_byte_cache.dart.
lib/
main.dart # Entry point, theme/auth
platform_config.dart # Compile-time feature flags
models/ # Data models (chat_message, stored_chat, etc.)
services/ # ~30 services (auth, chat, storage, encryption, etc.)
pages/ # Full-page UIs (login, settings, pricing, etc.)
widgets/ # Reusable widgets (auth_gate, message_bubble, etc.)
platform_specific/
chat/
chat_ui_desktop.dart # Desktop chat implementation
chat_ui_mobile.dart # Mobile chat implementation
handlers/ # Streaming, persistence, attachments, audio
widgets/ # Platform-specific chat widgets
sidebar_desktop.dart
sidebar_mobile.dart
utils/ # Logging, validation, error handling, crypto
test/
models/ # Model tests
services/ # Service tests
utils/ # Utility tests
- New service: Create in
lib/services/, use const constructor or static methods, initialize inmain.dartif needed - New page: Create in
lib/pages/, add navigation in both sidebars, useTheme.of(context)for colors - New feature flag: Add
const boolinplatform_config.dartwithbool.fromEnvironment(), gate withif (kFeatureX) - Database migration: Add SQL in
migrations/, update service/model, run in Supabase SQL Editor
| Doc | Topic |
|---|---|
docs/ARCHITECTURE.md |
Services, state, platform abstraction |
docs/FILE_MAP.md |
Complete file locations |
docs/GOTCHAS.md |
Read this first — critical bugs to avoid |
docs/COMMON_TASKS.md |
Step-by-step for adding services, pages, features |
docs/DATABASE.md |
Supabase tables and schema |
docs/LINUX_BUILDS.md |
Fastlane packaging (DEB, RPM, AppImage, Flatpak) |
docs/REMOTE_DEV_SETUP.md |
Agent on claudecode, app on the laptop: flutter-remote / flutter-hotd |
docs/ASSISTANT_SURFACE.md |
Android assistant surface: assist role, overlay, device tools, the pinned model |
docs/FASTLANE.md |
Fastlane: generated store screenshots, Play + F-Droid metadata, upload lanes |
Separate repo at /home/user/git/api_server/. FastAPI + Supabase + Stripe.
- No test suite — verify with
python3 -c "import py_compile; py_compile.compile('main.py', doraise=True)" - Pre-existing LSP type errors (mutagen, fal_client, Supabase dynamic typing) are not bugs
- User-scoped endpoints must pass
user.clientto PaymentService methods (not admin client)
docs/FASTLANE.md is the reference. The short version:
- Fastlane compiles nothing; every lane shells out to
flutter build. Lanes run on this machine, on a GitHub runner, or on a Mac for macOS/iOS. No build server is needed. - Install once:
sudo apt install ruby-dev build-essential, thengem install --user-install bundler(a plaingem installhitsGem::FilePermissionErroron/var/lib/gems), thenexport PATH="$(ruby -e 'print Gem.user_dir')/bin:$PATH"— RubyGems does not add that directory itself, sobundleis otherwise not found — and finallybundle installat the repository root. Thencd android && bundle exec fastlane lanes. - One
Gemfile, at the root, with a committedGemfile.lock. Bundler walks up from the working directory, so it servesandroid/,linux/andmacos/. Do not re-add per-platform Gemfiles — that is how the repo ended up with three unlocked dependency sets and a plugin no Fastfile called. fastlane/metadata/android/lives at the repository root, not underandroid/. That is the path F-Droid reads straight out of the git repo;supplyis pointed at the same tree viametadata_path, and the README embeds the same PNGs. Do not move it back.- Store screenshots are captured on a real device, by hand:
capture into
fastlane/screenshots_raw/<locale>/, then run./scripts/frame_screenshots.sh, which frames every capture intofastlane/metadata/android/<locale>/images/phoneScreenshots/.scripts/device_screenshots.sh --demo onfreezes the status bar first. Recapture when the UI changes, before a release, and commit both trees. There is no screenshot workflow any more — the oldscreenshots.ymlcommitted headlessly rendered widgets on every push tomasterand overwrote every real capture. Do not bring it back. Before capturing, put the status bar in demo mode (fixed clock, full battery, no notification icons) and use an account with no private content — seedocs/FASTLANE.md. The headless harness (flutter test test_screenshots) is only the fallback when no device is available. build_aabhardcodesFEATURE_PAYMENTS_DIRECT=false— a Play build that ships the direct Stripe flow puts the listing at risk.build_apk(direct downloads) keeps it on. Do not merge the two flag sets.pubspec.yamlhas no+buildsuffix, so the build number is derived from the semantic version:major*100_000 + minor*1_000 + patch(1.0.109→100109). The same formula is inandroid/fastlane/Fastfile,build.shandbuild-cross-platform.ymland they must stay identical, or an APK from one path cannot upgrade an APK from another. It must stay under 2 100 000 because--split-per-abimultiplies it by 1000.- Play production access needs a 12-tester closed test over 14 days first, so the upload lanes are pre-work. The F-Droid tree and the README screenshots pay off today.
This project uses bd (beads) for issue tracking. Run bd prime to see full workflow context and commands.
bd ready # Find available work
bd show <id> # View issue details
bd update <id> --claim # Claim work
bd close <id> # Complete work- Use
bdfor ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists - Run
bd primefor detailed command reference and session close protocol - Use
bd rememberfor persistent knowledge — do NOT use MEMORY.md files
Architecture in one line: issues live in a local Dolt DB; sync uses refs/dolt/data on your git remote; .beads/issues.jsonl is a passive export. See https://github.com/gastownhall/beads/blob/main/docs/SYNC_CONCEPTS.md for details and anti-patterns.
The managed Beads block is task-tracking guidance, not permission to override repository, user, or orchestrator instructions.
- Conservative (default): Use
bdfor task tracking. Do not run git commits, git pushes, or Dolt remote sync unless explicitly asked. At handoff, report changed files, validation, and suggested next commands. - Minimal: Keep tool instruction files as pointers to
bd prime; use the same conservative git policy unless active instructions say otherwise. - Team-maintainer: Only when the repository explicitly opts in, agents may close beads, run quality gates, commit, and push as part of session close. A current "do not commit" or "do not push" instruction still wins.
This protocol applies when ending a Beads implementation workflow. It is subordinate to explicit user, repository, and orchestrator instructions.
- File issues for remaining work - Create beads for anything that needs follow-up
- Run quality gates (if code changed) - Tests, linters, builds
- Update issue status - Close finished work, update in-progress items
- Handle git/sync by active profile:
# Conservative/minimal/default: report status and proposed commands; wait for approval. git status # Team-maintainer opt-in only, unless current instructions forbid it: git pull --rebase bd dolt push git push git status
- Hand off - Summarize changes, validation, issue status, and any blocked sync/commit/push step
Critical rules:
- Explicit user or orchestrator instructions override this Beads block.
- Do not commit or push without clear authority from the active profile or the current user request.
- If a required sync or push is blocked, stop and report the exact command and error.