Message Layout Improvements:
- Added proper padding to message items (px-3 py-2)
- Added flex-1 min-w-0 to message content container to allow shrinking
- Added break-words and overflow-hidden to prevent horizontal overflow
- Ensures long URLs and unbreakable text wrap properly
Message Composer Improvements:
- Added border-t and consistent padding (px-3 py-2)
- Added border and rounded-md to textarea for better visual separation
- Added min-w-0 to textarea to prevent overflow in flex layout
- Added flex-shrink-0 to Send button to prevent squishing
- Added mb-2 to reply preview for spacing
These changes ensure:
- Long messages and URLs wrap correctly without horizontal scroll
- Consistent spacing throughout the chat interface
- Proper flex behavior prevents layout breaks
- Professional chat UI appearance
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Improvements to kind 10009 (Public Chats) renderer:
Batch Loading:
- PublicChatsRenderer now batch-loads all group metadata (kind 39000) in a single subscription
- Much more efficient than individual subscriptions per group
- Filters out "_" groups from metadata fetch (they don't have metadata)
- Creates a metadata map to pass to each GroupLink
Special "_" Group Handling:
- "_" represents the unmanaged relay top-level group
- Displays the relay name instead of group ID
- Example: "pyramid.fiatjaf.com" instead of "_"
GroupLink Updates:
- Accepts optional metadata prop (pre-loaded from parent)
- No longer fetches metadata individually (more efficient)
- Extracts group name and icon from provided metadata
- Falls back to group ID if metadata not available
Performance:
- Single subscription for all groups vs N subscriptions
- Reduces relay traffic and improves rendering speed
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
GroupLink was passing a string identifier instead of a properly structured
ProtocolIdentifier object, causing chat window to fail opening.
Fixed to pass:
{
protocol: "nip-29",
identifier: {
type: "group",
value: groupId,
relays: [relayUrl]
}
}
This matches the expected ChatViewer props interface and allows the
NIP-29 adapter to properly resolve the conversation with the group relay.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements rendering of NIP-51 kind 10009 (Public Chats list) events,
displaying NIP-29 groups similar to how relay lists are rendered.
Components:
- GroupLink: Clickable group component with icon and name
- Fetches kind 39000 metadata from EventStore for group info
- Displays group icon (if available) or chat icon fallback
- Opens chat window on click with relay'group-id identifier
- PublicChatsRenderer: Renderer for kind 10009 events
- Extracts "group" tags: ["group", "<group-id>", "<relay-url>"]
- Displays each group as clickable link
- Similar styling to relay list renderers
Integration:
- Registered kind 10009 renderer in kinds index
- Opens chat with NIP-29 protocol on click
Fixes:
- Fix missing type imports in NIP-29 adapter (Participant, ParticipantRole)
- Fix relayUrl type error in sendMessage (use validated variable)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Core Architecture:
- Protocol adapter pattern for chat implementations
- Base adapter interface with protocol-specific implementations
- Auto-detection of protocol from identifier format
- Reactive message loading via EventStore observables
Protocol Implementations:
- NIP-C7 adapter: Simple chat (kind 9) with npub/nprofile support
- NIP-29 adapter: Relay-based groups with member roles and moderation
- Protocol-aware reply message loading with relay hints
- Proper NIP-29 members/admins fetching using #d tags
UI Components:
- ChatViewer: Main chat interface with virtualized message timeline
- ChatMessage: Message rendering with reply preview
- ReplyPreview: Auto-loading replied-to messages from relays
- MembersDropdown: Virtualized member list with role labels
- RelaysDropdown: Connection status for chat relays
- ChatComposer: Message input with send functionality
Command System:
- chat command with identifier parsing and auto-detection
- Support for npub, nprofile, NIP-05, and relay'group-id formats
- Integration with window system and dynamic titles
NIP-29 Specific:
- Fetch kind:39000 (metadata), kind:39001 (admins), kind:39002 (members)
- Extract roles from p tags: ["p", "<pubkey>", "<role1>", "<role2>"]
- Role normalization (admin, moderator, host, member)
- Single group relay connection management
Testing:
- Comprehensive chat parser tests
- Protocol adapter test structure
- All tests passing (704 tests)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Fix custom emoji shortcode matching to include dashes
The regex pattern for matching NIP-30 custom emoji shortcodes was missing
the dash character, causing emojis like :work-out: to fail matching.
Updated both ReactionRenderer and ReactionCompactPreview to use
[a-zA-Z0-9_-] instead of [a-zA-Z0-9_].
* Refactor: extract emoji shortcode regex to constant
Move the emoji shortcode matching pattern to EMOJI_SHORTCODE_REGEX
constant in emoji-helpers.ts for reuse across reaction renderers.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Enhance the Copy ID feature in the event menu to include relay hints
from where the event has been seen. This improves event discoverability
by providing clients with relay suggestions when decoding nevent/naddr.
Changes:
- Import getSeenRelays from applesauce-core/helpers/relays
- Update copyEventId to retrieve seen relays for the event
- Pass relay hints to both neventEncode and naddrEncode
- Follows the same pattern as EventDetailViewer
Benefits:
- Better event sharing with relay context
- Helps other clients find events more easily
- Improves NIP-19 identifier quality
Co-authored-by: Claude <noreply@anthropic.com>
* feat(calendar): add renderers for NIP-52 calendar events (kinds 31922 & 31923)
- Add CalendarDays and CalendarClock icons for date/time event kinds
- Create calendar-event.ts helper with parsing and formatting functions
- Add feed renderers showing status badge, title, date/time, location, participant count
- Add detail renderers with full description, participant list with names, tags, and links
- Register renderers in kinds/index.tsx for both feed and detail views
- Use locale-aware date/time formatting throughout
* refactor(calendar): improve feed renderer layout
- Rename both kinds to "Calendar Event" for consistency
- Move date/time info below title
- Place time on left, status badge on right with justify-between
- Remove timezone indicator from feed view (keep in detail)
* refactor(calendar): apply feed layout to detail views
- Move title above date/time in detail views
- Use justify-between for time left, status badge right
- Use Label component for hashtags (no # prefix, consistent with feed)
- Use Label component for participant roles (subtle dotted border style)
* refactor(calendar): separate location/tags rows and tone down time font
- Separate location/participants from hashtags onto different rows in feed
- Reduce time font size: text-xs in feed, text-sm in detail (was text-sm/text-lg)
- Remove font-medium from time display
* style(calendar): tone down status badges to match date/time styling
Replace bold background-colored badges with subtle text-colored badges:
- Use text-blue-500, text-green-500, text-muted-foreground instead of backgrounds
- Lowercase labels ("upcoming", "now", "past")
- Consistent sizing with date/time text
* refactor(calendar): extract shared components and add caching
- Extract CalendarStatusBadge to shared component with variant/size props
- Add symbol-based caching to parseDateCalendarEvent and parseTimeCalendarEvent
using applesauce's getOrComputeCachedValue for performance
- Use 'd' tag (identifier) as title fallback instead of "Untitled Event"
- Remove duplicate CalendarStatusBadge implementations from all 4 renderers
- Remove unused imports (cn, CalendarDays, CalendarClock, Clock, CheckCircle)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(CommandLauncher): redirect to dashboard when executing commands from preview routes
NIP-19 preview routes (/nevent..., /npub..., etc.) don't render the
window system, so commands executed there appeared to do nothing.
Now detects preview routes and navigates to the dashboard before
creating the window, so the command actually takes effect.
* fix(test): add WebSocket polyfill for Node.js test environment
nostr-tools relay code requires WebSocket which isn't available in
Node.js by default. Adding the ws package polyfill prevents
"ReferenceError: WebSocket is not defined" errors in tests.
* fix(test): add @types/ws and fix type cast for WebSocket polyfill
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add new 'grid' preset to MediaEmbed with square aspect ratio and object-cover
- Update Gallery component to use CSS grid (3 cols) for images/videos
- Separate audio items into stacked layout below the grid
- Grid adapts to container width for varied event card sizes
Co-authored-by: Claude <noreply@anthropic.com>
Update applesauce-react from ^5.0.0 to ^5.0.1 to get latest fixes.
React 19 (^19.2.1) is already in use.
Co-authored-by: Claude <noreply@anthropic.com>
Add slash commands for common workflows:
- /commit-push-pr: Streamlined PR creation
- /verify: Full verification suite (lint + test + build)
- /test: Run tests with results summary
- /lint-fix: Auto-fix lint and formatting
- /review: Code review for quality and Nostr patterns
Update settings.json:
- Expand permissions for common safe bash commands
- Add PostToolUse hook for auto-formatting with Prettier
Update CLAUDE.md:
- Add Verification Requirements section
- Document available slash commands
- Emphasize running /verify before PRs
Co-authored-by: Claude <noreply@anthropic.com>
* Add README.md with project overview and getting started guide
* Remove inaccurate offline support claim from README
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: implement NIP-89 app definitions and recommendations with rich rendering
Add comprehensive support for NIP-89 Application Handlers (kind 31990) and
Handler Recommendations (kind 31989) with rich, interactive visualizations.
Core Implementation:
- nip89-helpers.ts: Utility functions for extracting NIP-89 event metadata
- App name, description, image from kind 31990 content JSON
- Supported kinds from k tags
- Platform URLs (web, ios, android) from platform tags
- Handler references from kind 31989 a tags
- URL template substitution for <bech32> placeholders
Feed Renderers:
- ApplicationHandlerRenderer (31990): Shows app name, supported kinds as
clickable KindBadges (max 8 in feed), and platform badges
- HandlerRecommendationRenderer (31989): Shows recommended kind and handler
list (max 3 in feed) with platform indicators
Detail Renderers:
- ApplicationHandlerDetailRenderer (31990): Comprehensive view with app info,
all supported kinds in grid layout (clickable), platform URLs with copy
buttons, and metadata JSON viewer
- HandlerRecommendationDetailRenderer (31989): Full view with platform
filtering tabs, expanded handler cards showing app details, and raw
reference data
Features:
- Clickable KindBadges throughout for quick navigation
- Platform-aware filtering and display
- Fetches referenced kind 31990 events reactively
- Copy buttons for URL templates
- Platform icons (web, ios, android)
- Follows existing Grimoire patterns (SpellRenderer for kinds display,
CodeSnippetDetailRenderer for metadata sections)
Testing:
- Comprehensive test suite for nip89-helpers (50+ test cases)
- Tests cover all helper functions with edge cases
- Follows existing test patterns from codebase
Registry:
- Added both kinds (31989, 31990) to kindRenderers and detailRenderers
- Automatically expands supported kinds count in KindsViewer
* fix: remove unused imports and parameters in NIP-89 renderers
* fix: correct AddressPointer import and apply prettier formatting
- Change AddressPointer import from applesauce-core/helpers to nostr-tools/nip19
to match codebase conventions
- Auto-fix prettier formatting for nip89 files
* fix: add defensive type checks to prevent React error 31
- Add type guards in nip89-helpers to ensure string types
- Check metadata object structure before accessing properties
- Add fallbacks for undefined address.identifier values
- Prevents accidentally rendering objects as React children
* fix: stringify contentJson for CopyableJsonViewer and support 'about' field
- Fix React error 31: CopyableJsonViewer expects string, not object
- Add JSON.stringify() with pretty printing for metadata display
- Support both 'description' and 'about' fields in content JSON (common in kind 0)
- Add tests for 'about' field handling
* refactor: simplify NIP-89 detail renderers
Remove unnecessary metadata displays:
- Remove app image from ApplicationHandlerDetailRenderer
- Remove Event ID and Created timestamp from both detail renderers
- Remove Raw Metadata section from ApplicationHandlerDetailRenderer
- Remove Raw References section from HandlerRecommendationDetailRenderer
- Clean up unused imports (getAppImage, CopyableJsonViewer, useMemo, formatAddressPointer)
Keeps the UI focused on the essential information: app name, description,
supported kinds, and platform URLs.
* feat: add website display and filter non-platform tags
NIP-89 renderer improvements:
- Add getAppWebsite() helper to extract website from content JSON
- Display website URL in both feed and detail renderers with external link
- Filter out non-platform tags (r, t, client, alt, e, p, a) to prevent garbage display
- Remove relay hint display from HandlerRecommendationDetailRenderer
- Clean up unused relayHint parameter
Fixes the 'r r' tag appearing as a platform by properly excluding
common non-platform tags when detecting platform URLs.
* refactor: create reusable ExternalLink component for consistent styling
Create ExternalLink component following patterns from HighlightRenderer and
BookmarkRenderer with:
- Two variants: 'muted' (default, text-muted-foreground with underline)
and 'default' (text-primary with hover:underline)
- Three sizes: xs, sm, base
- Configurable icon display
- Consistent truncate behavior for long URLs
- Stop propagation on click
Apply to NIP-89 renderers:
- ApplicationHandlerRenderer: uses muted variant (feed view)
- ApplicationHandlerDetailRenderer: uses default variant (detail view)
This ensures consistent link styling across the entire application
and makes it easy to maintain a unified design language.
* refactor: consolidate JSON parsing into cached getAppMetadata helper
Performance optimization:
- Create getAppMetadata() helper that parses content JSON once and caches
the result using Symbol.for('nip89-metadata') as cache key
- All metadata helpers (getAppName, getAppDescription, getAppWebsite) now
use the cached metadata instead of parsing JSON multiple times
- Prevents redundant JSON.parse() calls when multiple helpers are used
Code cleanup - removed unused functions:
- getAppImage() - no longer used after removing image display
- getHandlersByPlatform() - filtering done in component state
- substituteTemplate() - not needed in current implementation
- hasPlaceholder() - utility never used
- formatAddressPointer() - not needed anymore
Updated tests:
- Replace getAppImage tests with getAppWebsite tests
- Remove tests for deleted utility functions
- All remaining tests pass
This consolidation improves performance by ensuring JSON.parse() is called
at most once per event, regardless of how many metadata fields are accessed.
* feat: use app name in window titles for NIP-89 app events
Add special handling for kind 31990 (Application Handler) events in
getEventDisplayTitle to use the app name from content JSON instead of
generic kind name. Falls back to identifier if app name not available.
This gives NIP-89 app handler events nice readable window titles.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: render highlight comments with RichText component
Enable support for custom emoji, mentions, hashtags, and other rich text features in highlight comments by using the RichText component instead of plain text rendering.
Changes:
- HighlightRenderer: Use RichText for comment rendering with media/embeds disabled
- HighlightDetailRenderer: Add RichText import and use it for comment rendering
* chore: update package-lock.json
* fix: pass event to RichText for custom emoji tag support
Create synthetic events that preserve emoji tags from the original
highlight event while using the comment as content. This ensures
custom emoji in comments render correctly.
Changes:
- HighlightRenderer: Create commentEvent with emoji tags preserved
- HighlightDetailRenderer: Create commentEvent with emoji tags preserved
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add preview routes for Nostr identifiers (npub, nevent, note, naddr)
This commit adds dedicated preview routes for Nostr identifiers at the root level:
- /npub... - Shows a single profile view for npub identifiers
- /nevent... - Shows a single event detail view for nevent identifiers
- /note... - Shows a single event detail view for note identifiers
- /naddr... - Redirects spellbooks (kind 30777) to /:actor/:identifier route
Key changes:
- Created PreviewProfilePage component for npub identifiers
- Created PreviewEventPage component for nevent/note identifiers
- Created PreviewAddressPage component for naddr redirects
- Added hideBottomBar prop to AppShell to hide tabs in preview mode
- Added routes to root.tsx for all identifier types
Preview pages don't show bottom tabs and don't affect user's workspace layout.
* chore: update package-lock.json
* refactor: create reusable useNip19Decode hook and improve preview pages
This commit makes the preview pages production-ready by:
1. Created useNip19Decode hook (src/hooks/useNip19Decode.ts):
- Reusable hook for decoding NIP-19 identifiers (npub, note, nevent, naddr, nprofile)
- Type-safe with discriminated union for decoded entities
- Comprehensive error handling with retry functionality
- Loading states and error messages
- Well-documented with JSDoc comments and usage examples
2. Comprehensive test coverage (src/hooks/useNip19Decode.test.ts):
- 11 tests covering all entity types (npub, note, nevent, naddr)
- Tests for error handling (missing identifier, invalid format, corrupted bech32)
- Tests for retry functionality and state changes
- Uses jsdom environment for React hook testing
- All tests passing ✓
3. Refactored preview pages to use the hook:
- PreviewProfilePage: Simplified from 80 to 81 lines with cleaner logic
- PreviewEventPage: Improved type safety and error handling
- PreviewAddressPage: Better separation of concerns
- All pages now have consistent error handling and retry functionality
- Better user experience with improved error messages
4. Dependencies added:
- @testing-library/react for React hook testing
- @testing-library/dom for DOM testing utilities
- jsdom and happy-dom for browser environment simulation in tests
Benefits:
- Code deduplication: Preview pages share decoding logic
- Type safety: Discriminated union prevents type errors
- Testability: Hook can be tested independently
- Maintainability: Single source of truth for NIP-19 decoding
- User experience: Consistent error handling and retry across all preview pages
- Production-ready: Comprehensive tests and error handling
* refactor: simplify useNip19Decode to synchronous with memoization
NIP-19 decoding is synchronous - removed unnecessary async complexity:
Hook changes (src/hooks/useNip19Decode.ts):
- Removed loading states (isLoading, setIsLoading)
- Removed retry functionality (unnecessary for sync operations)
- Now uses useMemo for efficient memoization
- Returns { decoded, error } instead of { decoded, isLoading, error, retry }
- Same string always yields same result (memoized)
- Went from ~120 lines to ~115 lines, but much simpler
Preview page changes:
- Removed loading spinners and states
- Removed retry buttons
- Simplified error handling
- Cleaner, more readable code
- PreviewProfilePage: 55 lines (down from 81)
- PreviewEventPage: 83 lines (down from 105)
- PreviewAddressPage: 83 lines (down from 117)
Test changes (src/hooks/useNip19Decode.test.ts):
- Removed waitFor and async/await (not needed for sync)
- Tests run faster (39ms vs 77ms - 49% improvement)
- Added memoization tests to verify caching works
- Simplified from 11 async tests to 11 sync tests
- All 11 tests passing ✓
Benefits:
- Simpler mental model: decode happens instantly
- Better performance: no state updates, just memoization
- Easier to test: synchronous tests are simpler
- More correct: matches the actual synchronous nature of nip19.decode()
- Less code: removed ~150 lines of unnecessary complexity
* feat: show detail view for all addressable events in naddr preview
Previously, PreviewAddressPage only handled spellbooks (kind 30777) and
showed errors for other addressable events. Now:
- Spellbooks (kind 30777): Redirect to /:actor/:identifier (existing behavior)
- All other addressable events: Show in EventDetailViewer
This enables previewing any addressable event (long-form articles, live
events, community posts, etc.) via naddr links.
Changes:
- Import EventDetailViewer
- Removed error state for non-spellbook kinds
- Show EventDetailViewer with AddressPointer for all other kinds
- Simplified from 83 lines to 77 lines
* fix: correct route patterns for NIP-19 identifier previews
The previous route patterns (/npub:identifier) conflicted with the catch-all
/:actor/:identifier route and didn't properly match NIP-19 identifiers.
Fixed by:
1. Using wildcard routes with correct prefixes:
- /npub1* (not /npub:identifier)
- /nevent1* (not /nevent:identifier)
- /note1* (not /note:identifier)
- /naddr1* (not /naddr:identifier)
2. Updated preview components to use params['*'] for wildcard capture:
- Reconstruct full identifier as prefix + captured part
- e.g., npub1 + params['*'] = npub107jk7htfv...
This ensures routes properly match before the catch-all /:actor/:identifier
route and correctly capture the full bech32-encoded identifier.
Test URL: /npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg
* style: apply prettier formatting
* fix: use loader-based routing for NIP-19 identifiers in React Router v7
Previous attempts using wildcard routes didn't work properly in React Router v7.
Solution:
- Single /:identifier route with a loader that validates NIP-19 prefixes
- Loader throws 404 if identifier doesn't start with npub1/note1/nevent1/naddr1
- Created Nip19PreviewRouter component that routes to correct preview page
- Routes are properly ordered: /:identifier before /:actor/:identifier catch-all
This ensures /npub107jk... routes to profile preview, not spellbook route.
Benefits:
- Simpler routing configuration (1 route vs 4 duplicate routes)
- Proper validation via loader
- Clean separation of concerns with router component
- Works correctly in React Router v7
---------
Co-authored-by: Claude <noreply@anthropic.com>
Previously, the auth dialog would appear whenever a relay sent an AUTH
challenge, even when there was no active user session. This was confusing
for users who weren't logged in.
Now the shouldPromptAuth() method checks if there's an active account
before returning true, ensuring auth dialogs only appear for logged-in users.
Co-authored-by: Claude <noreply@anthropic.com>
The MediaPlaceholder component in Link.tsx had text-sm hardcoded,
causing it to appear larger than surrounding text in compact contexts.
Now all placeholder components (Link, Gallery, Mention) consistently
inherit font size from their parent.
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add copy button for NIP markdown
- Add copy button to WindowToolbar for regular NIPs (appId: "nip")
- Button appears in window toolbar next to edit button
- Uses Copy/CopyCheck icons from lucide-react
- Fetches NIP content via useNip hook
- Shows toast notification on successful copy
- Add copy button to CommunityNIPDetailRenderer for community NIPs (kind 30817)
- Button appears in header next to title
- Copies event.content (markdown) to clipboard
- Uses same Copy/CopyCheck icon pattern
- Shows toast notification on successful copy
Both implementations use the existing useCopy hook for state management
and maintain consistent styling with other toolbar buttons.
* refactor: use Button component and remove misleading shortcuts
- Replace native button elements with Button component from shadcn/ui
- Use variant="ghost" and size="icon" for consistent styling
- Apply h-8 w-8 classes for uniform button sizing
- Remove manual className styling in favor of component variants
- Remove misleading keyboard shortcut hints from button titles
- Changed "Edit command (Cmd+E)" to "Edit command"
- Changed "Close window (Cmd+W)" to "Close window"
- These shortcuts don't actually work, so they were misleading
- Clean up imports and formatting
- Format lucide-react imports across multiple lines
- Add Button component import
- Run prettier for consistent code style
* refactor: use link variant and remove size class overrides
- Change all Button components from variant="ghost" to variant="link"
- Remove className="h-8 w-8" overrides to use default Button sizing
- Maintains size="icon" for proper icon button behavior
- Applies to WindowToolbar and CommunityNIPDetailRenderer
* style: add muted color to window toolbar icon buttons
- Add className="text-muted-foreground" to all Button components in WindowToolbar
- Improves visual contrast for toolbar buttons
- Applies to Edit, Copy NIP, More actions, and Close window buttons
---------
Co-authored-by: Claude <noreply@anthropic.com>
The $contacts alias in REQ command was broken due to an object reference issue.
The contact list pointer object was being created inline on every render, causing
useNostrEvent to treat it as a new pointer each time and disrupting the subscription.
Now properly memoize the pointer object so it only changes when accountPubkey or
needsAccount actually changes, ensuring stable subscriptions and proper contact
list retrieval.
Fixes the $contacts alias resolution in REQ commands.
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add clickable example commands to welcome screen
Add 4 example commands to the welcome splash screen to make the client
more friendly on cold start and to new users:
- nip 29: View relay-based groups spec
- profile verbiricha@habla.news: Explore a Nostr profile
- req -k 1 -l 20: Query recent notes
- nips: Browse all NIPs
Commands are clickable and execute directly, opening the appropriate
window without requiring users to type or use the command palette.
Changes:
- Update GrimoireWelcome component with EXAMPLE_COMMANDS constant
- Add onExecuteCommand prop to handle direct command execution
- Update WorkspaceView to pass handleExecuteCommand callback
- Use parseAndExecuteCommand from command-parser for execution
* fix: lint
---------
Co-authored-by: Claude <noreply@anthropic.com>
because you wanted to know what it feels like:
nostr:nevent1qvzqqqqqqypzqla9dawkjc4trc7dgf88trpsq2uxvhmmpkxua607nc5g6a634sv5qyd8wumn8ghj7urewfsk66ty9enxjct5dfskvtnrdakj7qgmwaehxw309a6xsetxdaex2um59ehx7um5wgcjucm0d5hsqgxwfrfg9kvh99tj6adq5v2spwm9vhnp76zljuseqcpz4nd7w9d6pg000a03
**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