mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-10 07:27:23 +02:00
158 lines
6.7 KiB
Markdown
158 lines
6.7 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## Project Overview
|
|
|
|
Grimoire is a Nostr protocol explorer and developer tool. It's a tiling window manager interface where each window is a Nostr "app" (profile viewer, event feed, NIP documentation, etc.). Commands are launched Unix-style via Cmd+K palette.
|
|
|
|
**Stack**: React 19 + TypeScript + Vite + TailwindCSS + Jotai + Dexie + Applesauce
|
|
|
|
## Core Architecture
|
|
|
|
### Dual State System
|
|
|
|
**UI State** (`src/core/state.ts` + `src/core/logic.ts`):
|
|
- Jotai atom persisted to localStorage
|
|
- Pure functions for all mutations: `(state, payload) => newState`
|
|
- Manages workspaces, windows, layout tree, active account
|
|
|
|
**Nostr State** (`src/services/event-store.ts`):
|
|
- Singleton `EventStore` from applesauce-core
|
|
- Single source of truth for all Nostr events
|
|
- Reactive: components subscribe via hooks, auto-update on new events
|
|
- Handles replaceable events automatically (profiles, contact lists, etc.)
|
|
|
|
**Relay State** (`src/services/relay-liveness.ts`):
|
|
- Singleton `RelayLiveness` tracks relay health across sessions
|
|
- Persisted to Dexie `relayLiveness` table
|
|
- Maintains failure counts, backoff states, last success/failure times
|
|
- Prevents repeated connection attempts to dead relays
|
|
|
|
**Critical**: Don't create new EventStore, RelayPool, or RelayLiveness instances - use the singletons in `src/services/`
|
|
|
|
### Window System
|
|
|
|
Windows are rendered in a recursive binary split layout (via `react-mosaic-component`):
|
|
- Each window has: `id` (UUID), `appId` (type identifier), `title`, `props`
|
|
- Layout is a tree: leaf nodes are window IDs, branch nodes split space
|
|
- **Never manipulate layout tree directly** - use callbacks from mosaic
|
|
|
|
Workspaces are virtual desktops, each with its own layout tree.
|
|
|
|
### Command System
|
|
|
|
`src/types/man.ts` defines all commands as Unix man pages:
|
|
- Each command has an `appId` (which app to open) and `argParser` (CLI → props)
|
|
- Parsers can be async (e.g., resolving NIP-05 addresses)
|
|
- Command pattern: user types `profile alice@example.com` → parser resolves → opens ProfileViewer with props
|
|
|
|
**Global Flags** (`src/lib/global-flags.ts`):
|
|
- Global flags work across ALL commands and are extracted before command-specific parsing
|
|
- `--title "Custom Title"` - Override the window title (supports quotes, emoji, Unicode)
|
|
- Example: `profile alice --title "👤 Alice"`
|
|
- Example: `req -k 1 -a npub... --title "My Feed"`
|
|
- Position independent: can appear before, after, or in the middle of command args
|
|
- Tokenization uses `shell-quote` library for proper quote/whitespace handling
|
|
- Display priority: `customTitle` > `dynamicTitle` (from DynamicWindowTitle) > `appId.toUpperCase()`
|
|
|
|
### Reactive Nostr Pattern
|
|
|
|
Applesauce uses RxJS observables for reactive data flow:
|
|
1. Events arrive from relays → added to EventStore
|
|
2. Queries/hooks subscribe to EventStore observables
|
|
3. Components re-render automatically when events update
|
|
4. Replaceable events (kind 0, 3, 10000-19999, 30000-39999) auto-replace older versions
|
|
|
|
Use hooks like `useProfile()`, `useNostrEvent()`, `useTimeline()` - they handle subscriptions.
|
|
|
|
## Key Conventions
|
|
|
|
- **Path Alias**: `@/` = `./src/`
|
|
- **Styling**: Tailwind + HSL CSS variables (theme tokens defined in `index.css`)
|
|
- **Types**: Prefer types from `applesauce-core`, extend in `src/types/` when needed
|
|
- **File Organization**: By domain (`nostr/`, `ui/`, `services/`, `hooks/`, `lib/`)
|
|
- **State Logic**: All UI state mutations go through `src/core/logic.ts` pure functions
|
|
|
|
## Important Patterns
|
|
|
|
**Adding New Commands**:
|
|
1. Add entry to `manPages` in `src/types/man.ts`
|
|
2. Create parser in `src/lib/*-parser.ts` if argument parsing needed
|
|
3. Create viewer component for the `appId`
|
|
4. Wire viewer into window rendering (`WindowTitle.tsx`)
|
|
|
|
**Working with Nostr Data**:
|
|
- Event data comes from singleton EventStore (reactive)
|
|
- Metadata cached in Dexie (`src/services/db.ts`) for offline access
|
|
- Active account stored in Jotai state, synced via `useAccountSync` hook
|
|
- Use inbox/outbox relay pattern for user relay lists
|
|
|
|
**Event Rendering**:
|
|
- Feed renderers: `KindRenderer` component with `renderers` registry in `src/components/nostr/kinds/index.tsx`
|
|
- Detail renderers: `DetailKindRenderer` component with `detailRenderers` registry
|
|
- Registry pattern allows adding new kind renderers without modifying parent components
|
|
- Falls back to `DefaultKindRenderer` or feed renderer for unregistered kinds
|
|
- **Naming Convention**: Use human-friendly names for renderers (e.g., `LiveActivityRenderer` instead of `Kind30311Renderer`) to make code understandable without memorizing kind numbers
|
|
- Feed renderer: `[Name]Renderer.tsx` (e.g., `LiveActivityRenderer.tsx`)
|
|
- Detail renderer: `[Name]DetailRenderer.tsx` (e.g., `LiveActivityDetailRenderer.tsx`)
|
|
|
|
**Mosaic Layout**:
|
|
- Layout mutations via `updateLayout()` callback only
|
|
- Don't traverse or modify layout tree manually
|
|
- Adding/removing windows handled by `logic.ts` functions
|
|
|
|
**Error Boundaries**:
|
|
- All event renderers wrapped in `EventErrorBoundary` component
|
|
- Prevents one broken event from crashing entire feed or detail view
|
|
- Provides diagnostic UI with retry capability and error details
|
|
- Error boundaries auto-reset when event changes
|
|
|
|
## Testing
|
|
|
|
**Test Framework**: Vitest with node environment
|
|
|
|
**Running Tests**:
|
|
```bash
|
|
npm test # Watch mode (auto-runs on file changes)
|
|
npm run test:ui # Visual UI for test exploration
|
|
npm run test:run # Single run (CI mode)
|
|
```
|
|
|
|
**Test Conventions**:
|
|
- Test files: `*.test.ts` or `*.test.tsx` colocated with source files
|
|
- Focus on testing pure functions and parsing logic
|
|
- Use descriptive test names that explain behavior
|
|
- Group related tests with `describe` blocks
|
|
|
|
**What to Test**:
|
|
- **Parsers** (`src/lib/*-parser.ts`): All argument parsing logic, edge cases, validation
|
|
- **Pure functions** (`src/core/logic.ts`): State mutations, business logic
|
|
- **Utilities** (`src/lib/*.ts`): Helper functions, data transformations
|
|
- **Not UI components**: React components tested manually (for now)
|
|
|
|
**Example Test Structure**:
|
|
```typescript
|
|
describe("parseReqCommand", () => {
|
|
describe("kind flag (-k, --kind)", () => {
|
|
it("should parse single kind", () => {
|
|
const result = parseReqCommand(["-k", "1"]);
|
|
expect(result.filter.kinds).toEqual([1]);
|
|
});
|
|
|
|
it("should deduplicate kinds", () => {
|
|
const result = parseReqCommand(["-k", "1,3,1"]);
|
|
expect(result.filter.kinds).toEqual([1, 3]);
|
|
});
|
|
});
|
|
});
|
|
```
|
|
|
|
## Critical Notes
|
|
|
|
- React 19 features in use (ensure compatibility)
|
|
- LocalStorage persistence has quota handling built-in
|
|
- Dark mode is default (controlled via HTML class)
|
|
- EventStore handles event deduplication and replaceability automatically
|
|
- Run tests before committing changes to parsers or core logic
|