**Compact Relay Item Display:**
- Removed left-side inbox/outbox count indicators (were causing misalignment)
- Replaced "EOSE" text with checkmark icon (✓)
- Event count shown as [N] badge (only if > 0)
- Auth icon now always visible (even for unauthenticated relays)
- Clean right-side layout: [count] [✓] [auth] [wifi]
**Always-Visible Auth Status:**
- Modified getAuthIcon() to always return an icon (never null)
- Unauthenticated relays show subtle shield icon (muted-foreground/40)
- Provides at-a-glance view of auth status for all relays
- Label: "No Authentication Required" for clarity
**Rich Hover Tooltips:**
- Comprehensive tooltip shows all relay details on hover
- Displays: connection status, auth status, subscription state, event count
- Shows inbox/outbox counts when available (moved from inline display)
- Formatted as structured table for easy scanning
- Positioned on left side to avoid blocking content
**Benefits:**
✅ Perfect alignment (no variable-width counts on left)
✅ Cleaner, more scannable visual design
✅ All information still accessible via hover
✅ Consistent icon count (always 2-4 icons per relay)
✅ Easy to spot EOSE status at a glance (green checkmark)
All 639 tests passing.
**CRITICAL FIX for EOSE detection:**
**The Problem:**
- Used pool.subscription(relays, filters) which creates a RelayGroup
- RelayGroup tracks per-relay EOSE internally but only emits ONE "EOSE" when ALL relays finish
- This caused:
1. EOSE indicators taking forever to appear (waiting for slowest relay)
2. REQ stuck in LOADING state when fast relays finish but slow relays never do
3. No way to show per-relay EOSE status accurately
**The Solution:**
Subscribe to each relay individually using pool.relay(url).subscription():
- Each relay subscription emits its own EOSE immediately when that relay finishes
- We track per-relay EOSE in relayStates map with accurate timing
- Overall EOSE is derived when ALL relays reach terminal state (eose/error/disconnected)
- EOSE indicators now appear immediately as each relay finishes
**Technical Details:**
- Changed from: pool.subscription(relays, filters)
- Changed to: relays.map(url => pool.relay(url).subscription(filters))
- Added eoseReceivedRef to track overall EOSE in closures
- Mark specific relay as EOSE when that relay emits "EOSE"
- Calculate overall EOSE when all relays in terminal states
- Use url from subscription context (more reliable than event._relay)
**Benefits:**
✅ Instant per-relay EOSE indicators (no waiting for slowest relay)
✅ Accurate relay state tracking (each relay independent)
✅ REQ transitions to LIVE/CLOSED as soon as all relays finish
✅ Better user feedback (see which relays are done vs still loading)
All 639 tests passing.
**Normalize All Relay URLs:**
- Added trailing slashes to AGGREGATOR_RELAYS constants
- Ensures consistency with RelayStateManager's normalization
- Fixes fallback relay connection state tracking issue
- All hardcoded relay URLs now match normalized keys in relayStates
**Reorganize Relay Item UI:**
- Removed type indicator icons (LinkIcon/Sparkles/Inbox) from individual relay items
- Strategy type is already shown in header, no need to repeat per-item
- Moved inbox/outbox indicators from right side to left side of relay URL
- Left side now shows: inbox count (Mail icon) and/or outbox count (Send icon)
- Right side shows: event count, EOSE indicator, auth status, connection status
- Cleaner, more semantic layout with better visual hierarchy
**Why This Matters:**
The relay URL normalization fix ensures that fallback relays (AGGREGATOR_RELAYS)
now show accurate connection state in the UI. Previously, the non-normalized
URLs couldn't match keys in relayStates, making them appear disconnected even
when connected. This was the root cause of the "fallback relays not tracking"
issue.
All 639 tests passing.
Critical fixes for ReqViewer relay state accuracy:
1. **URL Normalization Fix** (fixes mismatch with CONN):
- Added normalizeRelayURL to normalize all relay URLs in finalRelays
- RelayStateManager normalizes URLs (adds trailing slash, lowercase) but
finalRelays did not, causing lookup failures in relayStates
- Now normalizedRelays is used for all state lookups and passed to
useReqTimelineEnhanced to ensure consistency
- This fixes the bug where ReqViewer showed different connected relay
counts than CONN viewer
2. **EOSE Indicator**:
- Added back EOSE indicator to relay dropdown (was removed in UI redesign)
- Shows subtle "EOSE" text when relay has sent End of Stored Events
- Includes tooltip explaining "End of stored events received"
3. **Muted Icons** (per user request for subtlety):
- Type indicators: blue-500/purple-500 → muted-foreground/60
- Strategy header icons: all → muted-foreground/60
- Section headers: green-500 → muted-foreground
- Connection icons: green-500/yellow-500/red-500 → /70 opacity variants
- Auth icons: same color reduction for consistency
- Maintains semantic meaning while reducing visual noise
All 639 tests passing.
Critical Edge Case Fix:
Previously, when all relays disconnected before sending EOSE, the state
remained stuck in LOADING because overallEoseReceived stayed false.
Solution: Check if all relays are in terminal states
- Terminal states: eose, error, or disconnected
- If all terminal AND no overall EOSE yet, derive state from events:
* No events → FAILED
* Has events, all disconnected, streaming → OFFLINE
* Has events, all disconnected, non-streaming → CLOSED
* Some active, some terminal → PARTIAL
New Test Coverage (5 tests):
1. All relays disconnect before EOSE, no events → FAILED
2. All relays disconnect before EOSE, with events (streaming) → OFFLINE
3. All relays disconnect before EOSE, with events (non-streaming) → CLOSED
4. Some EOSE, others disconnect before EOSE → PARTIAL
5. Mix of EOSE and errors, all terminal → PARTIAL
This fixes the user-reported issue where disconnected relays show LOADING
instead of transitioning to appropriate terminal state.
Tests: 639/639 passing (added 5 new edge case tests)
Replace `break-all` with `break-words overflow-x-auto` to improve
text wrapping behavior in command preview areas. This prevents
words from breaking awkwardly in the middle while still allowing
long commands to be displayed properly.
Changes:
- CreateSpellDialog.tsx: Fixed command preview overflow
- SpellDialog.tsx: Fixed command display overflow
- SpellRenderer.tsx: Fixed detail view command overflow
The new approach breaks at word boundaries when possible and
provides horizontal scrolling for exceptionally long commands.
State Tracking Fixes:
- Sync connection state for ALL relays in query, not just initialized ones
- Defensively initialize missing relay states during sync
- Handle events from unknown relays (defensive initialization)
- Add debug console logs to track state transitions
Relay Type Indicators:
- Explicit relays: Blue link icon (relays specified directly)
- Outbox relays: Purple sparkles (NIP-65 selected)
- Fallback relays: Gray inbox icon (fallback when outbox incomplete)
- Each type has tooltip explaining source
This should fix:
- "0/4 relays but events coming in" bug
- "Stuck in LOADING" when events are arriving
- Missing visibility for relay source types
Tests: 634/634 passing
Previously only showed relays from NIP-65 reasoning array, missing fallback
relays. Now always iterates over finalRelays (actual queried relays) and
looks up NIP-65 info if available.
Fixes:
- Fallback relays now visible in dropdown
- Relays show connection/subscription status regardless of source
- NIP-65 info (inbox/outbox counts) shown when available
- Works for outbox, fallback, and explicit relay configurations
Tests: 634/634 passing
Core Infrastructure:
- Add ReqRelayState and ReqOverallState types for granular state tracking
- Implement deriveOverallState() state machine with 8 query states
- Create useReqTimelineEnhanced hook combining RelayStateManager + event tracking
- Add comprehensive unit tests (27 tests, all passing)
State Machine Logic:
- DISCOVERING: NIP-65 relay selection in progress
- CONNECTING: Waiting for first relay connection
- LOADING: Initial events loading
- LIVE: Streaming with active relays (only when actually connected!)
- PARTIAL: Some relays ok, some failed/disconnected
- OFFLINE: All relays disconnected after being live
- CLOSED: Query completed, all relays closed
- FAILED: All relays failed to connect
UI Updates:
- Single-word status indicators with detailed tooltips
- Condensed relay status into NIP-65 section (no duplicate lists)
- Per-relay subscription state badges (RECEIVING, EOSE, ERROR, OFFLINE)
- Event counts per relay
- Connection + Auth status integrated into single dropdown
Fixes Critical Bug:
- Solves "LIVE with 0 relays" issue (Scenario 5 from analysis)
- Distinguishes real EOSE from relay disconnections
- Accurate status for all 7 edge cases documented in analysis
Technical Approach:
- Hybrid: RelayStateManager for connections + event._relay for tracking
- Works around applesauce-relay catchError bug without forking
- No duplicate subscriptions
- Production-quality error handling
Tests: 27/27 passing including edge case scenarios
Phase 1 of applesauce helpers refactoring plan.
Removed useMemo from direct applesauce helper calls since these helpers
cache their results internally using symbol-based caching. This improves
code clarity without sacrificing performance.
Changes:
- ArticleRenderer.tsx: removed useMemo from getArticleTitle, getArticleSummary
- HighlightRenderer.tsx: removed useMemo from 6 highlight helpers
- HighlightDetailRenderer.tsx: removed useMemo from 6 highlight helpers
- CodeSnippetDetailRenderer.tsx: removed useMemo from 8 NIP-C0 helpers
- ChatView.tsx: removed useMemo from getNip10References, getTagValue
Kept useMemo in LiveActivityRenderer.tsx for parseLiveActivity and related
functions since these don't implement their own caching (noted for future
optimization).
All tests pass (607 tests).
- Created APPLESAUCE_REFACTORING_PLAN.md with detailed analysis
- Updated CLAUDE.md with Applesauce Helpers & Caching section
- Enhanced applesauce-core skill with helper documentation
Key findings:
- Applesauce helpers cache internally using symbols
- No need for useMemo when calling applesauce helpers
- Identified 40+ useMemo instances that can be removed
- Documented available helpers and custom grimoire helpers
- Provided migration strategy and refactoring opportunities
Use fake-indexeddb to provide IndexedDB API in Node.js test environment.
This fixes 10 failing tests in spellbook-storage.test.ts that were
previously blocked by missing IndexedDB.
Changes:
- Add fake-indexeddb as dev dependency
- Create vitest setup file that imports the polyfill
- Update vitest.config.ts to use setup file
- Fix relay-selection.test.ts to clear cache between tests for isolation
Analyze test failures due to missing IndexedDB in Node environment.
Present two options: fake-indexeddb polyfill vs full storage abstraction.
Recommend fake-indexeddb as pragmatic solution.
- Replace mounted boolean flag with AbortController pattern
- Check abort signal before initiating database writes
- Proper cleanup on unmount/pubkey change
This prevents stale data from being written to IndexedDB when:
- Component unmounts during async operations
- Pubkey changes while a fetch is in progress
- Create src/hooks/useStable.ts with:
- useStableValue<T>() - stabilizes any value using JSON.stringify
- useStableArray<T>() - stabilizes string arrays (uses JSON.stringify
for safety, handles arrays with commas in elements)
- useStableFilters<T>() - specialized for Nostr filters
- Update timeline hooks to use stabilization:
- useTimeline.ts - use useStableFilters for filter dependencies
- useReqTimeline.ts - use useStableValue for filter dependencies
- useLiveTimeline.ts - use useStableArray for relay dependencies
Prevents unnecessary re-renders and subscription restarts when
filter/relay objects are recreated with the same content.
- Create src/lib/nostr-kinds.ts with:
- Re-exports from nostr-tools/kinds (isRegularKind, isReplaceableKind, etc.)
- New isParameterizedReplaceableKind() function
- New isAddressableKind() for determining naddr vs nevent encoding
- NIP-01 boundary constants with clarifying comments
- getKindCategory() for display purposes
- Update KindRenderer.tsx to use shared utilities:
- Replace inline range checks with helper functions
- Fix "Regular Lists" -> "Replaceable Events" naming
- Simplify redundant condition (isReplaceableKind includes kinds 0, 3)
- Update BaseEventRenderer.tsx to use isAddressableKind()
- Add comprehensive tests for all utilities
The filter JSON in SpellDetailRenderer was missing since/until
fields when they were in relative format (e.g., "7d", "now").
This happened because decodeSpell only adds these fields to
the filter object when they're unix timestamps.
Now we extract the raw since/until values from event tags and
include them in a displayFilter object for the JSON viewer,
ensuring users see the complete filter regardless of format.
The command preview already worked correctly since it uses
the reconstructed command string which includes these values.
- Remove text-sm from MediaPlaceholder and EventPlaceholder to inherit parent font size
- Add options to hide media and event embeds in HighlightRenderer preview
- Ensures placeholders like [image], [note] match surrounding text size
- Add VoiceMessageRenderer for kinds 1222 (voice message) and 1244 (voice reply)
- Add compact preview components with mic icon for voice messages
- Support legacy NIP-71 video kinds 34235/34236 via existing video renderers
- Add fallback URL tag parsing for video events
- Replace music note icon with mic icon for audio embeds
- Remove border/padding from audio player for cleaner display
- Add kind metadata (names, icons) for voice and legacy video kinds
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>