* Add COUNT command for NIP-45 event counting
Implements the COUNT verb from NIP-45, allowing users to count events
on relays without fetching them. Features:
- New `count` command requiring at least one relay
- Filter-only flags (excludes --limit, --close-on-eose, --view)
- Single relay shows centered count result
- Multiple relays show per-relay breakdown
- Handles approximate counts, errors, and unsupported relays
- Supports $me/$contacts aliases and NIP-05 resolution
Examples:
count relay.damus.io -k 3 -p npub1...
count nos.lol relay.damus.io -k 1 -a fiatjaf.com
* Fix $me and $contacts alias resolution in CountViewer
- Fetch contact list (kind 3) using useNostrEvent hook
- Extract contacts from p tags to resolve $contacts alias
- Add "Account Required" message when aliases used without active account
- Match ReqViewer pattern for consistent alias resolution
* Refactor: extract FilterSummaryBadges for compact headers
- Create shared FilterSummaryBadges component (nostr/FilterSummaryBadges.tsx)
- Simplify CountViewer header to single compact line
- Use FilterSummaryBadges in both ReqViewer and CountViewer
- Remove verbose collapsible filter section from CountViewer
* Remove 'events' suffix from count result
* Update count synopsis to show relays can appear anywhere
* Refactor CountViewer to use applesauce-relay pool
Replace manual WebSocket connections with the relay pool's
count() method for NIP-45 COUNT requests. This provides:
- Proper connection reuse via the existing relay pool
- Automatic reconnection handling
- Better integration with the rest of the app
Remove the approximate property since applesauce-relay's
CountResponse type doesn't expose it yet.
* Simplify CountViewer with one-shot requests and compact UI
- Use per-relay count requests with firstValueFrom and timeout
instead of pool.count() observable that may not complete
- Replace Collapsible-based header with icon-only DropdownMenus
matching ReqViewer's compact style
- Add raw JSON filter view with syntax highlighting and copy button
- Show relay count and filter in dropdowns instead of expanded sections
- Requests complete after timeout (10s) instead of spinning indefinitely
* Add NIP-45 support detection via NIP-11 relay info
- Check relay's supported_nips via NIP-11 before sending COUNT request
- Return early with "unsupported" status if relay explicitly doesn't support NIP-45
- Differentiate UI between unsupported (yellow Ban icon) and error (red AlertCircle)
- Provide clearer error messages based on whether NIP-11 info was available
- Uses cached relay info when available to avoid redundant requests
* Improve CountViewer header with human-readable filter summary
- Show kinds as badges, authors ("by"), mentions ("@"), hashtags on left
- Move relay status into relay dropdown with per-relay results
- Dropdown shows count per relay, status icons, and error tooltips
- Header now shows "2/3" style relay count trigger with loading state
* Reorder CountViewer header controls and remove redundant mention prefix
- Change control order to: refresh, relays, filter (was: filter, relays, refresh)
- Remove redundant "@" prefix from mentions since UserName with isMention already shows @
* Increase COUNT timeout to 30s and improve window title
- Extend per-relay timeout from 10s to 30s for more reliable results
- Update count window title to show human-readable kind names instead of
command-line format (e.g., "count: Short Note by abc123..." instead of
"count -k 1 -a npub...")
* Add spell support for COUNT commands
- Extend spell system to support both REQ and COUNT commands
- Add detectCommandType() to identify command type from string
- Update encodeSpell to use ["cmd", "COUNT"] tag for count commands
- Update decodeSpell to handle COUNT spells
- Update reconstructCommand to accept cmdType parameter
- Add "Save as spell" option to COUNT windows in WindowToolbar
- Update SpellDialog to handle both REQ and COUNT commands
* Add dynamic window title for COUNT with human-readable filter summary
- Add profile fetching for COUNT authors and tagged users
- Add countTitle useMemo with human-readable kind names, authors, mentions, hashtags, and search
- Use same formatting helpers as REQ titles (getKindName, formatProfileNames, etc.)
- Add countTitle to title priority chain after reqTitle
- Title now shows "Short Note • @alice • #bitcoin" instead of "COUNT"
* Update count command documentation for production
- Add note about automatic NIP-11/NIP-45 support detection
- Mention spell saving capability in description
* Add automatic relay selection with NIP-45 filtering for COUNT
- Make relays optional in count-parser (no longer throws if none specified)
- Add useOutboxRelays for automatic relay selection based on filter criteria
- Filter selected relays by NIP-45 support via NIP-11 before querying
- Show "Selecting relays..." and "Filtering by NIP-45..." loading states
- Fall back to aggregator relays if no NIP-45 relays found
- Update man page: relays now optional, new examples showing auto-selection
* Revert automatic relay selection for COUNT command
Simplify COUNT by requiring explicit relay specification:
- Restore relay requirement validation in count-parser.ts
- Remove useOutboxRelays and NIP-45 auto-filtering from CountViewer
- Update man page documentation to reflect required relays
- Keep NIP-45 support detection for better error messages
This keeps the feature simpler for now; automatic relay selection
can be added later when the UX is better understood.
* Reduce padding and sizing in COUNT single result view
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: hide zap reply previews for missing or non-chat events
Only show reply previews for zaps when:
1. The replied-to event exists in the event store
2. The replied-to event is a chat kind (9, 9321, or 1311)
This prevents showing loading states or reply previews for
zaps replying to events we don't have or non-chat events.
* refactor: extract CHAT_KINDS constant and include zap receipts
- Create CHAT_KINDS constant in types/chat.ts with all chat event kinds
- Include kind 9735 (zap receipts) as a chat kind
- Update ChatViewer to use the new constant for validation
- Improves maintainability and makes it easier to update chat kinds
Chat kinds now include:
- 9: NIP-29 group chat messages
- 9321: NIP-61 nutzaps
- 1311: NIP-53 live chat messages
- 9735: NIP-57 zap receipts
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: Display blossom servers in user menu with caching
Implements caching and display of user's blossom server lists (kind 10063) in the user menu dropdown.
**Key Changes:**
1. **Database Schema (db.ts)**:
- Added `CachedBlossomServerList` interface
- Added `blossomServers` table to Dexie v15
- Indexed by pubkey and updatedAt for efficient querying
2. **Caching Service (blossom-server-cache.ts)**:
- Dual-layer caching: LRU memory cache (100 entries) + Dexie persistent cache
- 24-hour TTL to reduce network requests
- Auto-caches kind:10063 events from EventStore
- Mirrors relay-list-cache pattern for consistency
3. **Sync Hooks**:
- `useBlossomServerCacheSync`: Subscribes to EventStore for auto-caching kind:10063 events
- `useAccountSync`: Extended to fetch and watch active account's blossom servers
4. **State Management**:
- Added `blossomServers` field to `activeAccount` in GrimoireState
- Created `setActiveAccountBlossomServers` pure function and callback
- Reactive updates when user's server list changes
5. **User Menu UI (user-menu.tsx)**:
- Added blossom servers section with HardDrive icon
- Shows server count badge (e.g., "3 servers")
- Clickable links open servers in new tabs
- Displays only when user has configured servers
**Architecture:**
- Follows existing relay list caching pattern for consistency
- Reactive: UI auto-updates when kind:10063 events arrive
- Incremental sync: Fetches on login, caches for 24h
- Performance: Memory cache for <1ms lookups, Dexie for persistence
**Testing:**
- Build: ✓ No TypeScript errors
- Tests: ✓ All 838 tests passing
* feat: Open blossom server file lists directly from menus
**User Menu & Profile Viewer Improvements:**
1. **Enhanced Click Behavior**:
- Clicking a blossom server now opens the file list for that server
- Shows blobs uploaded by the user (user menu) or profile owner (profile viewer)
- Pre-selects the clicked server in the dropdown
2. **UX Improvements**:
- Removed server count from user menu label (cleaner UI)
- Added `cursor-crosshair` to blossom server items (consistent with other clickable items)
- Removed external link icon (not opening external URL anymore)
3. **Technical Changes**:
- Updated `ListBlobsView` to accept optional `serverUrl` prop for pre-selection
- User menu: Opens `blossom list` with `serverUrl` for active user
- Profile viewer: Opens `blossom list` with both `pubkey` and `serverUrl`
**Flow:**
- User menu → Click server → Opens files for active user on that server
- Profile viewer → Click server → Opens files for viewed user on that server
* fix: Properly fetch blossom servers for any profile view
**Problem:**
Blossom servers were only visible for the logged-in user's profile,
not for other users' profiles being viewed.
**Solution:**
Enhanced ProfileViewer blossom server fetching with multi-layer approach:
1. **Cache-first loading**: Check blossomServerCache for instant display
2. **EventStore check**: Use existing cached event if available
3. **Reactive subscription**: Subscribe to EventStore for real-time updates
4. **Network fetch**: Use addressLoader to fetch latest from relays
5. **Auto-caching**: Update cache when new events arrive
**Benefits:**
- Blossom servers now display for ANY user's profile
- Instant display from cache (< 1ms)
- Reactive updates when data changes
- Proper cache hydration for future visits
- Consistent with relay list fetching pattern
**Technical:**
- Imported and integrated blossomServerCache service
- Added cache check before network fetch
- Separated EventStore subscription from network fetch
- Added cache updates on event arrival
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add Blossom blob storage integration
- Add blossom-client-sdk dependency for blob storage operations
- Create blossom.ts service with upload, list, check, mirror, delete primitives
- Add kind 10063 server list fetching and parsing
- Create blossom-parser.ts for command argument parsing with subcommands
- Add BLOSSOM command to man.ts with subcommands:
- servers: Show configured Blossom servers
- check: Check server health
- upload: Upload files to user's servers
- list: List blobs for a user
- mirror: Mirror blobs between servers
- delete: Delete blobs from servers
- Create BlossomViewer component with views for each subcommand
- Wire up BlossomViewer in WindowRenderer
- Add Blossom servers dropdown to ProfileViewer header
- Upload primitives can be called programmatically for use in other components
* Enhance Blossom viewer with server selection and blob details
- Add server selection checkboxes to upload view for choosing target servers
- Add BlobDetailView with media preview (image/video/audio) and metadata display
- Add 'blob' subcommand to view individual blob details
- Remove unused 'check' subcommand
* Add Blossom upload dialog with chat integration
- Create BlossomUploadDialog component with file picker, server selection, and preview
- Create useBlossomUpload hook for easy integration in any component
- Add insertText method to MentionEditor for programmatic text insertion
- Integrate upload button (paperclip icon) in chat composer
- Supports image, video, and audio uploads with drag-and-drop
* Add rich blob attachments with imeta tags for chat
- Add BlobAttachment TipTap extension with inline preview (thumbnail for images, icons for video/audio)
- Store full blob metadata (sha256, url, mimeType, size, server) in editor nodes
- Convert blob nodes to URLs in content with NIP-92 imeta tags when sending
- Add insertBlob method to MentionEditor for programmatic blob insertion
- Update NIP-29 and NIP-53 adapters to include imeta tags with blob metadata
- Pass blob attachments through entire send flow (editor -> ChatViewer -> adapter)
* Add fallback public Blossom servers for users without server list
- Add well-known public servers as fallbacks (blossom.primal.net, nostr.download, files.v0l.io)
- Use fallbacks when user has no kind 10063 server list configured
- Show "Public Servers" label with Globe icon when using fallbacks
- Inform user that no server list was found
- Select first fallback server by default (vs all user servers)
* Fix: Don't show fallback servers when not logged in
Blossom uploads require signed auth events, so users must be logged in.
The 'Account required' message is already shown in this case.
* Remove files.v0l.io from fallback servers
* Add rich renderer for kind 10063 Blossom server list
- Create BlossomServerListRenderer.tsx with feed and detail views
- Show user's configured Blossom servers with clickable links
- Clicking a server opens the Blossom window with server info
- Register renderers for kind 10063 (BUD-03)
- Fix lint error by renaming useFallbackServers to applyFallbackServers
* Add individual server view and NIP-05 support for blossom commands
- Add 'server' subcommand to view info about a specific Blossom server
- Update BlossomServerListRenderer to open server view on click
- Make blossom parser async to support NIP-05 resolution in 'list' command
- Add kind 10063 (Blossom Server List) to EVENT_KINDS constants with BUD-03 reference
- Update command examples with NIP-05 identifier support
* Add comprehensive tests for blossom-parser
- 34 test cases covering all subcommands (servers, server, upload, list, blob, mirror, delete)
- Tests for NIP-05 resolution, npub/nprofile decoding, $me alias
- Tests for error handling and input validation
- Tests for case insensitivity and command aliases (ls, view, rm)
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add protocol-specific actions to chat adapters
Extends the chat adapter system with support for slash commands and
protocol-specific actions without parameters.
New features:
- ChatAction type system for defining simple commands
- Base adapter getActions() and executeAction() methods
- NIP-29 /join and /leave slash commands
- Slash command parser for detecting /commands
- ChatViewer integration with toast notifications
Example usage in NIP-29 groups:
/join - Request to join the group
/leave - Leave the group
The action system is extensible and can be enhanced with parameterized
actions (e.g., /kick <user>, /ban <user>) in future iterations.
Tests: All 804 tests passing
Build: Successful
Lint: No errors
* feat: add autocomplete for slash commands
Extends the chat system with autocomplete UI for slash commands, making
actions discoverable and easier to execute.
New features:
- SlashCommandSuggestionList component with keyboard navigation
- Slash command search integrated into MentionEditor
- Autocomplete popup shows command name and description
- Arrow keys for navigation, Enter to select, Escape to close
- Filters available commands based on typed text
When typing "/" in chat, users now see:
/join Request to join the group
/leave Leave the group
The autocomplete uses the same TipTap suggestion system as @mentions
and :emoji:, providing a consistent UX across all autocomplete features.
Tests: All 804 tests passing
Build: Successful
Lint: 1 warning fixed (unused parameter)
* feat: execute slash commands immediately on autocomplete selection
When selecting a slash command from the autocomplete menu, the action
now executes immediately and clears the input, providing a more
streamlined UX.
Changes:
- Added onCommandExecute prop to MentionEditor
- Modified slash command suggestion to call onCommandExecute on selection
- Clears editor content immediately after selection
- Added handleCommandExecute callback in ChatViewer
- Executes action and shows toast notifications
Before: Type "/" → select "/join" → press Enter → executes
After: Type "/" → select "/join" → executes immediately
This matches the expected behavior for command selection - when you
choose a command from autocomplete, you want to execute it, not just
insert it into the text field.
Tests: All 804 tests passing
Build: Successful
Lint: No new errors
* feat: make chat actions context-aware and filter by membership status
Actions are now intelligently filtered based on the user's membership in
the conversation, providing a cleaner and more intuitive UX.
Changes:
- Added GetActionsOptions type with conversation and activePubkey
- Updated base adapter getActions() to accept optional context
- Modified NIP-29 adapter to filter actions based on membership:
- /join: only shown when user is NOT a member/admin
- /leave: only shown when user IS a member
- Updated ChatViewer to pass conversation and user context to searchCommands
- Moved searchCommands callback after conversation is defined
This prevents showing irrelevant commands like "/join" when you're already
a member, or "/leave" when you haven't joined yet. The autocomplete menu
now only displays actions that are actually executable in the current context.
Implementation notes:
- NIP-29 uses getAllActions() fallback when context unavailable
- Membership determined by checking conversation.participants array
- Other protocols return empty array by default (no actions yet)
Tests: All 804 tests passing
Build: Successful
Lint: No new errors
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add multi-room group chat interface (GroupListViewer)
Add a Discord/Slack-style multi-room chat interface for NIP-29 groups:
- New GroupListViewer component with split layout:
- Left panel: List of groups from kind 10009, sorted by recency
- Right panel: Full chat view for selected group
- Loads group metadata (kind 39000) for icons and names
- Tracks latest messages (kind 9) for activity-based sorting
* feat: add NIP-61 nutzap support to NIP-29 groups
Fetch and render nutzap events (kind 9321) in NIP-29 relay groups
using the same visual styling as lightning zaps. Nutzaps are P2PK
locked Cashu token transfers defined in NIP-61.
- Add nutzap filter subscription in loadMessages
- Combine chat and nutzap observables with RxJS combineLatest
- Add nutzapToMessage helper to parse NIP-61 event structure
- Extract amount by summing proof amounts from proof tag JSON
- Add nutzapUnit metadata field for future multi-currency support
* fix: improve zap/nutzap rendering in chat
- Add mb-1 margin bottom to zap messages for spacing
- Show inline reply preview for zaps that target specific messages
- Fix nutzap amount extraction to handle multiple proof tags
- Extract replyTo from e-tag for nutzaps
- Pass nutzap event to RichText for custom emoji rendering
* fix: pass event only to RichText for proper emoji rendering
* refactor: consolidate NIP-29 chat and nutzap into single REQ
- Use single filter with kinds [9, 9000, 9001, 9321] instead of
separate subscriptions with combineLatest
- Enables proper pagination for "load older" with single page fetches
- Add rounded corners to zap gradient border for consistent rendering
* refactor: consolidate NIP-53 chat and zap into single REQ
- Use single filter with kinds [1311, 9735] instead of separate
subscriptions with combineLatest
- Enables proper pagination for "load older" with single page fetches
- Filter invalid zaps inline during event mapping
* feat: add load older messages support to chat adapters
- Implement loadMoreMessages in NIP-29 adapter using pool.request
- Implement loadMoreMessages in NIP-53 adapter using pool.request
- Add "Load older messages" button to ChatViewer header
- Use firstValueFrom + toArray to convert Observable to Promise
- Track loading state and hasMore for pagination UI
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add NIP-53 live activity chat adapter
Add support for joining live stream chat via naddr (kind 30311):
- Create Nip53Adapter with parseIdentifier, resolveConversation, loadMessages, sendMessage
- Show live activity status badge (LIVE/UPCOMING/ENDED) in chat header
- Display host name and stream metadata from the live activity event
- Support kind 1311 live chat messages with a-tag references
- Use relays from activity's relays tag or naddr relay hints
- Add tests for adapter identifier parsing and chat-parser integration
* ui: derive live chat participants from messages, icon-only status badge
- Derive participants list from unique pubkeys in chat messages for NIP-53
- Move status badge after title with hideLabel for compact icon-only display
* feat: show zaps in NIP-53 live chat with gradient border
- Fetch kind 9735 zaps with #a tag matching the live activity
- Combine zaps and chat messages in the timeline, sorted by timestamp
- Display zap messages with gradient border (yellow → orange → purple → cyan)
- Show zapper, amount, recipient, and optional comment
- Add "zap" message type with zapAmount and zapRecipient metadata
* fix: use RichText for zap comments and remove arrow in chat
- Use RichText with zap request event for zap comments (renders emoji tags)
- Remove the arrow (→) between zapper and recipient in zap messages
* refactor: simplify zap message rendering in chat
- Put timestamp right next to recipient (removed ml-auto)
- Use RichText with content prop and event for emoji resolution
- Inline simple expressions, remove unnecessary variables
- Follow codebase patterns from ZapCompactPreview
* docs: update chat command to include NIP-53 live activity
- Update synopsis to use generic <identifier>
- Add NIP-53 live activity chat to description
- Update option description to cover both protocols
- Add naddr example for live activity chat
- Add 'live' to seeAlso references
* fix: use host outbox relays for NIP-53 live chat events
Combine activity relays, naddr hints, and host's outbox relays when
subscribing to chat messages and zaps. This ensures events are fetched
from all relevant sources where they may be published.
* ui: show host first in members list, all relays in dropdown
- derivedParticipants now puts host first with 'host' role
- Other participants from messages follow as 'member'
- RelaysDropdown shows all NIP-53 liveActivity.relays
---------
Co-authored-by: Claude <noreply@anthropic.com>
Implements rich text editing with profile mentions, NIP-29 system messages,
day markers, and naddr support for a more complete chat experience.
Editor Features:
- TipTap-based rich text editor with @mention autocomplete
- FlexSearch-powered profile search (case-insensitive)
- Converts mentions to nostr:npub URIs on submission
- Keyboard navigation (Arrow keys, Enter, Escape)
- Fixed Enter key and Send button submission
NIP-29 Chat Improvements:
- System messages for join/leave events (kinds 9000, 9001, 9021, 9022)
- Styled system messages aligned left with muted text
- Shows "joined" instead of "was added" for consistency
- Accepts kind 39000 naddr (group metadata addresses)
- Day markers between messages from different days
- Day markers use locale-aware formatting (short month, no year)
Components:
- src/components/editor/MentionEditor.tsx - TipTap editor with mention support
- src/components/editor/ProfileSuggestionList.tsx - Autocomplete dropdown
- src/services/profile-search.ts - FlexSearch service for profile indexing
- src/hooks/useProfileSearch.ts - React hook for profile search
Dependencies:
- @tiptap/react, @tiptap/starter-kit, @tiptap/extension-mention
- @tiptap/extension-placeholder, @tiptap/suggestion
- flexsearch@0.7.43, tippy.js@6.3.7
Tests:
- Added 6 new tests for naddr parsing in NIP-29 adapter
- All 710 tests passing
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>
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
- Add -v, --view <list|compact> flag to req command
- Remove UI toggle controls from ReqViewer header
- View mode is now set via command flag instead of runtime toggle
- Update man page with new flag documentation and example
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Redesign SpellbookDropdown with clear status indicators (ownership, storage)
- Add SpellbookStatus component showing you/other and local/published/network
- Enhance activeSpellbook type with source, localId, isPublished fields
- Fix PublishSpellbook action to properly yield events (caller handles side-effects)
- Add k tags extraction from REQ windows for kind-based filtering/discovery
- Update terminology from "Layout" to "Spellbook" consistently
- Add comprehensive tests for k tags and source tracking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add activeSpellbook to GrimoireState
- Display active spellbook title in header with clear button
- Add 'Update Layout' and 'Save as new' to SpellbookDropdown
- Highlight active spellbook in dropdown list
- Add 'Manage Spells' link to dropdown
- Refine dropdown styles (muted hover, no accent color)
- Update spellbook icon to BookHeart across the app
- Implement Preview mode with routing /:actor/:identifier
- Add Preview banner in Home component with Apply/Discard actions
- Add Preview and Share buttons to Spellbook renderers
- Clean up unused imports
Simplified layout system based on user feedback:
**1. Global Layout Config (Simpler)**
- Moved layoutConfig from per-workspace to global state
- One configuration applies to all workspaces (easier to understand)
- Updated state migration v8→v9 to move config to global level
- Updated WorkspaceSettings UI to edit global config
- Renamed updateWorkspaceLayoutConfig → updateLayoutConfig
**2. Preserve Extra Windows (Fixed Bug)**
- Fixed applyPresetToLayout to keep windows beyond preset slots
- When applying 4-window grid to 6 windows, windows 5-6 are preserved
- Extra windows stacked vertically on right side (70/30 split)
- No more window loss when applying presets
**3. Layout Dropdown in TabBar (Better UX)**
- Added dropdown menu next to workspace tabs
- Shows all available presets with icons (Grid2X2, Columns2, Split)
- Displays window requirements and availability
- Disables presets that need more windows than available
- One-click preset application with toast feedback
- More accessible than /layout command
All tests passing (457 passed). State migration handles v6→v7→v8→v9 correctly.
Generated with [Claude Code](https://claude.com/claude-code)
Phase 3 implementation:
- Created layout-presets.ts with 3 built-in presets (side-by-side, main-sidebar, grid)
- Implemented fillLayoutTemplate() for recursive template filling with window IDs
- Added collectWindowIds() for depth-first traversal of layout trees
- Created applyPresetToLayout() to reorganize existing windows
- Created layout-parser.ts for /layout command argument parsing
- Added layout command to man.ts with documentation and examples
- Built LayoutViewer component with:
* Visual preset gallery with diagrams
* Window count validation
* Apply preset functionality
* Error handling for insufficient windows
* Command-line preset specification support
- Wired LayoutViewer into WindowRenderer with lazy loading
- Added "layout" to AppId type definition
- Exposed applyPresetLayout in useGrimoire hook
Presets allow users to quickly reorganize multiple windows into
common layouts: 50/50 splits, 70/30 main+sidebar, or 2×2 grids.
Generated with [Claude Code](https://claude.com/claude-code)
Add per-workspace layout configuration with smart auto-balancing:
**Core Changes:**
- Add LayoutConfig interface to Workspace type with insertionMode, splitPercentage, insertionPosition
- Create layout-utils.ts with smart direction algorithm that auto-balances horizontal/vertical splits
- Update addWindow() to use workspace layoutConfig instead of hardcoded values
- Migrate state from v7→v8, adding layoutConfig to all workspaces with smart defaults
**Smart Direction Algorithm:**
- Analyzes layout tree to count horizontal vs vertical splits
- Automatically balances by favoring the less-common direction
- Defaults to horizontal (row) for first split
- Provides foundation for "Balanced (auto)" insertion mode
**Testing:**
- Add 30 comprehensive tests for layout-utils.ts (tree analysis, smart direction, window insertion)
- Add 30 tests for logic.ts addWindow() with different layout configs (row/column/smart modes)
- Update migration tests to verify v6→v7→v8 and v7→v8 paths
**Migration:**
- v7→v8 adds layoutConfig with defaults: smart mode, 50% split, second position
- All existing workspaces automatically get smart auto-balancing behavior
Implements orthogonal design: layout behavior = workspace settings (not command flags)