# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What This Is

Vega is a cross-platform Nostr desktop client built with Tauri 2.0 (Rust) + React + TypeScript. It connects to Nostr relays via NDK (Nostr Dev Kit) and aims for Telegram Desktop-quality UX. Long-form content (NIP-23) is a first-class, distinguishing feature — not an afterthought.

## AgentDocs

When the user shares an AgentDocs URL, fetch the raw content using:
```
curl https://agentdocs.exe.xyz/api/shared/<token>/raw
```
The `<token>` is the hash at the end of the shared URL. This returns the document as plain text/markdown with embedded image references.

## Commands

```bash
npm run tauri dev       # Run full app with hot reload (recommended for development)
npm run dev             # Vite-only dev server (no Tauri window)
npm run build           # TypeScript compile + Vite build
npm run tauri build     # Production binary
```

Prerequisites: Node.js 20+, Rust stable, `@tauri-apps/cli`

## Releasing a New Version

**Order matters — do not tag before bumping versions.**

1. Bump version to `X.Y.Z` in all four files (they must stay in sync):
   - `src-tauri/tauri.conf.json` → `"version": "X.Y.Z"`
   - `package.json` → `"version": "X.Y.Z"`
   - `src-tauri/Cargo.toml` → `version = "X.Y.Z"`
   - `PKGBUILD` → `pkgver=X.Y.Z`
2. Update the release notes in `.github/workflows/release.yml`
3. Commit: `git commit -m "Bump to vX.Y.Z — <summary>"`
4. Tag: `git tag vX.Y.Z`
5. Push: `git push origin main vX.Y.Z`
6. Update AUR: in `/home/hoornet/projects/vega-aur/`, bump `pkgver=X.Y.Z` in `PKGBUILD`, then:
   ```bash
   makepkg --printsrcinfo > .SRCINFO
   git add PKGBUILD .SRCINFO && git commit -m "Bump to vX.Y.Z" && git push
   ```

CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS ARM). All jobs must complete for `latest.json` to be assembled.

**Hard-won CI rules:**
- `includeUpdaterJson: true` must be set in tauri-action — without it `latest.json` is never uploaded and the auto-updater silently does nothing
- `bundle.createUpdaterArtifacts: true` must be set in `tauri.conf.json` — without it `.sig` files are never generated even if the signing key is set (Tauri 2 requirement)
- Valid `bundle.targets`: `"deb"`, `"rpm"`, `"nsis"`, `"msi"`, `"dmg"`, `"app"` — do NOT add `"updater"` (that's a plugin, not a bundle format)
- `"app"` MUST be in `bundle.targets` or the macOS auto-updater silently breaks: the `.app.tar.gz` updater artifact (and its `.sig`) is generated from the `app` target, NOT `dmg`. With only `dmg`, the `.app` is built as a throwaway intermediate, no `.app.tar.gz` is emitted, and `latest.json` gets no `darwin-aarch64` entry. `app`/`dmg` are macOS-only and skipped on Linux/Windows builds. This was broken from before v0.12.9 through v0.12.13; fixed in v0.12.14.
- macOS runner is `macos-latest` (ARM only) — `macos-12`/`macos-13` are gone
- Verify after CI: `https://api.github.com/repos/hoornet/vega/releases/latest` (check for `.sig` assets + `latest.json`)
- **Release notes in `release.yml` can trigger a GitHub workflow startup failure** ("workflow file issue", 0 jobs) that passes `yaml.safe_load` AND `actionlint`. Hit in v0.14.1 by 5 lines of plain-markdown release notes in the `releaseBody` block scalar; root cause never found. Keep the workflow's `releaseBody` minimal and known-good; put rich per-version notes on the release afterward with `gh release edit vX.Y.Z --notes-file f.md` — the release-body path doesn't go through the workflow parser.

## CommonJS default imports (bundler gotcha)

`react-qr-code` (and any CommonJS-only lib) can crash a view: under Vite 8 / Rolldown, `import X from "cjs-lib"` may resolve to the module namespace object `{ ..., default }` instead of the component, so `<X/>` throws React #130 ("element type is invalid… got: object") and the error boundary blanks the app. This only shows in built bundles, not `npm run tauri dev`. `AboutView.tsx` keeps a defensive unwrap (`(X as ...).default ?? X`) — don't revert it to a plain default import, and use the same guard for other CJS libs rendered as JSX.

## Architecture

**Frontend** (`src/`): React 19 + TypeScript + Vite + Tailwind CSS 4

- `src/App.tsx` — root component; shows `OnboardingFlow` for new users, then view routing via UI store
- `src/stores/` — Zustand stores per domain: `feed.ts`, `user.ts`, `ui.ts`, `lightning.ts`, `drafts.ts`, `relayHealth.ts`, `bookmark.ts`, `toast.ts`
- `src/lib/nostr/` — NDK wrapper split into domain modules (`core.ts`, `notes.ts`, `social.ts`, `articles.ts`, `engagement.ts`, `dms.ts`, `bookmarks.ts`, `muting.ts`, `search.ts`, `relays.ts`, `trending.ts`, `vertex.ts`); barrel `index.ts` re-exports all; all Nostr calls go through here
- `src/lib/themes.ts` — Color theme definitions (7 themes) and `applyTheme()` utility
- `src/lib/lightning/` — NWC client (`nwc.ts`); Lightning payment logic
- `src/hooks/` — `useProfile.ts`, `useReactions.ts` (grouped emoji reactions with throttled fetch queue), `useReputation.ts` (Vertex WoT with cache)
- `src/lib/debug.ts` — Dev-only logger (silent in production builds)
- `src/components/feed/` — Feed, NoteCard, NoteContent, NoteActions, InlineReplyBox, TextSegments, MediaCards, ComposeBox
- `src/components/profile/` — ProfileView, EditProfileForm, ImageField, Nip05Field, ProfileMediaGallery
- `src/components/thread/` — ThreadView
- `src/components/search/` — SearchView (advanced search with modifiers, NIP-50, hashtag, people, articles)
- `src/lib/search.ts` — Advanced search query parser (by:, has:, is:, kind:, since:, until:, OR)
- `src/lib/nostr/relayHealth.ts` — Relay health checker (NIP-11, latency probing, status classification)
- `src/components/article/` — ArticleEditor, ArticleView, ArticleFeed, ArticleCard, MarkdownToolbar (NIP-23)
- `src/components/bookmark/` — BookmarkView
- `src/components/media/` — MediaFeed (media discovery with tab filtering)
- `src/components/zap/` — ZapModal
- `src/components/onboarding/` — OnboardingFlow (welcome, create key, backup, login)
- `src/components/shared/` — RelaysView (relay health dashboard + recommendations), SettingsView (themes + font size + NWC + identity + data export), EmojiPicker (categorized emoji insertion)
- `src/components/sidebar/` — Sidebar navigation

**Backend** (`src-tauri/`): Rust + Tauri 2.0

- `src-tauri/src/lib.rs` — Tauri app init and command registration
- Rust commands must return `Result<T, String>`
- OS keychain via `keyring` crate — `store_nsec`, `load_nsec`, `delete_nsec` commands
- SQLite note/profile cache via `rusqlite`
- File uploads handled entirely in TypeScript with NIP-98 auth (Rust upload_file removed in v0.7.0)
- Future: lightning node integration

## Key Conventions (from AGENTS.md)

- Functional React components only — no class components
- Never use `any` — define types in `src/types/`
- Tailwind classes only — no inline styles, except unavoidable WebkitUserSelect
- Private keys stored in OS keychain via Rust `keyring` crate; nsec persists across restarts
- New Zustand stores per domain when adding features
- NDK interactions only through `src/lib/nostr/` wrapper
- Lightning/NWC only through `src/lib/lightning/` wrapper

## NIP Priority Reference

- **P1 (core):** NIP-01, 02, 03, 10, 11, 19, 21, 25, 27, 50
- **P2 (monetization):** NIP-47 (NWC/Lightning), NIP-57 (zaps), NIP-65 (relay lists)
- **P3 (advanced):** NIP-04/44 (DMs), NIP-11 (relay info — used by health checker), NIP-23 (articles), NIP-96 (file storage), NIP-98 (HTTP Auth — implemented for uploads)

## Current State

**Implemented:**
- Onboarding: key generation, nsec backup flow, login with nsec/npub
- Global + following feed, compose, reply, thread view
- Reactions (NIP-25) with **grouped emoji pills** (❤️5 🤙3 🔥2), multi-reaction per note, throttled fetch queue
- Follow/unfollow (NIP-02), contact list publishing
- Profile view + edit (kind 0) with Notes/Articles tab toggle
- Long-form article editor (NIP-23) with **markdown toolbar** (bold, italic, heading, link, image, quote, code, list), **keyboard shortcuts** (Ctrl+B/I/K), **multi-draft management**, **cover image file picker**
- **Article discovery feed** — dedicated "Articles" view in sidebar; Latest/Following tabs
- **Article reader** — markdown rendering, reading time, bookmark, like, zap
- **Article search** — NIP-50 + hashtag search for kind 30023 articles
- **Article cards** — reusable component with title, summary, author, cover thumbnail, reading time, tags
- **NIP-98 HTTP Auth** for image uploads with fallback services (nostr.build, void.cat, nostrimg.com)
- Zaps: NWC wallet connect (NIP-47) + NIP-57 via NDKZapper
- **Advanced search** — query parser with modifiers: `by:author`, `mentions:npub`, `kind:N`, `is:article`, `has:image`, `since:date`, `until:date`, `#hashtag`, `"phrase"`, boolean `OR`; NIP-05 resolution; client-side content filters; search help panel
- Search: NIP-50 full-text, hashtag (#t filter), people, articles, **npub/nprofile direct navigation**
- Settings: color themes (7 presets), font size presets, NWC wallet, notifications, data export, identity, mute lists
- **Relay management** — consolidated Relays view with add/remove individual relays, health checker (NIP-11 info, WebSocket latency, online/slow/offline status), expandable cards with all supported NIPs, per-relay remove button, "Remove dead" workflow, publish relay list (NIP-65)
- **Relay recommendations** — suggest relays based on follows' NIP-65 relay lists; "Discover relays" button with follow count, one-click "Add"
- **Relay status badge** — compact "N/M relays" indicator in feed header with color coding; hover tooltip shows per-relay connection state
- **Toast notifications** — transient status messages for relay connection events (lost, reconnecting, back online)
- **Per-tab "last updated" timestamp** — relative time in feed header, tracked independently per tab (global/following/trending)
- **Subscription debug panel** — Ctrl+Shift+D toggles hidden panel showing NDK uptime, live sub status, per-relay state, feed timestamps, recent diagnostics log
- **Data export** — export bookmarks, follows, and relay list as JSON via native save dialog (Tauri plugin-dialog + plugin-fs)
- **Profile banner polish** — hero-height banner (h-36), click-to-lightbox, avatar overlaps banner edge with ring, loading shimmer
- **Reading list tracking** — read/unread state on bookmarked articles (localStorage-backed), unread dot indicators, sidebar badge, auto-mark-read on open
- **Trending hashtags** — #t tag frequency analysis from recent events; clickable tag pills on search idle screen
- OS keychain integration — nsec persists across restarts via `keyring` crate
- SQLite note + profile cache
- Direct messages (NIP-04 + NIP-17 gift wrap)
- NIP-65 outbox model
- Image lightbox (click to expand, arrow key navigation)
- Bookmark list (NIP-51 kind 10003) with sidebar nav, **Notes/Articles tabs**, article `a` tag support, **read/unread tracking**
- Follow suggestions / discovery (follows-of-follows algorithm)
- Language/script feed filter (Unicode script detection + NIP-32 tags)
- Skeleton loading states, view fade transitions
- Note sharing (nevent URI to clipboard)
- Reply counts on notes
- Media players (video/audio inline, YouTube/Vimeo/Spotify cards)
- Multi-account switcher with keychain-backed session restore
- System tray, keyboard shortcuts, auto-updater
- **Update banner changelog** (v0.13.2) — "What's new" toggle expands the new version's release notes inline (sliced from the cumulative updater body via `latestChangelogSection` in `App.tsx`, rendered with `renderMarkdown`), plus a "Full changelog on GitHub" link
- **Mute-aware search** (v0.13.2) — search results (notes/articles by pubkey + keyword, people by pubkey) honour the mute list, not just the feed
- **Collapsing compose box** (v0.13.2) — single line when idle, expands on focus; uses focus-within blur semantics so toolbar clicks don't collapse it
- **Resizable sidebar** (v0.13.2) — drag right edge (160–360px), double-click reset, width persisted to `wrystr_sidebar_width`
- **NIP-05 verification badges** — cached verification with green checkmark on note cards
- **Dedicated hashtag pages** — clicking #tag opens a live feed, not generic search
- **Keyword muting** — word/phrase mute list, client-side filtering across all views
- **Follow suggestion dismissal** — persistent "don't suggest again" per person
- **Background notification poller** — 60s polling for mentions, zaps, new followers; each type independently toggleable; relay-aware startup (waits for connection before first fetch)
- **Dev-only debug logger** — `debug.log/warn/error` via `src/lib/debug.ts`; uses `import.meta.env.DEV`, silent in production
- **Trending feed polish** — 24h time window, time decay scoring, articles mixed with notes
- **NIP-46 remote signer** — bunker:// URI login, session persistence via toPayload/fromPayload, account switching
- **Media feed** — dedicated "Media" view with All/Videos/Images/Audio tabs; filters notes by embedded media type
- **Profile media gallery** — "Media" tab on profiles with grid layout; images open lightbox, videos/audio navigate to thread
- **Emoji picker** — shared categorized emoji picker (Frequent/Faces/Gestures/Objects/Symbols) in compose box, inline reply, thread reply; emoji reaction picker on note cards via visible + button
- **External link opener** — global click handler intercepts http(s) links and opens in system browser via `@tauri-apps/plugin-opener`
- **Color themes** — 7 built-in themes (Midnight, Light, Catppuccin Mocha, Sepia, Gruvbox, Nord Frost, Hackerman); CSS custom properties swapped at runtime; persisted to localStorage
- **Font size presets** — Small/Normal/Large/Extra Large; CSS zoom scaling on document root; persisted to localStorage
- **Web of Trust** — Vertex DVM integration (kind 5312→6312); personalized "Followed by people you trust" on profiles with clickable follower avatars
- **SQLite-backed notifications** — instant load on startup from local cache; relay diff merged in background; read state persists in DB across restarts
- **SQLite-backed followers cache** — instant load from DB, relay results merged in background; follower count only grows (never lost to partial relay results)
- **SQLite-backed bookmarks cache** — bookmarked notes load instantly from DB; relay fetch fills in any new additions; articles auto-classified to correct tab
- **SQLite-backed articles cache** — articles feed (latest tab) loads instantly from DB
- **Instant own-profile load** — sidebar badge shows name/picture from DB cache immediately, no raw npub on slow relays
- **Retry-on-empty pattern** — followers, profile notes/articles, hashtag feeds retry once after 3s if relays return empty
- **Embedded Nostr relay** — built-in strfry relay with catch-up sync on startup; notes always available locally
- **naddr clickable links** — `nostr:naddr1…` references resolve to clickable named links (article titles)
- **New follower badges** — recently gained followers marked with "new" badge, sorted to top of follows list
- **Batch bookmark fetch** — fetches bookmarked notes with `{ ids: [...] }` filter; debounced kind 10003 publishes prevent race conditions
- **Resilient relay pool** — resetNDK preserves outbox-discovered relay URLs (fixes relay pool dropping to 3)

**Not yet implemented:**
- NIP-96 file storage
- Custom feeds / lists
- Safe Blossom URL auto-detection (temporarily disabled in v0.12.8 after OOM regression — needs HEAD `Content-Type` validation or known-server whitelist before reintroduction)

## App identifier & data migration (v0.14.0)

The identifier is **`com.veganostr.Vega`** (was `com.hoornet.vega` before v0.14.0). It must stay on a domain the project controls — **Flathub rejects anything else**, and it keys the app ID for winget and the native installers too.

- **Never migrate app data from inside `.setup()`.** Tauri builds the config-defined windows *before* invoking the setup hook, and building a webview unconditionally `create_dir_all`s `<LocalData>/<identifier>` — which on Linux is the same directory holding `vega.db`, `relay.db` and localStorage. A migration running in `setup()` therefore always sees a non-empty destination. `migrate_legacy_data_dirs()` runs at the **top of `run()`, before `tauri::Builder`**, and uses the `dirs` crate (no `App` needed). Do not move it back.
- **Never use "destination is empty" as the migration trigger** — it is guaranteed non-empty after one launch, so the failure is self-sealing and unrecoverable. Migrate entry-by-entry; never overwrite an entry that already exists at the destination.
- **localStorage is not in the app data dir on every platform.** Linux → data dir (WebKitGTK). Windows → `%LOCALAPPDATA%` (WebView2 `EBWebView/`), *not* `%APPDATA%`. macOS → `~/Library/WebKit/<bundle-id>`. Migrating only `app_data_dir` silently loses themes, drafts, podcast subs and read-state on Windows and macOS.
- Keys are safe across identifier changes: `KEYRING_SERVICE` is the hardcoded literal `"wrystr"`, independent of the identifier.
- **Changing the identifier makes Windows treat the app as new** — it installs alongside the old version rather than upgrading in place. Document it; it's a one-time manual uninstall for users.
- The v0.10.0 Wrystr→Vega rename did this *without* a migration and silently stranded a full data directory (768 MB found on the dev machine). That is what this machinery exists to prevent.

## Hard-won Linux/WebKitGTK lessons

- **Startup/lifecycle changes must be verified by running the real binary**, not unit tests. The v0.14.0 migration had four passing unit tests and would still have wiped every Linux user's data — the defect was in *when* it ran, which no unit test could see. Run it against a scratch env: `XDG_DATA_HOME=/tmp/scratch ./target/debug/vega`, with data seeded beforehand and asserted after. Note `#[cfg(target_os = "...")]` branches are never type-checked on the build host.
- **WebKitGTK does not evict decoded bitmaps** under memory pressure the way Chromium does. Any path that multiplies `<img>` elements per feed page will translate ~linearly into WebProcess RSS. Validate new "render as image" heuristics (e.g. Blossom SHA-256 URLs) with a real content-type probe before shipping.
- **`MemoryPressureSettings` set on `WebsiteDataManager` only affects the NetworkProcess**, not the WebProcess where decoded bitmaps live. Setting a WebProcess cap requires reaching `WebContext` at construction time (construct-only GObject property) — wry does not currently expose this.
- **Bisect regression windows before investigating root cause.** If memory behavior changed between versions, `git bisect` the release tags first. Four days of WebKit-level investigation was avoidable once the regression was traced to a single commit in v0.12.6.
- **Distinguish caches from leaks.** Oscillating memory = elastic cache (fine). Monotonic growth = leak or uncapped working set (fix it).
