2026-03-12 14:21:25 -04:00
# AGENTS.md — AI Agent Contributor Guide
2026-03-09 15:15:25 -04:00
2026-06-11 09:28:18 -07:00
This guide is for AI agents contributing to the Buzz codebase. It covers
2026-03-12 14:21:25 -04:00
agent-specific context and conventions. For general contributor info (setup,
code style, PR process, architecture), see [CONTRIBUTING.md ](CONTRIBUTING.md ).
2026-03-09 15:15:25 -04:00
---
2026-08-14 10:03:37 -06:00
## Product Contract
Before planning or reviewing a non-trivial change:
1. Read [VISION.md ](VISION.md ).
2. Read the `VISION_*.md` documents relevant to the affected product surface.
3. Read the applicable guidance in [TESTING.md ](TESTING.md ) and any
package-local `TESTING.md` .
4. Check that the proposed design advances, or at least does not contradict,
that product intent. Call out any intentional tension explicitly.
Implementation describes the product today; the vision documents describe the
product it is becoming. A locally correct change can still be wrong if it works
against that direction. Scale validation to the change's risk and exercise the
real workflow for user-visible or integration behavior when practical; green CI
and runtime evidence answer different questions.
---
2026-05-27 19:25:09 -04:00
## Ecosystem
2026-06-28 07:53:03 -07:00
Buzz spans five repos. This one (`block/buzz` ) is the OSS source for the relay, desktop, mobile, and CLI. The others handle internal builds and deployment:
2026-05-27 19:25:09 -04:00
| Repo | Purpose |
|------|---------|
2026-06-28 07:53:03 -07:00
| [block/buzz ](https://github.com/block/buzz ) | OSS source — relay, desktop app, mobile app, CLI, agent harness |
2026-08-03 10:43:10 -06:00
| [squareup/buzz-releases ](https://github.com/squareup/buzz-releases ) | Buildkite pipelines producing Block-signed macOS + iOS builds with `-block` desktop version suffix |
2026-05-27 19:25:09 -04:00
| [squareup/sprout-oss ](https://github.com/squareup/sprout-oss ) | CI pipeline building the relay Docker image and pushing to internal ECR |
| [squareup/block-coder-tf-stacks ](https://github.com/squareup/block-coder-tf-stacks ) | Terraform + ArgoCD deploying the relay to the staging Kubernetes cluster |
| [squareup/sprout-backend-blox ](https://github.com/squareup/sprout-backend-blox ) | Desktop backend provider script connecting Blox workstation agents to the relay |
```
2026-06-28 07:53:03 -07:00
block/buzz (source)
2026-08-03 10:43:10 -06:00
├─► buzz-releases (desktop + mobile builds → Artifactory, GitHub, Mobile Releases)
2026-05-27 19:25:09 -04:00
├─► sprout-oss (relay Docker image → ECR)
│ └─► block-coder-tf-stacks (Helm chart → ArgoCD → staging cluster)
└─── sprout-backend-blox (Blox compute provider for Desktop agent launch)
```
2026-06-05 12:47:31 -04:00
See [RELEASING.md ](RELEASING.md ) for the desktop release flow and
[CONTRIBUTING.md § Ecosystem ](CONTRIBUTING.md#ecosystem ) for contributor
access information.
2026-05-27 19:25:09 -04:00
---
2026-03-12 14:21:25 -04:00
## Repo Structure
2026-03-09 15:15:25 -04:00
```
2026-03-12 14:21:25 -04:00
crates/
2026-05-24 13:09:34 -04:00
# Relay + core
2026-06-11 09:28:18 -07:00
buzz-relay # WebSocket relay server — main entry point; also hosts git + huddle audio
buzz-core # Core types, event verification, filter matching, kind registry
buzz-db # Postgres event store and data access layer
buzz-auth # Authentication and authorization
buzz-pubsub # Redis pub/sub fan-out, presence, typing indicators
2026-06-29 12:39:02 -04:00
buzz-search # Postgres FTS full-text search
2026-06-11 09:28:18 -07:00
buzz-audit # Hash-chain audit log
buzz-media # Blossom/S3 media storage
2026-05-24 13:09:34 -04:00
# Agent surface
2026-06-11 09:28:18 -07:00
buzz-acp # ACP harness bridging Buzz events to AI agents
buzz-agent # Minimal ACP-compliant agent (non-streaming, tool-calls-as-output)
buzz-dev-mcp # Developer MCP server — shell + file-edit tools
buzz-persona # Agent persona packs
buzz-workflow # YAML-as-code workflow engine (evalexpr conditions)
2026-05-24 13:09:34 -04:00
# Clients + interop
2026-06-11 09:28:18 -07:00
buzz-pair-relay # Ephemeral sidecar relay for NIP-AB device pairing
buzz-pairing-cli # CLI for NIP-AB device pairing interop testing
2026-05-24 13:09:34 -04:00
git-sign-nostr # Sign git objects with a Nostr key
git-credential-nostr # Git credential helper for Nostr-authed push/fetch
# Tooling + shared
2026-06-11 09:28:18 -07:00
buzz-cli # Agent-first CLI
buzz-sdk # Typed Nostr event builders
buzz-admin # Operator CLI for relay administration
buzz-ws-client # Shared NIP-42 WebSocket client (connect, auth, publish)
buzz-test-client # Integration test client and E2E test suite
2026-05-24 13:09:34 -04:00
sprig # All-in-one harness bundling ACP, agent, and dev MCP
2026-03-09 15:15:25 -04:00
2026-03-12 14:21:25 -04:00
desktop/ # Tauri 2 + React 19 desktop app
2026-05-24 13:09:34 -04:00
web/ # Browser web client (repo browser, served by the relay)
mobile/ # Flutter mobile app
2026-03-12 14:21:25 -04:00
migrations/ # SQL migrations (auto-applied on relay startup)
scripts/ # Dev tooling
.env.example # Config template — copy to .env before running
2026-03-09 15:15:25 -04:00
```
---
2026-03-12 14:21:25 -04:00
## Getting Started
2026-03-09 15:15:25 -04:00
```bash
2026-03-12 14:21:25 -04:00
. ./bin/activate-hermit # activate hermit toolchain (Rust, Node, etc.)
cp .env.example .env # configure local environment
just setup # install deps, run migrations
just relay # start relay at ws://localhost:3000
just ci # run before any PR
2026-03-09 15:15:25 -04:00
```
2026-03-12 14:21:25 -04:00
See CONTRIBUTING.md for full setup details and dependency requirements.
---
## Quality Gates
2026-08-16 09:25:51 -06:00
Run `just ci` before every PR — it runs repository-wide formatting, lint,
and static checks; Rust, Tauri, desktop, and mobile tests; and desktop and web
builds. Clippy passing does not mean fmt passes; run both.
2026-03-12 14:21:25 -04:00
2026-06-11 09:28:18 -07:00
Run `just test` for integration tests if you touched `buzz-relay` ,
`buzz-db` , or `buzz-auth` — these require a running Postgres and Redis.
2026-03-12 14:21:25 -04:00
2026-05-28 17:42:35 -04:00
**Pre-commit hooks** are installed automatically by `just setup` and auto-fix
formatting via `stage_fixed` . Pre-commit runs fix variants in parallel (Rust
fmt, Tauri Rust fmt, desktop biome fix, web biome fix, mobile dart format).
Auto-fixable issues are fixed and re-staged; unfixable lint issues block the
2026-08-06 16:57:12 -04:00
commit. **Pre-push hooks** run clippy (workspace + Tauri), desktop TypeScript
typechecking (`tsc --noEmit` ), and fast unit tests in parallel (Rust, desktop
JS, Tauri Rust, mobile Flutter) — no overlap with pre-commit. Builds are
CI-only. Run `just fix-all` to auto-fix all formatting in one shot. Run
`just ci` for the full local gate. Run `just hooks` to
2026-07-15 19:49:17 -06:00
re-install hooks after env changes. Before agents run Git or hooks, activate the
repo's Hermit environment (`. ./bin/activate-hermit` ); do not rewrite hook
commands to compensate for an unconfigured shell `PATH` .
2026-05-26 18:05:42 -04:00
2026-07-26 10:43:06 -07:00
**Commit with `git commit -s`.** The required **DCO Check** fails any PR with a commit missing a `Signed-off-by` trailer, and `just hooks` installs a `commit-msg` hook that adds it to commits you create locally (`git rebase` and `git cherry-pick` still need `--signoff` ) — if you build commit commands programmatically, include `-s` every time. To repair a branch that already has unsigned commits: `git rebase --signoff main` , then force-push.
2026-03-12 14:21:25 -04:00
Additional rules:
- No `unsafe` code
2026-04-05 10:22:45 -04:00
- Do not introduce new `unwrap()` or `expect()` in production paths — use `?` and proper error types
2026-03-12 14:21:25 -04:00
- New public API must have doc comments
---
## Key Patterns
2026-06-29 12:39:02 -04:00
**Nostr-first HTTP surface** : Buzz's primary API is NIP-29 over WebSocket. The relay also exposes a narrow HTTP surface: NIP-11/NIP-05 metadata, `POST /events` , `POST /query` , `POST /count` , workflow webhooks at `/hooks/{id}` , Blossom media, git smart HTTP, git policy hooks, and health probes. These HTTP paths all preserve the same host-derived community boundary.
2026-03-12 14:21:25 -04:00
2026-06-29 12:39:02 -04:00
**Prefer Nostr events over new HTTP endpoints** : For new feature work, model
2026-06-11 09:28:18 -07:00
the operation as a Nostr event (new kind in `buzz-core/src/kind.rs` , handler
2026-06-29 12:39:02 -04:00
in `buzz-relay` ) rather than adding endpoint-specific JSON APIs. HTTP is
reserved for things that genuinely need an HTTP-only surface: media upload/download
(Blossom), webhooks, git smart HTTP, NIP-11/NIP-05 metadata, health checks,
and the generic Nostr bridge endpoints:
2026-05-12 14:39:19 -04:00
- `POST /events` — submit any signed event (same path the WebSocket uses).
- `POST /query` — Nostr REQ filters over HTTP. NIP-50 `search` filters
2026-06-29 12:39:02 -04:00
are routed to `buzz-search` (Postgres FTS) automatically.
2026-05-12 14:39:19 -04:00
- `POST /count` — Nostr COUNT filters over HTTP.
2026-06-29 12:39:02 -04:00
If you find yourself reaching for a new HTTP endpoint, first check whether
2026-05-12 14:39:19 -04:00
an event kind would do the job — it usually will, and you get realtime
fan-out, NIP-29 scoping, and the existing auth pipeline for free.
Reference https://github.com/nostr-protocol/nips
2026-03-12 14:21:25 -04:00
**Event kinds** : All event kind integers are defined in
2026-06-11 09:28:18 -07:00
`buzz-core/src/kind.rs` . New features get new kind integers — add them here
2026-03-12 14:21:25 -04:00
first, then implement handling in the relay.
**Channel scoping** : Channels use `h` tags (NIP-29 group tag), not `e` tags.
Filters and queries must scope to `h` tags when operating within a channel.
2026-08-01 14:53:41 +02:00
This applies to events *inside* a channel. Addressable events that describe a
channel carry its id in their `d` tag instead: kind:39000 (metadata),
kind:39001, kind:39002 (membership). `get_channels` resolves a user's channels
from the `d` tag of their kind:39002 events, not from `h` .
2026-03-12 14:21:25 -04:00
2026-06-11 09:28:18 -07:00
**Agent-facing operations go in `buzz-cli`** : New agent-facing features belong in `buzz-cli` — add a subcommand there first, then wire the REST/WebSocket call in `client.rs` . `buzz-dev-mcp` (shell + file tools for `buzz-agent` ) is separate.
2026-03-12 14:21:25 -04:00
2026-06-11 09:28:18 -07:00
**Workflow conditions** : `buzz-workflow` uses
2026-03-12 14:21:25 -04:00
[evalexpr ](https://docs.rs/evalexpr ) for condition evaluation. Keep expressions
simple and testable.
**Thread counters** : `reply_count` and `descendant_count` are materialized on
thread root events. Any code that inserts replies must update these counters —
check existing reply handlers for the pattern.
---
2026-06-11 09:28:18 -07:00
## Agent CLI (`buzz-cli`)
2026-05-22 10:32:20 -04:00
2026-06-11 09:28:18 -07:00
`buzz` is the agent-first CLI. Auth env vars
(`BUZZ_RELAY_URL` , `BUZZ_PRIVATE_KEY` , `BUZZ_AUTH_TAG` ) are auto-injected
2026-06-02 17:11:30 -04:00
by the ACP harness into managed agent subprocesses. In development, set
2026-06-11 09:28:18 -07:00
`BUZZ_PRIVATE_KEY` and `BUZZ_RELAY_URL` in your environment manually.
2026-06-02 17:11:30 -04:00
### Building the CLI
```bash
2026-06-11 09:28:18 -07:00
cargo build --release -p buzz-cli
2026-06-02 17:11:30 -04:00
```
2026-06-11 09:28:18 -07:00
Binary location: `./target/release/buzz` . Add `./target/release` to `PATH`
2026-06-02 17:11:30 -04:00
or invoke with the full path.
### Deep Links
2026-06-10 14:07:02 -07:00
`buzz://message?channel=<uuid>&id=<hex>` links reference a specific message
2026-06-02 17:11:30 -04:00
thread. To read the linked thread:
```bash
2026-08-16 09:25:51 -06:00
buzz --format compact messages thread --channel <uuid> --event <hex>
2026-06-02 17:11:30 -04:00
```
Extract `channel` and `id` from the URL query parameters. The optional
`thread` parameter (root event ID) can be ignored — `messages thread` resolves
the full thread from the event ID alone.
2026-05-22 10:32:20 -04:00
All reads return sig-stripped JSON arrays; all writes return
`{event_id, accepted, message}` ; creates add the entity ID. Exit codes:
2026-05-26 18:05:42 -04:00
0=ok, 1=input error, 2=network/relay, 3=auth, 4=other, 5=write conflict (NIP-33 LWW).
`--format compact` is a **global** flag — it goes before the subcommand:
2026-06-11 09:28:18 -07:00
`buzz --format compact channels list` , NOT `buzz channels list --format compact` .
2026-05-26 18:05:42 -04:00
2026-06-11 09:28:18 -07:00
See `crates/buzz-cli/TESTING.md` for the full live-testing runbook.
2026-05-22 10:32:20 -04:00
---
2026-03-12 14:21:25 -04:00
## Testing
2026-03-09 15:15:25 -04:00
```bash
2026-03-12 14:21:25 -04:00
just test-unit # unit tests, no infrastructure needed
2026-04-05 10:22:45 -04:00
just test # full integration suite (requires Postgres + Redis)
2026-03-09 15:15:25 -04:00
```
2026-06-11 09:28:18 -07:00
E2E tests live in `crates/buzz-test-client/tests/` :
2026-03-12 14:21:25 -04:00
- `e2e_relay.rs` — WebSocket relay protocol
2026-04-05 10:22:45 -04:00
- `e2e_media.rs` — media upload/download (Blossom)
- `e2e_media_extended.rs` — extended media scenarios
- `e2e_nostr_interop.rs` — Nostr interop (NIP-50 search, NIP-10 threads, NIP-17 gift wraps)
2026-03-12 14:21:25 -04:00
2026-08-16 09:25:51 -06:00
Desktop E2E: `cd desktop && pnpm test:e2e:smoke` for mock-bridge smoke
coverage, or `pnpm test:e2e:integration` for relay-backed coverage. These
scripts build the required E2E bridge before running Playwright.
2026-03-12 14:21:25 -04:00
See [TESTING.md ](TESTING.md ) for the full multi-agent E2E guide.
2026-03-09 15:15:25 -04:00
2026-06-25 18:40:00 -06:00
### PR Screenshots
2026-05-29 17:10:56 -04:00
2026-06-11 09:28:18 -07:00
> **Do NOT use `buzz upload`, the relay media endpoint, or any third-party
2026-06-04 18:35:10 -04:00
> image host for PR screenshots.** Relay media URLs fail through GitHub's camo
2026-06-25 18:40:00 -06:00
> proxy. Always use `scripts/post-screenshots.sh` for PNGs before linking them
> from a PR body/comment. If you hand-edit PR markdown, run
> `scripts/check-pr-image-urls.sh <markdown-file>` first to catch relay URLs.
For mobile simulator screenshots, save the PNGs in a local directory and run
`./scripts/post-screenshots.sh <PR-number> <png-dir>` or use the third argument
with a markdown template containing `{{filename}}` placeholders.
2026-06-04 18:35:10 -04:00
2026-06-03 15:33:56 -04:00
The desktop app requires the E2E mock bridge to render — it cannot run in a plain
browser. Use `just desktop-screenshot` to capture screenshots (builds frontend,
starts preview server, runs Playwright automatically):
2026-05-29 17:10:56 -04:00
```bash
2026-06-03 15:33:56 -04:00
just desktop-screenshot --name home
just desktop-screenshot --name channel --route /channels/general
just desktop-screenshot --name search --click open-search
just desktop-screenshot --name settings --click open-settings
2026-05-29 17:10:56 -04:00
```
2026-06-03 21:16:49 -04:00
Options: `--name` (filename), `--route` (client route), `--active-channel`
(channel to view), `--click` (left-click data-testid or CSS selector),
`--right-click` (right-click for context menus), `--hover` (hover before
capture), `--clip` (crop region as `x,y,w,h` — e.g. `0,0,256,720` for sidebar
only), `--wait` (ms, default 2000), `--viewport` (WxH, default 1280x720),
`--outdir` (default `test-results/screenshots` ), `--messages` (JSON file path).
Output is a PNG path on stdout.
2026-05-29 17:10:56 -04:00
2026-06-03 15:33:56 -04:00
Use `--messages` to inject content into a channel before capture. The JSON file
2026-06-03 21:16:49 -04:00
is an array of objects — `channelName` and `content` are required, all other
2026-06-11 09:28:18 -07:00
fields are optional and passed through to `__BUZZ_E2E_EMIT_MOCK_MESSAGE__` :
2026-06-03 15:33:56 -04:00
```json
[
2026-06-03 21:16:49 -04:00
{
"channelName" : "random" ,
"content" : "Hey @tyler check this out" ,
"pubkey" : "953d..." ,
"kind" : 40002 ,
"mentionPubkeys" : [ "deadbeef..." ],
"extraTags" : [[ "broadcast" , "1" ], [ "e" , "some-root-id" ]],
"parentEventId" : "abc123"
}
2026-06-03 15:33:56 -04:00
]
```
2026-06-03 21:16:49 -04:00
Without `--active-channel` , all messages must target the same channel and the
helper navigates to that channel (useful for showing message content). With
`--active-channel` , messages can target multiple channels while the "camera"
stays on the specified channel (useful for unread indicators, badges, etc.).
```bash
# Messages in the channel you're viewing (code blocks, formatting, etc.)
just desktop-screenshot --name code-blocks --messages /tmp/msgs.json
# Messages in OTHER channels to trigger unread state
just desktop-screenshot --name unread-dot \
--active-channel general --messages /tmp/badge-msgs.json
# Cropped to sidebar only (256px wide)
just desktop-screenshot --name sidebar-unread \
--active-channel general --messages /tmp/badge-msgs.json \
--clip 0,0,256,720
# Context menu on an unread channel (wider crop to include popup)
just desktop-screenshot --name ctx-mark-read \
--active-channel general --messages /tmp/badge-msgs.json \
--right-click channel-random --clip 0,200,320,300
# Hover state (e.g. copy button reveal)
just desktop-screenshot --name copy-hover \
--messages /tmp/code-msgs.json --hover "[data-testid='copy-code']"
```
2026-06-03 15:33:56 -04:00
Available mock channels: `general` , `random` , `design` , `sales` , `engineering` ,
`agents` , `watercooler` , `announcements` , `alice-tyler` , `bob-tyler` .
2026-06-03 22:28:11 -04:00
`scripts/post-screenshots.sh` hosts PNGs on a per-developer branch
(`agent-screenshots/<github-username>` ) and posts a PR comment with
commit-SHA-based image URLs (immutable — safe from later overwrites):
2026-06-03 15:33:56 -04:00
```bash
./scripts/post-screenshots.sh 803 test-results/screenshots
./scripts/post-screenshots.sh 803 test-results/screenshots body.md # custom body prepended
```
2026-06-03 21:16:49 -04:00
The body file supports `{{filename}}` placeholders (without `.png` ) to inline
images at specific positions. Images not referenced by any placeholder are
appended at the end. Without placeholders, all images are appended (backward
compatible).
```markdown
### Unread dot
A message arrives in `#random` .
{{01-unread-dot}}
### Context menu
Right-click shows "Mark as read".
{{02-context-menu}}
```
2026-06-09 23:22:34 -04:00
Re-runs overwrite the image blobs on the `agent-screenshots/<username>`
branch, but the script **appends a new PR comment** — it does not edit or
delete the previous one. After reposting, delete the superseded comment so
only the current set remains, otherwise reviewers still see the stale images:
```bash
# List screenshot comments to find the stale one's id
2026-06-28 07:53:03 -07:00
gh pr view <pr> --repo block/buzz --json comments \
2026-06-09 23:22:34 -04:00
--jq '.comments[] | select(.body | test("pr-<pr>--")) | {id, url}'
2026-06-28 07:53:03 -07:00
gh api -X DELETE repos/block/buzz/issues/comments/<stale-comment-id>
2026-06-09 23:22:34 -04:00
```
Branch cleanup when fully done: `git push origin --delete agent-screenshots/<username>` .
2026-05-29 17:10:56 -04:00
2026-06-04 15:53:53 -04:00
### Writing E2E Screenshot Specs
When screenshots need seeded state, live messages, or UI interaction before
capture, write a Playwright spec instead of using `just desktop-screenshot` .
Add specs to `desktop/tests/e2e/` and register them in `playwright.config.ts`
(`smoke` project `testMatch` ). Every test calls `installMockBridge(page)` for
mock Tauri IPC. Mock pubkey, channel names, and UUIDs live in `e2eBridge.ts` .
2026-07-27 16:45:34 -04:00
**Always build with `pnpm build:e2e`, never `pnpm run build`.** The mock Tauri
bridge is compiled in only for `--mode e2e` (see `installE2eBridgeIfConfigured`
in `desktop/src/main.tsx` ). A plain `pnpm run build` strips it, so
`window.__TAURI_INTERNALS__` is never defined and **every** mock-mode spec fails
with `Cannot read properties of undefined (reading 'invoke')` — the app renders
"Community connection failed" instead of the UI under test. That looks exactly
like a product bug rather than a build mistake, so it burns real time.
`pnpm test:e2e:smoke` and `pnpm test:e2e:integration` run the right build for
you; prefer them over a manual build plus `playwright test` .
2026-06-04 15:53:53 -04:00
**Stale server:** `reuseExistingServer: true` means a previous build's server
2026-07-27 16:45:34 -04:00
serves old code. Kill port 4173 and re-run `pnpm build:e2e` before re-running
tests after code changes.
2026-06-04 15:53:53 -04:00
** `addInitScript` before bridge:** `page.addInitScript` (localStorage seeding)
must run BEFORE `installMockBridge(page)` — React reads state on mount, the
bridge triggers mount.
**Live messages:** Call `waitForMockLiveSubscription(page, channelName)` before
2026-06-11 09:28:18 -07:00
`__BUZZ_E2E_EMIT_MOCK_MESSAGE__` — messages are silently dropped without a
2026-06-04 15:53:53 -04:00
subscription. Navigate to the channel first (triggers subscription), then away
(so unread indicators appear), then inject.
**Animation timing:** Radix components animate in via CSS. `toBeVisible()`
2026-06-17 11:03:45 -04:00
resolves mid-animation — wait for completion before screenshotting. Use the
shared helper (mandatory before any `page.screenshot()` or
`locator.screenshot()` in specs):
```ts
import { waitForAnimations } from "../helpers/animations" ;
// ... after the element is visible but before capturing:
await waitForAnimations ( page );
await page . screenshot ({ path : "..." , clip : { ... } });
```
The `just desktop-screenshot` path (`screenshot.mjs` ) calls
`waitForAnimations` automatically — no manual step needed there.
For per-element waits (rare — prefer the page-level helper above):
2026-06-04 15:53:53 -04:00
```ts
await menuItem . evaluate (( el ) =>
Promise . all (
el . closest ( "[data-state]" ) ? . getAnimations (). map (( a ) => a . finished ) ?? [],
),
);
```
**Cropping:** Use `clip` — full-window (1280x720) screenshots are unreadable
for sidebar features. Sidebar = 256px; context menus ~450px.
2026-06-09 23:22:34 -04:00
**Distinct states — verify before posting:** when one view renders many
elements at once (e.g. all team cards in a single grid), an unscoped
full-page `page.screenshot()` captures the *same* pixels for every shot, so
multiple PNGs come out byte-identical. Scope each shot to its subject with
`locator.screenshot()` (full-page `clip` only when an overlay like an open
dropdown must be included). Then gate on hash distinctness before posting:
```bash
shasum -a 256 test-results/<dir>/*.png # every hash must be unique
```
Identical hashes mean two shots captured the same state — fix the spec, do
not post. This catches the most common screenshot regression.
2026-06-04 15:53:53 -04:00
** `general` has pre-seeded messages** making `hasUnread` always true. Use
`engineering` for "muted + no unread" visual states.
**PR comments:** Use a body template (3rd arg to `post-screenshots.sh` ) with
`{{filename}}` placeholders. Each screenshot gets a `###` heading + one-line
2026-06-28 07:53:03 -07:00
description. See [PR #803 ](https://github.com/block/buzz/pull/803 ).
2026-06-04 15:53:53 -04:00
2026-03-09 15:15:25 -04:00
---
2026-05-22 10:32:20 -04:00
## Common Gotchas
2026-06-11 09:28:18 -07:00
1. **Kind `39000` for channel metadata, not `41`** — kind 41 is NIP-01 (unused). All kinds defined in `buzz-core/src/kind.rs` .
2026-05-22 10:32:20 -04:00
2. **Relay queries must specify `kinds`** — omitting `kinds` triggers the p-gate (403). Always include explicit kind filters.
2026-08-16 09:25:51 -06:00
3. ** `messages search` chooses its own supported kinds** — do not add a `--kinds` option; the current command does not accept one. This differs from raw relay filters, which still need explicit kinds.
2026-05-26 18:05:42 -04:00
4. **Worktrees: `cd` in the same command** — shell CWD doesn't persist between tool calls. Use `cd /path && cargo build` as one command.
5. **Desktop crate excluded from root workspace** — `cargo test` at repo root does NOT run desktop tests. Use `cargo test --manifest-path desktop/src-tauri/Cargo.toml` explicitly.
2026-08-16 09:25:51 -06:00
6. **React render perf: `React.memo` is all-or-nothing** — it only skips a re-render when *every* prop is reference-stable; one unstable prop (inline arrow/JSX, or a hook returning a fresh `{}` /`[]` /`Map` each render) defeats it. Two repeat offenders: (a) React Query results (`useMutation` /`useQuery` ) are a **new object each render** — depend on the stable method (`mutation.mutateAsync` ), not the object; (b) derived `Map` /array state that recomputes on a version bump — wrap in a content-equality ref cache (`shared/hooks/useStableReference.ts` ). When chasing interaction lag, **measure with DevTools closed and no perf probes** (an open Web Inspector + per-keystroke `console.log` inflate the numbers), and isolate by removing one suspect at a time rather than guessing.
2026-05-22 10:32:20 -04:00
---
2026-03-12 14:21:25 -04:00
## Desktop App
2026-03-09 15:15:25 -04:00
2026-03-12 14:21:25 -04:00
The desktop app is Tauri 2 + React 19 + Vite + Tailwind CSS. Features are
organized under `desktop/src/features/` . Biome handles linting and formatting.
2026-03-09 15:15:25 -04:00
```bash
2026-03-12 14:21:25 -04:00
just desktop-dev # web-only dev server (faster iteration)
2026-05-29 18:26:24 -04:00
just dev # full Tauri app with native shell
2026-03-09 15:15:25 -04:00
```
2026-06-16 22:28:24 -07:00
### Text sizing & zoom (use rem, never px)
The desktop app implements Cmd +/- zoom by scaling the root `<html>`
font-size (`desktop/src/app/useWebviewZoomShortcuts.ts` ) and pinning the native
webview zoom. **Only rem-based text scales with zoom — hardcoded px text sizes
are frozen.**
So for any readable text, reach for rem-based Tailwind tokens, never arbitrary
px:
- ✅ Stock rem tokens (`text-base` , `text-sm` , `text-xs` , …). **Chat body/author
text === `text-base` (16px) — chat is the app's base type size**, and the
surrounding timeline elements (timestamps, system rows, code, reactions) are
deliberate steps on that same stock ramp.
- ✅ The `text-2xs` (0.6875rem / 11px) and `text-3xs` (0.5rem / 8px) meta-text
tokens (in `desktop/tailwind.config.js` under `theme.extend.fontSize` ) for the
sub-`text-xs` ramp — timestamps, count badges, tracking labels, tiny glyphs.
These replaced the dozens of arbitrary `text-[…rem]` literals that had drifted
apart pixel-by-pixel; keep meta text on these two tokens, not new arbitrary
values.
- ❌ `text-[15px]` , `text-[13px]` , CSS `font-size: 15px` — px froze against zoom
and caused the message-timeline regression (PR #891 ).
- ❌ Arbitrary rem literals too: `text-[0.6875rem]` , `text-[0.9rem]` , etc. They
zoom fine but re-fragment the scale we consolidated. Use a named token.
Prefer stock tokens — they're rem and zoom-safe. Only if a design genuinely
needs a size the stock/`2xs` /`3xs` scale can't express should you **add a
rem-based token** (in `desktop/tailwind.config.js` under `theme.extend.fontSize` )
rather than an arbitrary literal. A CI guard (`pnpm check:px-text` , in
`desktop/scripts/check-px-text.mjs` ) scans all of `desktop/src` and fails on any
new arbitrary text-size literal — px **or** rem/em. Genuinely decorative glyphs
(e.g. the `text-[6rem]` avatar emoji) are allowlisted by `path:line` in that
script.
2026-07-21 12:18:20 -04:00
### Community Switching
2026-04-28 12:13:12 -06:00
2026-07-21 12:18:20 -04:00
The desktop app supports multiple communities (each backed by a different relay).
Switching communities does **not** reload the page — it uses React key-based
remounting. `<AppReady key={communityKey} />` in `App.tsx` forces the entire
community-scoped subtree to unmount and remount with fresh state.
2026-04-28 12:13:12 -06:00
**Module-level singletons must be explicitly reset.** React remounting only
clears React state (useState, useRef, context). Module-level variables (Maps,
2026-07-21 12:18:20 -04:00
class instances, cached promises) survive across remounts. Every community-scoped
singleton needs a reset function wired into `resetCommunityState()` in
`desktop/src/features/communities/useCommunityInit.ts` .
2026-04-28 12:13:12 -06:00
2026-08-16 09:25:51 -06:00
`resetCommunityState()` is the canonical inventory of community-scoped
singletons. **If you add a new module-level cache, Map, or class instance that
holds community-scoped data, add its reset there in the same change.** Failure
to do so causes data from the old community to leak into the new one. Avoid
duplicating its complete reset list here; the implementation is the source of
truth.
2026-04-28 12:13:12 -06:00
Key files:
2026-07-21 12:18:20 -04:00
- `desktop/src/app/App.tsx` — community key, init gate, remount boundary
- `desktop/src/features/communities/useCommunityInit.ts` — `resetCommunityState()` , applies config to Tauri backend
- `desktop/src/main.tsx` — provider hierarchy (`QueryClientProvider` > `App` )
2026-04-28 12:13:12 -06:00
2026-03-12 14:21:25 -04:00
---
2026-03-09 15:15:25 -04:00
2026-04-15 09:55:32 -06:00
## Mobile App (Flutter)
The mobile app lives in `mobile/` — a Flutter app using Riverpod + Hooks.
### Architecture
- **State management:** Riverpod + `flutter_hooks` (`HookConsumerWidget` )
- **Theme:** Catppuccin Latte (light) / Macchiato (dark) — matches desktop
- **Features:** Isolated under `lib/features/` , shared code in `lib/shared/`
- **Nostr models:** `lib/shared/relay/nostr_models.dart` — event kinds must
stay in sync with `desktop/src/shared/constants/kinds.ts`
### Rules
2026-06-30 13:27:37 -06:00
- **NEVER use `StatefulWidget` ** — favor Riverpod for state and always use
`HookConsumerWidget` or `ConsumerWidget` with `flutter_hooks` for local state.
2026-08-16 09:25:51 -06:00
- Agents may build and run the Flutter app when it materially helps implement,
debug, or validate mobile changes. Prefer the smallest relevant command and
reuse an already-running simulator/emulator and the app's configured staging
or production community when that is sufficient. Do not start or rebuild
local relay services unless the task specifically requires relay-side or
isolated integration behavior.
- For iOS runtime validation, prefer `just mobile-dev` ; it applies the
worktree-specific debug identity and runs `flutter run` . Direct `flutter run`
or IDE workflows are also allowed. Use `just mobile-build-android` only when
an APK build is relevant to the task.
- Do not rebuild, reinstall, or relaunch merely for ceremony. Preserve Flutter's
incremental build cache and use hot reload/restart where appropriate. Use
`flutter clean` only when stale build artifacts are a credible cause. Run
`flutter upgrade` only when the task explicitly requires a toolchain change.
- For user-visible or integration changes, exercise the affected workflow in a
real app when practical and report the device/simulator, connected community,
and workflow actually tested.
2026-04-15 09:55:32 -06:00
- **Do NOT use `print()` ** — use `debugPrint()` or structured logging.
- Prefer `context.colors` and `context.textTheme` (via theme extensions)
over raw `Theme.of(context)` calls.
2026-06-30 13:27:37 -06:00
- **Keep widgets small and composable.** One public widget per file; push
private sub-widgets (`_Foo` ) into sibling `part` files under a
`<page>/` folder rather than growing the page file. Hard ceiling:
**1000 lines/file** , enforced by `mobile/scripts/check-file-sizes.mjs` via
`just mobile-check` (runs in `just check` + pre-push, mirroring desktop/web).
If the guard trips, **split the file — never bump the limit or add an
override to slip under it.**
2026-04-15 09:55:32 -06:00
- Feature modules must not import from other feature modules — only from
`shared/` .
- Use `Grid` tokens for spacing, `Radii` for border radius.
### Quality Checks
```bash
cd mobile
dart format --output= none --set-exit-if-changed .
flutter analyze
flutter test
```
2026-05-26 18:05:42 -04:00
Or from repo root: `just mobile-fmt` (auto-fix), `just mobile-check` (lint + fmt check), `just mobile-test` (tests).
2026-04-15 09:55:32 -06:00
2026-08-16 09:25:51 -06:00
To run the app locally with a worktree-specific debug identity and a
started or reused iOS Simulator:
2026-05-29 18:26:24 -04:00
```bash
just mobile-dev
```
2026-08-16 09:25:51 -06:00
This runs `flutter run` against the app's configured community; it does not
start Docker or local relay services.
2026-07-26 13:47:35 -07:00
When run from a git worktree, `just mobile-dev` (and `just
mobile-build-android` ) give the debug build a per-worktree app identifier
(keyed to the worktree directory name) and a branch-labelled app name via
`scripts/mobile-worktree-overrides.sh` , so builds from multiple worktrees
install side by side. Release builds are unaffected. `just mobile-clean`
removes stale worktree-suffixed installs from simulators/emulators. See
[mobile/README.md ](mobile/README.md ) for direct Xcode / Android Studio
usage.
2026-04-15 09:55:32 -06:00
### Testing Conventions
- Prefer **widget tests** over unit tests for UI components — test the
whole widget tree, not individual methods.
- Use `ProviderScope(overrides: [...])` to inject fake notifiers.
- Fake notifiers should extend the real notifier class and override `build()` .
- Use the `WidgetHelpers.testable()` wrapper for simple widget tests or
build a custom `ProviderScope` + `MaterialApp` when you need specific overrides.
---
2026-03-12 14:21:25 -04:00
## See Also
2026-03-09 15:15:25 -04:00
2026-06-29 12:39:02 -04:00
- [CONTRIBUTING.md ](CONTRIBUTING.md ) — setup, code style, PR process, how to add event kinds / CLI subcommands / HTTP endpoints
2026-03-12 14:21:25 -04:00
- [TESTING.md ](TESTING.md ) — multi-agent E2E test guide
- [ARCHITECTURE.md ](ARCHITECTURE.md ) — system design and component relationships
2026-07-23 15:05:59 -07:00
- [RELEASING.md ](RELEASING.md ) — release process: `release-desktop` , `release-relay` , `scripts/mobile-release.sh` , candidate tags, internal builds
2026-03-12 14:21:25 -04:00
- [README.md ](README.md ) — project overview and quick start