Alejandro 94982ca7f4 feat(post): add POST command with rich text editor and relay selection (#180)
* feat(post): add POST command with rich text editor and relay selection

Phase 3: Create POST command for publishing kind 1 notes

Features:
- RichEditor component with @mentions, :emoji: autocomplete
- Image/video upload via Blossom with drag-and-drop
- Relay selection UI with write relays pre-selected by default
- Per-relay publish status tracking (loading/success/error)
- Submit with Ctrl/Cmd+Enter keyboard shortcut
- Multi-line editing with rich previews for attachments
- NIP-30 emoji tags and NIP-92 imeta tags for attachments

Implementation:
- Add 'post' to AppId type
- Add POST command to man pages
- Create PostViewer component using RichEditor
- Wire PostViewer into WindowRenderer
- Publish events using EventFactory and RelayPool
- Track per-relay status with visual indicators

Usage:
- Run 'post' command to open the composer
- Type content with @mentions and :emoji:
- Upload media via button or drag-and-drop
- Select/deselect relays before publishing
- Press Publish button or Ctrl/Cmd+Enter to post
- View per-relay publish status in real-time

* feat(editor): add nostr bech32 paste handler with inline previews

Implements paste handling for nostr: URIs (npub, note, nevent, naddr, nprofile)
that transforms them into rich inline preview chips in the chat composer.

Changes:
- Add NostrEventPreview TipTap node type for displaying bech32 previews
- Add NostrPasteHandler extension to detect and transform pasted bech32 strings
- Update serializeContent to convert previews back to nostr: URIs on submit
- Add CSS styling for preview chips with hover effects
- Support all major bech32 types: npub, note, nevent, naddr, nprofile

Features:
- Automatic detection of nostr: URIs in pasted text (with or without prefix)
- Visual preview chips with type icon, label, and truncated ID
- Maintains nostr: URI format in final message content (NIP-27 compatible)
- Simple implementation without event fetching (fast, no loading states)
- Styled with primary theme colors for consistency

Technical details:
- Uses ProseMirror Plugin API for paste interception
- Inline atomic nodes for previews (similar to mentions and blob attachments)
- Regex pattern matches all valid bech32 formats
- Proper error handling for invalid bech32 strings
- Extensible foundation for future rich metadata fetching

* refactor(editor): improve nostr preview display with kind icons

Updates the nostr event preview chips to show more meaningful information:

Changes:
- Add getKindIcon() helper function with 15+ kind mappings
- Display format: kind-icon + author/id (e.g., "📄 a1b2c3d4")
- For naddr: show kind icon + author pubkey (first 8 chars)
- For nevent: show kind icon + author if available, else event ID
- For note: show 📝 + event ID (first 8 chars)
- For npub/nprofile: show 👤 + pubkey (first 8 chars)
- Removed verbose type labels (NPUB, NEVENT, etc.)
- Increased label max-width to 120px for better readability

Kind icon mappings:
- 👤 Profile (kind 0)
- 📝 Note (kind 1)
- 👥 Contacts (kind 3)
- 🔁 Repost (kind 6)
- ❤️ Reaction (kind 7)
-  Zap (kind 9735)
- 📄 Long-form (kind 30023)
- 🎙️ Live event (kind 30311)
- 📦 File metadata (kind 1063)
- 📌 Addressable events (30000-39999)
- 🔄 Replaceable events (10000-19999)

Result: More compact, visually intuitive previews that immediately
convey the content type and author without requiring event fetching.

* refactor(editor): simplify nostr preview display and reuse mentions

Major simplification of the nostr bech32 preview display:

Profile handling (npub/nprofile):
- Now creates regular @mention nodes instead of custom preview nodes
- Reuses existing mention infrastructure, styling, and UserName component
- Displays as "@username" with existing mention chip styling
- Serializes to nostr:npub1... on submit (same as manual @mentions)

Event/Address display (note/nevent/naddr):
- Removed emoji icons for cleaner, more minimal appearance
- Display format: "event abc12345" for note/nevent
- Display format: "address article-slug" for naddr (shows d identifier)
- Falls back to short pubkey if naddr has no d identifier
- Simple text-only chips with type + identifier

Benefits:
- Less visual noise (no emojis)
- Consistent mention styling for all profiles
- Profile mentions can now be clicked/hovered like manual mentions
- Smaller code footprint (removed complex icon mapping logic)
- Better UX: profiles look and behave like regular mentions

Technical changes:
- Paste handler creates mention nodes for npub/nprofile
- NostrEventPreview only handles note/nevent/naddr now
- Removed npub/nprofile from serialization (handled by mention serializer)
- Updated type definitions to reflect reduced scope

* feat(editor): add drag-and-drop and paste file upload support

Adds comprehensive file upload support via drag-and-drop and paste
directly into the chat composer editor.

Changes to MentionEditor:
- Add FileDropHandler TipTap extension to intercept file drops/pastes
- Filter for valid media types (image/video/audio)
- Add onFileDrop callback prop to communicate with parent
- Handle both drag-and-drop and clipboard paste events

Changes to useBlossomUpload hook:
- Update open() method to accept optional File[] parameter
- Add initialFiles state to track pre-selected files
- Clear initialFiles when dialog closes
- Pass initialFiles to BlossomUploadDialog component

Changes to BlossomUploadDialog:
- Add initialFiles prop for pre-selecting files
- Auto-select first file when initialFiles provided
- Generate preview URL for images/video on initialization
- Seamless UX: dropped files immediately appear in dialog

Changes to ChatViewer:
- Wire up onFileDrop callback to open upload dialog
- Pass dropped files to upload dialog via open(files)
- Fix onClick handler to properly call openUpload()

User experience:
- Drag image/video/audio file onto chat composer → upload dialog opens
- Paste image from clipboard → upload dialog opens
- File automatically selected and previewed
- Click upload to complete (same as manual file selection)

Technical details:
- Uses ProseMirror Plugin API for drop/paste interception
- File type validation: /^(image|video|audio)\//
- Bonus: Also works with clipboard paste (Ctrl+V)
- Clean state management with automatic cleanup

* fix(editor): add dragover handler to enable file drag-and-drop

The drag-and-drop wasn't working because we need to handle dragover
events and call preventDefault() to signal that drops are allowed.

Changes:
- Add handleDOMEvents with dragover handler to FileDropHandler
- Check for Files type in dataTransfer
- Set dropEffect to 'copy' for visual feedback
- Call preventDefault() to allow drops

Without this, the browser blocks all drops by default and handleDrop
never fires. This is a standard requirement for HTML5 drag-and-drop API.

* feat(editor): show usernames when pasting npub/nprofile mentions

When pasting npub or nprofile URIs, the editor now looks up the profile
from the event store cache and uses the display name if available.

Changes:
- Add getDisplayNameForPubkey helper function
- Import eventStore and profile helpers
- Synchronously check if profile is cached via .value property
- Use getDisplayName from nostr-utils for consistent formatting
- Falls back to short pubkey (first 8 chars) if profile not cached

Result:
- Paste npub1abc... → shows "@alice" if profile is cached
- Paste npub1abc... → shows "@abc12345" if profile not cached
- Consistent with typed @mentions behavior
- No async delays or loading states needed

* refactor(editor): remove drag-and-drop, keep paste file support

Removes drag-and-drop file handling while keeping clipboard paste functionality.
Drag-and-drop will be implemented differently later.

Changes:
- Rename FileDropHandler → FilePasteHandler
- Remove handleDrop and dragover event handlers
- Keep handlePaste for clipboard file support
- Rename onFileDrop prop → onFilePaste for clarity
- Update ChatViewer to use onFilePaste callback
- Update all references and comments

Result:
- Paste images from clipboard (Ctrl+V) still works
- Drag-and-drop removed completely
- Clearer naming reflects actual functionality

* refactor(editor): extract shared extensions and create React node views

Phase 1 of RichEditor variant implementation:

- Extract NostrPasteHandler extension for reuse across editors
- Extract FilePasteHandler extension for clipboard file handling
- Create NostrEventPreviewRich React node view using DetailKindRenderer
- Create BlobAttachmentRich React node view with full-size media previews
- Create rich TipTap node extensions using ReactNodeViewRenderer
- Update MentionEditor to use shared extensions

These shared components will be used by the upcoming RichEditor
variant for long-form content composition with full previews.

* feat(editor): add RichEditor component for long-form content

Phase 2 of RichEditor variant implementation:

- Create RichEditor component based on MentionEditor
- No slash commands (removed SlashCommand extension)
- Multi-line support with Enter for newlines
- Ctrl/Cmd+Enter to submit
- Full-size image/video previews using BlobAttachmentRich
- Full event rendering using NostrEventPreviewRich
- Configurable min/max height (defaults: 200px-600px)
- Retains @mentions and :emoji: autocomplete
- Reuses shared extensions (NostrPasteHandler, FilePasteHandler)
- Targets kind 1 notes composition

Key differences from MentionEditor:
- Block-level rich previews instead of inline badges
- Multi-line editing without Enter-to-submit
- Resizable with overflow-y: auto
- No command palette functionality

* refactor(post): improve PostViewer layout and editor previews

UI improvements:
- Remove flex-1 from editor, use fixed min/max height
- Move upload button next to publish button (icon-only)
- Place relay selector below action buttons (not fixed)
- Limit image/video previews to max-h-96
- Add pointer-events-none to event previews to prevent accidental interaction
- Use EventDetailSkeleton for loading event previews

Layout changes:
- Single scrollable container with space-y-4
- Editor: 150-400px height range
- Action buttons in single row (upload icon + publish button)
- Relay list with max-h-64 scroll area
- Better spacing and visual hierarchy

* feat(post): add draft persistence and improve relay UI

Draft persistence:
- Save draft content to localStorage every 2 seconds
- Load draft on component mount (per-user with pubkey)
- Clear draft after successful publish
- Prevent data loss on tab change or page reload

UI improvements:
- Reset button now icon-only (RotateCcw) with tooltip
- Remove borders from relay list items
- Use RelayLink component for relay rendering
- Show relay icons and secure/insecure indicators
- Cleaner, more compact relay list appearance
- Increased spacing between relay items (space-y-1)

Storage key format: grimoire-post-draft-{pubkey}

* feat(post): enhance draft storage with full JSON state persistence

Draft persistence improvements:
- Store complete editor state as JSON (preserves blobs, emojis, mentions, formatting)
- Save selected relays array in draft
- Restore full editor content using setContent() method
- Maintain draft per-user with pubkey-based key

RichEditor enhancements:
- Add getJSON() method to export editor state
- Add setContent() method to restore from JSON
- Enables lossless draft save/restore

UI improvements:
- Change "Relays" label to plain text without Label component
- Keep unselected relays visible during/after publish
- Only update status for selected relays during publish

Draft storage format:
{
  editorState: {...},      // Full TipTap JSON state
  selectedRelays: [...],   // Array of selected relay URLs
  timestamp: 1234567890
}

* feat(post): add nostr tag extraction, retry failed relays, and disable empty publish

Nostr tag extraction:
- Extract p tags from @mentions (pubkey references)
- Extract e tags from note/nevent references (event ID references)
- Extract a tags from naddr references (address/parameterized replaceable)
- Update SerializedContent interface to include mentions, eventRefs, addressRefs
- Serialize editor content walks all node types to extract references
- Build complete tag array for kind 1 events with proper NIP compliance

Retry failed relays:
- Add retryRelay() function to republish to specific failed relay
- Make error icon clickable with hover state
- Show "Click to retry" in tooltip
- Rebuild event and attempt publish again
- Update status indicators in real-time

Disable publish when empty:
- Track isEditorEmpty state
- Update every 2 seconds along with draft save
- Disable publish button when editor isEmpty()
- Prevents publishing empty notes

Tag generation order:
1. p tags (mentions)
2. e tags (event references)
3. a tags (address references)
4. emoji tags (NIP-30)
5. imeta tags (NIP-92 blob attachments)

This ensures proper Nostr event structure with all referenced pubkeys, events, and addresses tagged.

* feat(post): add client tag and remove reset button

Client tag:
- Add ["client", "grimoire"] tag to all published events
- Added to both handlePublish and retryRelay functions
- Identifies posts as coming from Grimoire client

UI cleanup:
- Remove reset button from relay selection UI
- Relay selection persists across sessions via draft storage
- Simplify header to just show relay count
- Remove unused RotateCcw icon import

Tag generation order (final):
1. p tags (mentions)
2. e tags (event references)
3. a tags (address references)
4. client tag (grimoire)
5. emoji tags (NIP-30)
6. imeta tags (NIP-92)

* fix(post): trim content before publishing

Content trimming:
- Trim content in handlePublish() before building event
- Trim content in retryRelay() before building event
- Removes leading and trailing whitespace from published notes
- Prevents accidental whitespace-only or padded messages
- Applied via content.trim() before factory.build()

* fix(post): reuse signed event for retries instead of recreating

Event reuse for retries:
- Add lastPublishedEvent state to store the signed event
- Store signed event after creation in handlePublish()
- Simplify retryRelay() to republish the same signed event
- Remove duplicate event creation and signing in retryRelay()
- Remove duplicate client tag addition in retryRelay()

Benefits:
- Same event ID and signature across all relays
- More efficient - no need to re-sign
- Consistent event across retries
- Single source of truth for published event

Flow:
1. User publishes - event created, signed, stored
2. Some relays fail - event remains in state
3. User retries - same event republished to failed relay
4. New post overwrites lastPublishedEvent with new event

* feat(post): add published event preview and reset button

After successful publish, replace editor and action buttons with:
- Kind1Renderer preview of the published event
- Reset button to compose another post
- Relay list remains visible with publish status indicators

Users can click "Compose Another Post" to reset the form and
return to the editor for the next post.

* feat(post): improve layout and relay handling

- Fix double scrollbar issue by using flexbox layout instead of overflow-auto
- Disable relay checkboxes during preview mode (can't change after publish)
- Use aggregator relays as fallback when no write relays configured
- Remove conditional error message since fallback relays always available
- Improve layout responsiveness with flex-1 and flex-shrink-0 classes

Aggregator relays (nos.lol, relay.snort.social, relay.primal.net, relay.damus.io)
ensure users can always publish even without configured write relays.

* fix(editor): use feed renderer and skeleton for event previews

When pasting event URLs in the editor, use KindRenderer (feed) and
EventCardSkeleton instead of DetailKindRenderer and EventDetailSkeleton.

Event previews in the editor are inline/compact like feed items, so they
should use the feed renderer rather than the detail view renderer.

* fix(post): remove flex-1 and improve scrolling layout

- Change layout from flex-col with flex-1 to overflow-y-auto on outer container
- Remove flex-1 from all child elements to prevent layout issues with complex posts
- Hide scrollbar in RichEditor for cleaner appearance
- Single scrollbar on container prevents double-scrollbar issues
- Editor now truly full-width without extra padding

This creates a more predictable layout that avoids overlaps and weird
behavior when posts contain lots of content (images, videos, embedded events).

* feat(post): add max-width constraint to content

Add max-w-2xl (768px) with mx-auto to center and constrain post content
width, similar to ZapWindow. Prevents content from becoming too wide on
large screens while maintaining good readability.

* fix(editor): add remove button and fix cursor positioning for event embeds

- Add X button on hover to NostrEventPreviewRich (same pattern as BlobAttachmentRich)
- Fix cursor positioning after pasting nevent/naddr by tracking node sizes correctly
- Import TextSelection from prosemirror-state to properly set cursor position
- Change from `tr.insert(from + index, node)` to `tr.insert(insertPos, node)` with cumulative position tracking

Previously, cursor would stay behind the event embed node after pasting.
Now it correctly positions after the inserted content.

* feat(post): add discard button, settings dropdown, and improve button layout

- Add Discard button to clear editor state and draft
- Add Settings dropdown with client tag toggle (enabled by default)
- Limit publish button width to w-32 and push to right with spacer
- Conditionally include client tag based on setting
- Improve action button layout with better spacing

* feat(post): add relay connectivity status, input widget, and improve error handling

- Show relay connectivity status (Server/ServerOff icons) next to each relay in list
- Add input widget to add custom relays during post composition
  - Validates relay URLs (supports with/without wss:// prefix)
  - Normalizes URLs automatically (defaults to wss://)
  - Shows Plus button enabled only when input is valid
  - Press Enter or click Plus to add relay
- Improve signing failure handling: draft is only cleared after successful publish
- New relay input widget disabled during preview mode
- Connect to relay pool state using use$() hook for reactive connectivity status

Technical details:
- Import Server, ServerOff, Plus icons and Input component
- Add newRelayInput state for relay URL input
- Use pool.relays$ observable to get real-time connection state
- Create isValidRelayInput() validator with URL pattern matching
- Create handleAddRelay() to normalize and add relays to list
- Update relay list rendering to show connectivity icons
- Add input + button widget after relay list

* feat(post): add auth status icons and window-specific draft persistence

- Show auth status icon next to connectivity icon for each relay
  - Uses getAuthIcon() utility from relay-status-utils
  - Displays shield icons for authenticated/failed/rejected states
  - Includes tooltip with auth status label
- Make draft persistence window-specific using window ID
  - Draft key format: `grimoire-post-draft-{pubkey}-{windowId}`
  - Each post window maintains its own independent draft
  - Falls back to pubkey-only key when windowId not available
- Remove toast notification when adding relays (less noisy UX)
  - Still shows error toast if relay URL is invalid
- Pass windowId prop from WindowRenderer to PostViewer

Technical details:
- Import getAuthIcon from @/lib/relay-status-utils
- Import useRelayState hook to get relay auth status
- Add PostViewerProps interface with optional windowId
- Update all draft key computations to include windowId
- Update dependency arrays for useEffect/useCallback with windowId
- Get relay state via getRelay(url) for auth icon display

* feat(post): add global persistent settings and event JSON preview

Settings (persist in localStorage, global across all post windows):
- "Include client tag" - toggle whether to add client tag to events (default: true)
- "Show event JSON" - display copyable JSON preview of event (default: false)

Features:
- Settings stored in localStorage at "grimoire-post-settings"
- Settings persist across sessions and windows
- Event JSON preview shows unsigned event initially, updates to signed version after signing
- Preview displays "(Signed)" or "(Unsigned)" label
- Uses CopyableJsonViewer component for syntax highlighting and copy button
- Preview limited to max-h-64 with scrolling

Draft persistence improvements:
- Added relays now saved in draft state under "addedRelays" field
- On draft load, custom relays are restored to relay list
- Relays identified as "added" if not in user's write relay list

Technical details:
- Added PostSettings interface with includeClientTag and showEventJson fields
- Settings loaded on mount from localStorage, saved on change via useEffect
- updateSetting callback handles individual setting updates
- previewEvent state holds current event (unsigned or signed)
- setPreviewEvent called before and after signing in handlePublish
- Draft format: { editorState, selectedRelays, addedRelays, timestamp }
- Draft loading checks addedRelays and restores non-duplicate relays

* feat(post): add live draft JSON preview with 2-second updates

This commit enhances the POST command with a live JSON preview feature:

- Added generateDraftEventJson() that builds unsigned draft event from editor content
- JSON preview updates every 2 seconds when "Show event JSON" setting is enabled
- Extracts all tags from editor: mentions (p), events (e), addresses (a), emojis, blobs (imeta)
- Displays as "Event JSON (Draft - Unsigned)" to distinguish from signed events
- Uses CopyableJsonViewer component for syntax highlighting and copying

The live preview helps users understand event structure before publishing.

* feat(post): improve JSON preview responsiveness and layout

This commit addresses UX feedback on the JSON preview feature:

- Reduced update interval from 2000ms to 200ms for much more responsive UI
- Moved JSON preview below relay list for better visual flow
- Increased max-height from 256px to 400px for better visibility
- Separated JSON update logic into dedicated effect for cleaner code
- JSON preview now feels instant when typing (10x faster updates)

The JSON preview is now much more pleasant to use and doesn't feel
laggy when editing content.

* refactor(post): replace intervals with debounced onChange handlers

This commit improves the code quality and UX of the POST command:

**Debounced onChange Pattern:**
- Added onChange prop to RichEditor that fires on every content change
- Replaced interval-based updates with proper debounced handlers
- Draft saves debounced to 2000ms (unchanged behavior, cleaner code)
- JSON updates debounced to 200ms (same responsiveness, event-driven)
- Much cleaner React pattern - no weird intervals

**JSON Preview Improvements:**
- Removed "Event JSON" title header for cleaner look
- Shows signed event JSON when published and setting is enabled
- Shows draft (unsigned) JSON when composing
- Always scrollable up to 400px height

**Technical Improvements:**
- Added onUpdate handler to TipTap editor config
- Proper cleanup of debounce timeouts on unmount
- Type-safe timeout refs using ReturnType<typeof setTimeout>

* refactor(post): remove JSON preview and ensure proper content serialization

**Removed JSON preview feature:**
- Removed showEventJson setting and all related UI
- Removed draftEventJson state and generation logic
- Removed "Show event JSON" checkbox from settings
- Simplified PostViewer code by ~100 lines
- Cleaner, production-ready codebase

**Ensured proper content serialization:**
- Added renderText() to Mention extension to serialize as nostr:npub URIs
- Added renderText() to BlobAttachmentRichNode to serialize URLs
- NostrEventPreviewRichNode already had renderText() for nostr:note/nevent/naddr
- Editor now properly converts all rich content to plain text:
  - @mentions → nostr:npub...
  - Event references → nostr:note/nevent...
  - Address references → nostr:naddr...
  - Blob attachments → URLs
  - Custom emojis → :shortcode:

**Result:**
- Cleaner, simplified code ready for production
- All editor elements properly serialize to content string
- JSON preview can be re-added later with better implementation

* perf(post): reduce draft save debounce from 2s to 500ms

Makes the draft saving feel more responsive without hammering localStorage
on every keystroke.

* fix(post): preserve content when signing fails or is rejected

Critical bug fix: Separate signing from publishing to handle failures correctly.

**The Problem:**
- When user rejected signing (or signing failed), we treated it as a publishing failure
- Set all relay states to "error" even though we never tried to publish
- Destroyed the user's post content

**The Solution:**
- Split signing and publishing into separate try-catch blocks
- If signing fails: show toast, reset isPublishing, and RETURN EARLY
- User keeps their content and can try again
- Only update relay states if we actually attempted to publish

**User Experience:**
- User rejects signing → toast appears, post is preserved, can edit or try again
- Publishing fails → relay states show errors, can retry individual relays
- No more losing your carefully crafted post to a rejected signature!

* refactor(post): use Promise.allSettled for relay publishing

Use Promise.allSettled instead of Promise.all to wait for all relay
publish attempts to complete, regardless of individual success/failure.

More defensive and semantically correct since we're handling errors
individually per relay and want all attempts to finish.

* feat(post): use GRIMOIRE_CLIENT_TAG constant with NIP-89 app address

Update PostViewer to use the new GRIMOIRE_CLIENT_TAG constant from main
which includes the NIP-89 app definition address (31990) as the third
element.

Before: ["client", "grimoire"]
After: ["client", "grimoire", "31990:..."]

This allows Nostr clients to display a clickable link to Grimoire's
app definition event.

* fix(post): preserve content when publishing fails to all relays

Critical UX improvement: Only clear draft and show preview if at least
one relay succeeded.

**The Problem:**
- When publishing failed to ALL relays, we still cleared the draft
- User lost their content with no way to retry
- Frustrating experience when all relays are down

**The Solution:**
- Track success/failure by returning result from each publish promise
- Count successful publishes from Promise.allSettled results
- Only clear draft and show preview if successCount > 0
- Keep editor visible with content if all relays fail

**User Experience:**
- All relays succeed → Clear draft, show preview, success toast
- Some relays succeed → Clear draft, show preview, warning toast
- All relays fail → Keep editor with content, error toast, can retry

**Toast Messages:**
- All succeeded: "Published to all X relays"
- Partial success: "Published to X of Y relays"
- Total failure: "Failed to publish to any relay. Please check..."

* fix(post): preserve content when signing or publishing

Critical fix: Don't clear editor content until publish succeeds.

**The Problem:**
- RichEditor automatically cleared content on submit
- Content was wiped immediately when clicking "Publish"
- User lost their content before knowing if publish succeeded

**The Solution:**
- Remove automatic `clearContent()` from RichEditor's handleSubmit
- Let PostViewer control when to clear the editor
- Only clear editor after successful publish (successCount > 0)

**User Experience Now:**
- Click "Publish" → content stays visible while publishing
- Publishing succeeds → content clears, show preview
- Publishing fails → content stays, user can edit and retry
- User rejects signing → content stays, can try again

No more losing your post to a rejected signature or failed publish!

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-01-21 13:03:30 +01:00
2025-12-19 12:49:29 +01:00
2025-12-22 20:40:16 +00:00
2025-12-20 14:25:40 +01:00
2026-01-14 19:24:37 +01:00

Grimoire

A Nostr protocol explorer and developer tool with a tiling window manager interface.

Features

  • Tiling Windows - Each window is a Nostr "app" (profile viewer, event feed, NIP docs, etc.)
  • Command Palette - Unix-style commands via Cmd+K to open apps and navigate
  • Multi-workspace - Virtual desktops with independent layouts
  • Real-time - Reactive event subscriptions with automatic updates

Stack

React 19, TypeScript, Vite, TailwindCSS, Jotai, Dexie, Applesauce

Getting Started

npm install
npm run dev

Scripts

Command Description
npm run dev Start dev server
npm run build Build for production
npm test Run tests in watch mode
npm run lint Lint code
npm run format Format code

License

MIT

Description
No description provided
Readme MIT 14 MiB
Languages
TypeScript 98.9%
CSS 0.8%
JavaScript 0.3%