feat: import Chinese-localized Buzz source snapshot
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
This commit is contained in:
@@ -0,0 +1,600 @@
|
||||
# AGENTS.md — AI Agent Contributor Guide
|
||||
|
||||
This guide is for AI agents contributing to the Buzz codebase. It covers
|
||||
agent-specific context and conventions. For general contributor info (setup,
|
||||
code style, PR process, architecture), see [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
---
|
||||
|
||||
## Ecosystem
|
||||
|
||||
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:
|
||||
|
||||
| Repo | Purpose |
|
||||
|------|---------|
|
||||
| [block/buzz](https://github.com/block/buzz) | OSS source — relay, desktop app, mobile app, CLI, agent harness |
|
||||
| [squareup/buzz-releases](https://github.com/squareup/buzz-releases) | Buildkite pipelines producing Block-signed macOS + iOS builds with `-block` desktop version suffix |
|
||||
| [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 |
|
||||
|
||||
```
|
||||
block/buzz (source)
|
||||
├─► buzz-releases (desktop + mobile builds → Artifactory, GitHub, Mobile Releases)
|
||||
├─► 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)
|
||||
```
|
||||
|
||||
See [RELEASING.md](RELEASING.md) for the desktop release flow and
|
||||
[CONTRIBUTING.md § Ecosystem](CONTRIBUTING.md#ecosystem) for contributor
|
||||
access information.
|
||||
|
||||
---
|
||||
|
||||
## Repo Structure
|
||||
|
||||
```
|
||||
crates/
|
||||
# Relay + core
|
||||
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
|
||||
buzz-search # Postgres FTS full-text search
|
||||
buzz-audit # Hash-chain audit log
|
||||
buzz-media # Blossom/S3 media storage
|
||||
# Agent surface
|
||||
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)
|
||||
# Clients + interop
|
||||
buzz-pair-relay # Ephemeral sidecar relay for NIP-AB device pairing
|
||||
buzz-pairing-cli # CLI for NIP-AB device pairing interop testing
|
||||
git-sign-nostr # Sign git objects with a Nostr key
|
||||
git-credential-nostr # Git credential helper for Nostr-authed push/fetch
|
||||
# Tooling + shared
|
||||
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
|
||||
sprig # All-in-one harness bundling ACP, agent, and dev MCP
|
||||
|
||||
desktop/ # Tauri 2 + React 19 desktop app
|
||||
web/ # Browser web client (repo browser, served by the relay)
|
||||
mobile/ # Flutter mobile app
|
||||
migrations/ # SQL migrations (auto-applied on relay startup)
|
||||
scripts/ # Dev tooling
|
||||
.env.example # Config template — copy to .env before running
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
. ./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
|
||||
```
|
||||
|
||||
See CONTRIBUTING.md for full setup details and dependency requirements.
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates
|
||||
|
||||
Run `just ci` before every PR — it runs `fmt` + `clippy` + desktop lint +
|
||||
unit tests + builds. Clippy passing does not mean fmt passes; run both.
|
||||
|
||||
Run `just test` for integration tests if you touched `buzz-relay`,
|
||||
`buzz-db`, or `buzz-auth` — these require a running Postgres and Redis.
|
||||
|
||||
**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
|
||||
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
|
||||
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`.
|
||||
|
||||
**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.
|
||||
|
||||
Additional rules:
|
||||
- No `unsafe` code
|
||||
- Do not introduce new `unwrap()` or `expect()` in production paths — use `?` and proper error types
|
||||
- New public API must have doc comments
|
||||
|
||||
---
|
||||
|
||||
## Key Patterns
|
||||
|
||||
**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.
|
||||
|
||||
**Prefer Nostr events over new HTTP endpoints**: For new feature work, model
|
||||
the operation as a Nostr event (new kind in `buzz-core/src/kind.rs`, handler
|
||||
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:
|
||||
|
||||
- `POST /events` — submit any signed event (same path the WebSocket uses).
|
||||
- `POST /query` — Nostr REQ filters over HTTP. NIP-50 `search` filters
|
||||
are routed to `buzz-search` (Postgres FTS) automatically.
|
||||
- `POST /count` — Nostr COUNT filters over HTTP.
|
||||
|
||||
If you find yourself reaching for a new HTTP endpoint, first check whether
|
||||
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
|
||||
|
||||
**Event kinds**: All event kind integers are defined in
|
||||
`buzz-core/src/kind.rs`. New features get new kind integers — add them here
|
||||
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.
|
||||
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`.
|
||||
|
||||
**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.
|
||||
|
||||
**Workflow conditions**: `buzz-workflow` uses
|
||||
[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.
|
||||
|
||||
---
|
||||
|
||||
## Agent CLI (`buzz-cli`)
|
||||
|
||||
`buzz` is the agent-first CLI. Auth env vars
|
||||
(`BUZZ_RELAY_URL`, `BUZZ_PRIVATE_KEY`, `BUZZ_AUTH_TAG`) are auto-injected
|
||||
by the ACP harness into managed agent subprocesses. In development, set
|
||||
`BUZZ_PRIVATE_KEY` and `BUZZ_RELAY_URL` in your environment manually.
|
||||
|
||||
### Building the CLI
|
||||
|
||||
```bash
|
||||
cargo build --release -p buzz-cli
|
||||
```
|
||||
|
||||
Binary location: `./target/release/buzz`. Add `./target/release` to `PATH`
|
||||
or invoke with the full path.
|
||||
|
||||
### Deep Links
|
||||
|
||||
`buzz://message?channel=<uuid>&id=<hex>` links reference a specific message
|
||||
thread. To read the linked thread:
|
||||
|
||||
```bash
|
||||
buzz messages thread --channel <uuid> --event <hex> --format compact
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
All reads return sig-stripped JSON arrays; all writes return
|
||||
`{event_id, accepted, message}`; creates add the entity ID. Exit codes:
|
||||
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:
|
||||
`buzz --format compact channels list`, NOT `buzz channels list --format compact`.
|
||||
|
||||
See `crates/buzz-cli/TESTING.md` for the full live-testing runbook.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
just test-unit # unit tests, no infrastructure needed
|
||||
just test # full integration suite (requires Postgres + Redis)
|
||||
```
|
||||
|
||||
E2E tests live in `crates/buzz-test-client/tests/`:
|
||||
- `e2e_relay.rs` — WebSocket relay protocol
|
||||
- `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)
|
||||
|
||||
Desktop E2E: `cd desktop && pnpm exec playwright test`
|
||||
|
||||
See [TESTING.md](TESTING.md) for the full multi-agent E2E guide.
|
||||
|
||||
### PR Screenshots
|
||||
|
||||
> **Do NOT use `buzz upload`, the relay media endpoint, or any third-party
|
||||
> image host for PR screenshots.** Relay media URLs fail through GitHub's camo
|
||||
> 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.
|
||||
|
||||
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):
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Use `--messages` to inject content into a channel before capture. The JSON file
|
||||
is an array of objects — `channelName` and `content` are required, all other
|
||||
fields are optional and passed through to `__BUZZ_E2E_EMIT_MOCK_MESSAGE__`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"channelName": "random",
|
||||
"content": "Hey @tyler check this out",
|
||||
"pubkey": "953d...",
|
||||
"kind": 40002,
|
||||
"mentionPubkeys": ["deadbeef..."],
|
||||
"extraTags": [["broadcast", "1"], ["e", "some-root-id"]],
|
||||
"parentEventId": "abc123"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
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']"
|
||||
```
|
||||
|
||||
Available mock channels: `general`, `random`, `design`, `sales`, `engineering`,
|
||||
`agents`, `watercooler`, `announcements`, `alice-tyler`, `bob-tyler`.
|
||||
|
||||
`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):
|
||||
|
||||
```bash
|
||||
./scripts/post-screenshots.sh 803 test-results/screenshots
|
||||
./scripts/post-screenshots.sh 803 test-results/screenshots body.md # custom body prepended
|
||||
```
|
||||
|
||||
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}}
|
||||
```
|
||||
|
||||
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
|
||||
gh pr view <pr> --repo block/buzz --json comments \
|
||||
--jq '.comments[] | select(.body | test("pr-<pr>--")) | {id, url}'
|
||||
gh api -X DELETE repos/block/buzz/issues/comments/<stale-comment-id>
|
||||
```
|
||||
|
||||
Branch cleanup when fully done: `git push origin --delete agent-screenshots/<username>`.
|
||||
|
||||
### 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`.
|
||||
|
||||
**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`.
|
||||
|
||||
**Stale server:** `reuseExistingServer: true` means a previous build's server
|
||||
serves old code. Kill port 4173 and re-run `pnpm build:e2e` before re-running
|
||||
tests after code changes.
|
||||
|
||||
**`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
|
||||
`__BUZZ_E2E_EMIT_MOCK_MESSAGE__` — messages are silently dropped without a
|
||||
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()`
|
||||
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):
|
||||
|
||||
```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.
|
||||
|
||||
**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.
|
||||
|
||||
**`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
|
||||
description. See [PR #803](https://github.com/block/buzz/pull/803).
|
||||
|
||||
---
|
||||
|
||||
## Common Gotchas
|
||||
|
||||
1. **Kind `39000` for channel metadata, not `41`** — kind 41 is NIP-01 (unused). All kinds defined in `buzz-core/src/kind.rs`.
|
||||
2. **Relay queries must specify `kinds`** — omitting `kinds` triggers the p-gate (403). Always include explicit kind filters.
|
||||
3. **`messages search` must include `--kinds`** — an open-ended search (no kinds) hits the relay p-gate and returns 403. Pass at least `--kinds 9,45001,45003` to scope the query.
|
||||
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.
|
||||
6. **Desktop Tauri fmt fails in worktrees and blocks commits** — the pre-commit hook runs `just desktop-tauri-fmt`, which fails in git worktrees because `cargo fmt` resolves workspace paths relative to the worktree root. Run `just desktop-tauri-fmt` from the main checkout to apply the fix, then re-stage and commit. CI is unaffected.
|
||||
7. **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.
|
||||
|
||||
---
|
||||
|
||||
## Desktop App
|
||||
|
||||
The desktop app is Tauri 2 + React 19 + Vite + Tailwind CSS. Features are
|
||||
organized under `desktop/src/features/`. Biome handles linting and formatting.
|
||||
|
||||
```bash
|
||||
just desktop-dev # web-only dev server (faster iteration)
|
||||
just dev # full Tauri app with native shell
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
### Community Switching
|
||||
|
||||
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.
|
||||
|
||||
**Module-level singletons must be explicitly reset.** React remounting only
|
||||
clears React state (useState, useRef, context). Module-level variables (Maps,
|
||||
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`.
|
||||
|
||||
Current singletons that are reset on relay boundary changes (same-relay
|
||||
reconnects preserve pending avatar verification work):
|
||||
- `relayClient.disconnect()` — WebSocket teardown + promise rejection
|
||||
- `resetRateLimitGate()` — clears any active rate-limit window from the old relay
|
||||
- `clearAllDrafts()` — message draft cache
|
||||
- `resetAgentObserverStore()` — agent observer relay store
|
||||
- `resetActiveAgentTurnsStore()` — active agent turn timers
|
||||
- `resetAgentWorkingSignal()` — agent working indicator signal
|
||||
- `resetAvatarProfileSync()` — pending verified-avatar profile writes
|
||||
- `resetAvatarPresentations()` — avatar probes, previews, and Retry toasts
|
||||
- `resetSidebarRelayConnectionCardState()` — sidebar relay card dismiss state
|
||||
- `resetMediaCaches()` — proxy port and relay origin caches
|
||||
- `resetVideoPlayerState()` — video player singleton
|
||||
- `resetRenderScopedReactionHydration()` — reaction hydration cache
|
||||
- `clearSearchHitEventCache()` — search result event cache
|
||||
- `clearMarkdownNodeCache()` — markdown parse-node cache
|
||||
- `resetLinkPreviewTitleCache()` — link preview title cache (Buzz entity titles come from relay events)
|
||||
|
||||
**If you add a new module-level cache, Map, or class instance that holds
|
||||
community-scoped data, you must add its reset to `resetCommunityState()`.**
|
||||
Failure to do so causes data from the old community to leak into the new one.
|
||||
|
||||
Key files:
|
||||
- `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`)
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
- **NEVER use `StatefulWidget`** — favor Riverpod for state and always use
|
||||
`HookConsumerWidget` or `ConsumerWidget` with `flutter_hooks` for local state.
|
||||
- **NEVER run `flutter run`, `flutter build`, `flutter clean`, or
|
||||
`flutter upgrade`** — only `flutter test`, `flutter analyze`, and
|
||||
`dart format` are safe for agents to run.
|
||||
- **Do NOT use `print()`** — use `debugPrint()` or structured logging.
|
||||
- Prefer `context.colors` and `context.textTheme` (via theme extensions)
|
||||
over raw `Theme.of(context)` calls.
|
||||
- **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.**
|
||||
- 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
|
||||
```
|
||||
|
||||
Or from repo root: `just mobile-fmt` (auto-fix), `just mobile-check` (lint + fmt check), `just mobile-test` (tests).
|
||||
|
||||
To run the app locally (starts Docker, relay, iOS simulator automatically):
|
||||
|
||||
```bash
|
||||
just mobile-dev
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — setup, code style, PR process, how to add event kinds / CLI subcommands / HTTP endpoints
|
||||
- [TESTING.md](TESTING.md) — multi-agent E2E test guide
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) — system design and component relationships
|
||||
- [RELEASING.md](RELEASING.md) — release process: `release-desktop`, `release-relay`, `scripts/mobile-release.sh`, candidate tags, internal builds
|
||||
- [README.md](README.md) — project overview and quick start
|
||||
Reference in New Issue
Block a user