···11+# Agent Instructions
22+33+## Project Context
44+55+Tatami is a desktop GUI client for Jujutsu (jj) version control. Tauri v2 + React frontend with Rust backend.
66+77+## Key Documentation
88+99+- **[DEVELOPMENT.md](./DEVELOPMENT.md)** - Data architecture and patterns (MUST READ before modifying data layer)
1010+- **[../../CLAUDE.md](../../CLAUDE.md)** - Build commands and project structure
1111+1212+## Linting
1313+1414+### Biome
1515+Standard linting via `biome.jsonc`. Run with `bun run lint`.
1616+1717+### ast-grep
1818+Architecture rules in `rules/`. Run with:
1919+2020+```bash
2121+# Scan all rules
2222+sg scan
2323+2424+# Scan single rule
2525+sg scan --rule rules/no-direct-ipc-data-fetch.yml
2626+```
2727+2828+## Data Flow (Critical)
2929+3030+```
3131+Components → useLiveQuery (instant) → TanStack DB Collections
3232+ ↑
3333+ Batch Loader (debounced)
3434+ ↑
3535+ Tauri IPC (batched, expensive!)
3636+```
3737+3838+**Principle: All reads are local. All IPC calls are batched.**
3939+4040+See [DEVELOPMENT.md](./DEVELOPMENT.md) for details.
4141+4242+## Working with Issues
4343+4444+This project uses `fp` for issue tracking:
4545+4646+```bash
4747+fp tree # View issue hierarchy
4848+fp issue show <id> # View issue details
4949+fp issue list --status todo # List open issues
5050+```
+152
apps/desktop/DEVELOPMENT.md
···11+# Data Architecture: Local-First with Batch IPC
22+33+## Principle
44+55+**All data reads are local. All IPC calls are batched.**
66+77+```
88+┌────────────────────────────────────────────────────────────┐
99+│ Components │
1010+│ │
1111+│ useDiff(changeId) useChanges(changeId) │
1212+│ usePrefetch() (always instant from local DB) │
1313+└────────────────────────────────────────────────────────────┘
1414+ │
1515+ ▼
1616+┌────────────────────────────────────────────────────────────┐
1717+│ TanStack DB │
1818+│ │
1919+│ diffsCollection changesCollection │
2020+│ (all diffs, keyed (all file lists, keyed │
2121+│ by repoPath:changeId) by repoPath:changeId) │
2222+└────────────────────────────────────────────────────────────┘
2323+ ▲
2424+ │ batch sync
2525+ │
2626+┌────────────────────────────────────────────────────────────┐
2727+│ Batch Loader │
2828+│ │
2929+│ - Queues IDs that need loading │
3030+│ - Debounces (50ms) to collect multiple requests │
3131+│ - Batches into single IPC call │
3232+│ - Syncs results → collections │
3333+└────────────────────────────────────────────────────────────┘
3434+ │
3535+ │ single batched invoke()
3636+ ▼
3737+┌────────────────────────────────────────────────────────────┐
3838+│ Tauri IPC (expensive!) │
3939+│ │
4040+│ getDiffsBatch({ changeIds: string[] }) │
4141+│ getChangesBatch({ changeIds: string[] }) │
4242+│ │
4343+│ ~50-200ms per call - minimize these! │
4444+└────────────────────────────────────────────────────────────┘
4545+```
4646+4747+## Rules
4848+4949+### 1. Components Never Call IPC Directly
5050+5151+```typescript
5252+// ❌ WRONG
5353+import { getRevisionDiff } from '@/tauri-commands';
5454+const diff = await getRevisionDiff(repoPath, changeId);
5555+5656+// ✅ RIGHT
5757+import { useDiff } from '@/db';
5858+const { data: diff } = useDiff(repoPath, changeId);
5959+```
6060+6161+### 2. Use Unified Collections, Not Per-Entity
6262+6363+```typescript
6464+// ❌ WRONG - creates N collections, causes GC issues
6565+function getRevisionDiffCollection(repoPath, changeId) {
6666+ return createCollection({
6767+ queryKey: ["diff", repoPath, changeId], // Per changeId!
6868+ ...
6969+ });
7070+}
7171+7272+// ✅ RIGHT - single collection, query locally
7373+const diffsCollection = createCollection({
7474+ queryKey: ["diffs"],
7575+ getKey: (d) => `${d.repoPath}:${d.changeId}`,
7676+});
7777+7878+// Query with filter - instant local read
7979+useLiveQuery(diffsCollection, q =>
8080+ q.where('changeId', '==', selectedId)
8181+);
8282+```
8383+8484+### 3. Batch IPC Calls
8585+8686+```typescript
8787+// ❌ WRONG - N IPC calls = N × 200ms
8888+for (const id of changeIds) {
8989+ await getRevisionDiff(repoPath, id);
9090+}
9191+9292+// ✅ RIGHT - 1 IPC call = 200ms total
9393+const diffs = await getDiffsBatch(repoPath, changeIds);
9494+9595+// ✅ EVEN BETTER - use batch loader with debounce
9696+const { prefetchDiffs } = usePrefetch(repoPath);
9797+prefetchDiffs(changeIds); // Queued, debounced, batched
9898+```
9999+100100+### 4. Prefetch Strategically
101101+102102+Prefetch data before user needs it:
103103+104104+```typescript
105105+// Prefetch visible range
106106+useEffect(() => {
107107+ const visibleIds = visibleRevisions.map(r => r.change_id);
108108+ prefetchDiffs(visibleIds);
109109+}, [visibleRevisions]);
110110+111111+// Prefetch around selection for smooth navigation
112112+useEffect(() => {
113113+ const nearbyIds = getNearbyRevisionIds(selectedIndex, ±5);
114114+ prefetchDiffs(nearbyIds);
115115+}, [selectedIndex]);
116116+117117+// Prefetch search results
118118+useEffect(() => {
119119+ if (searchResults.length > 0) {
120120+ prefetchDiffs(searchResults.slice(0, 20).map(r => r.change_id));
121121+ }
122122+}, [searchResults]);
123123+```
124124+125125+### 5. File Watcher Handles Invalidation
126126+127127+```typescript
128128+// When repo changes, clear and re-fetch
129129+listen('repo-changed', (repoPath) => {
130130+ // Clear affected data from collections
131131+ clearCollectionsForRepo(repoPath);
132132+133133+ // Component effects will re-trigger prefetch
134134+ // No manual refetch needed
135135+});
136136+```
137137+138138+## Why This Architecture?
139139+140140+| Problem | Old Approach | New Approach |
141141+|---------|--------------|--------------|
142142+| IPC latency | 1 call per selection (200ms wait) | Batched prefetch (instant reads) |
143143+| GC issues | Per-entity collections get cleaned up | Unified collections persist |
144144+| Prefetch broken | Collections GC'd before use | Data stays in collection |
145145+| Search results | Fetch on select (slow) | Prefetch on search (instant) |
146146+147147+## Files
148148+149149+- `src/db.ts` - Collections, batch loaders, hooks
150150+- `src/lib/batch-loader.ts` - BatchLoader class
151151+- `src/tauri-commands.ts` - IPC wrappers (batch APIs)
152152+- `src-tauri/src/lib.rs` - Rust batch commands
···11-# Rule: Discourage useEffect for data fetching
22-id: no-useeffect-data-fetching
33-language: typescript
44-severity: warning
55-message: "useEffect for data fetching is discouraged. Use TanStack DB (useLiveQuery) instead."
66-rule:
77- pattern: useEffect($$$ARGS)