For BYOK (Bring Your Own Key) scenarios, OpenRouter returns:
- cost: 0 (no charge to OpenRouter account)
- cost_details.upstream_inference_cost: actual API cost
Now checks upstream_inference_cost first, which contains the actual
cost regardless of billing mode.
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
Different providers use different field names for reasoning traces:
- Claude/DeepSeek: delta.reasoning_content
- OpenAI/OpenRouter: delta.reasoning
Now checks both fields to ensure reasoning is captured regardless of
which format the API uses.
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
The model was showing "auto" instead of the actual model (e.g.,
"anthropic/claude-opus-4.5") because we were storing the requested
modelId rather than the model from the API response.
Now properly captures:
- chunk.model: the actual model that generated the response
- chunk.usage.cost or chunk.usage.cost_details.upstream_inference_cost:
API-provided cost (falls back to calculated cost if not available)
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
Per-message cost display:
- Add model, usage, cost fields to AssistantMessage (local-only, not sent to API)
- Store cost/model/usage when saving assistant messages in session-manager
- Display subtle cost footer on assistant messages: "gpt-4o-mini • 847 tokens • $0.0012"
- Clean model names by removing provider prefixes and date suffixes
Session cost display:
- Calculate total conversation cost from messages
- Show "Session: $0.0234" above input area when cost > 0
- Updates automatically as messages are added
Cost formatting:
- Show 4 decimal places for costs < $0.01
- Show 2 decimal places for costs >= $0.01
- Show "<$0.0001" for very small costs
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
- Add LLMSystemPrompt type and llmSystemPrompts table to Dexie (v19)
- Create SystemPromptManager with built-in Grimoire prompt that includes
Nostr protocol knowledge (kinds, NIPs, commands, tools)
- Add useSystemPrompts and usePromptOptions hooks for React integration
- Add AIPromptsViewer component for viewing/creating/editing prompts
- Add prompt selector to new conversation UI in AIViewer
- Wire systemPromptId through conversation creation to API calls
- Prepend system prompt to messages when calling LLM API
The Grimoire prompt dynamically includes:
- Nostr protocol basics
- Common event kinds with descriptions
- Key NIPs for reference
- Available Grimoire commands
- Registered tools
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
- Initialize finishReason to "stop" in new sessions (was undefined)
- Fix canResume to only be true when there's actually something to resume:
- tool_calls: model called tools, waiting for results
- length: response was truncated
- null with streamingContent: user aborted mid-stream
Previously, undefined finishReason passed the check (undefined !== "stop")
causing Resume button to appear on fresh conversations.
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
Two bugs were causing the first message in a new conversation to fail:
1. Session not found: sendMessage threw "No session found" because the
session was only created in useEffect (which runs after sendMessage).
Fix: Auto-open session in sendMessage if one doesn't exist, using
the conversation's provider/model from Dexie.
2. Pending message cleared: useEffect that resets state on conversationId
change also ran when creating a new conversation, clearing the
pendingUserMessage we just set.
Fix: Track isCreatingConversationRef and skip the reset when we're
in the middle of creating a conversation.
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
- Extend LLMMessage types to support tool calls, tool results, multimodal
content (images), and reasoning content (extended thinking)
- Add ChatStreamChunk types for tool_call and reasoning streaming
- Implement agentic loop in session manager that continues generation
after tool execution until finish_reason is "stop"
- Create tool interface and registry for registering executable tools
- Update provider manager to stream tool calls and pass tools to API
- Update AIViewer UI to render tool calls, tool results, and reasoning
- Clean up legacy hooks in useLLM.ts (replaced by session manager)
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
Introduce a production-quality session management system for AI chat:
Architecture:
- ChatSessionManager singleton manages all active chat sessions
- Sessions keyed by conversationId (not window) for multi-window support
- Multiple windows viewing same conversation share streaming state
- Reference counting with delayed cleanup for quick tab switching
Features:
- RxJS BehaviorSubject for reactive session state
- Subject streams for events (streaming, messages, errors)
- Automatic usage and cost tracking per session
- Resume functionality when generation is interrupted
- Proper abort handling with partial message saving
React Integration:
- useChatSession(conversationId) - main hook combining Dexie + session state
- useChatActions() - stable action functions
- useConversations() - reactive conversation list
- useStreamingContent() - lightweight streaming-only subscription
Migrated AIViewer to use new hooks, significantly simplifying the
component by removing direct DB operations and complex state management.
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
Fix multiple state consistency bugs in the AI chat UI:
1. Model reset race condition: When selecting a conversation with a
different provider, the model was being set correctly but then
immediately reset to null by an effect. Now uses refs to track
when we're selecting a conversation that specifies its own model.
2. Stale closure in first message: When creating a new conversation
and adding the first message, addMessage had a stale conversationId
in its closure and returned null. Now uses direct DB operations
with the correct conversation ID.
3. No state cleanup on conversation switch: streamingContent,
pendingUserMessage, and isWaitingForResponse now reset when
switching conversations.
4. Direct DB access consistency: All message operations now use
direct DB access to avoid stale closure issues.
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
Remove WebLLM (local model) support and standardize all AI providers
on the OpenAI-compatible API pattern using the official OpenAI SDK.
Key changes:
- Add provider presets for PPQ, OpenRouter, OpenAI, Anthropic, DeepSeek,
Groq, Together, Fireworks, xAI, and custom providers
- Create unified OpenAI client factory with provider-specific handling
- Dynamic model fetching from provider /v1/models endpoints
- Add connection testing before saving providers
- Two-step provider add flow: select preset → configure API key
- Track recently used models per provider
- Remove webllm-provider.ts, webllm-worker.ts, ppq-provider.ts
- Simplify AIViewer UI by removing WebLLM status checks
This follows the architecture patterns from shakespeare/soapbox-pub
for a more maintainable multi-provider LLM chat implementation.
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
- Fix lint error by using regular import instead of require()
- Load conversation's provider and model when selecting a conversation
- Fix thinking indicator to be small muted text instead of bubble
- Skip rendering empty assistant message placeholders
- Show "Add a provider" button when no providers configured
- Remove WebLLM model selector from main ai command
- Clean up unused imports and variables
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
- Show user messages immediately (optimistic UI)
- Add thinking indicator while waiting for AI response
- Remember last used model per provider
- Pre-select last model when switching providers
- Fall back to first available model if last isn't available
- Disable input while waiting for response
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
- Add 'ai providers' subcommand for provider management
- Add markdown rendering for AI responses using react-markdown
- Add thinking indicator with brain icon while waiting for response
- Handle PPQ 402 Payment Required with 'Insufficient balance' message
- Extract AIProvidersViewer to separate component
- Move 'Add Provider' button to providers interface only
- Fix chat input width to be full width
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
Implements browser-based AI chat functionality:
- WebLLM provider for local inference via WebGPU
- Downloads and caches models in IndexedDB
- Runs inference in web worker to keep UI responsive
- Curated list of recommended models (SmolLM2, Llama 3.2, Phi 3.5, etc.)
- PPQ.ai provider for cloud-based inference
- OpenAI-compatible API with Lightning payments
- Dynamic model list fetching with caching
- Provider manager for coordinating multiple providers
- Dexie tables for persisting provider instances and conversations
- Model list caching with 1-hour TTL
- AIViewer component with sidebar pattern
- Conversation history in resizable sidebar (mobile: sheet)
- Model selector for WebLLM with download progress
- Streaming chat responses
- Auto-generated conversation titles
- New `ai` command to launch the interface
https://claude.ai/code/session_01HqtD9R33oqfB14Gu1V5wHC
The --type flag for blossom blob command was being parsed correctly but
the mediaType and blobUrl props were not forwarded from WindowRenderer
to BlossomViewer, causing the preview to always show "preview not available".
https://claude.ai/code/session_018edX9kUcEudJzwuFCGh1xf
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add custom media renderer support to RichText
Add renderMedia prop to RichText component that allows overriding
how images, videos, and audio are rendered across the entire
RichText subtree (including Link and Gallery components).
The custom renderer receives:
- url: the media URL
- type: "image" | "video" | "audio"
- imeta: optional NIP-92 metadata (dimensions, blurhash, alt, etc.)
This enables chat-specific media rendering with compact thumbnails,
lightboxes, or any custom presentation.
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* feat: add ChatMediaRenderer for compact inline media display
Custom media renderer for chat that shows:
- Media type icon (image/video/audio)
- Filename (extracted from URL or imeta alt)
- File size (from imeta if available)
- Blossom link button (opens blob viewer for blossom URLs)
Usage:
<RichText event={message} renderMedia={ChatMediaRenderer} />
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* feat: use ChatMediaRenderer in chat messages
- Updated ChatMediaRenderer to use getHashFromURL from blossom-client-sdk
- Integrated ChatMediaRenderer into ChatViewer for message content
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* style: make ChatMediaRenderer inline with subtle dotted border
Use a subtle dotted bottom border instead of background/padding
to better integrate with surrounding text flow.
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* feat: improve ChatMediaRenderer UX
- Full dotted border with rounded corners (was bottom-only)
- Increased gap between elements
- Images/videos open in MediaDialog zoom viewer on click
- Audio still opens in new tab
- Better extension extraction for display
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* feat: improve blossom integration in ChatMediaRenderer
- Use HardDrive icon instead of Flower2 (consistent with rest of app)
- Add commandString for Edit functionality in window toolbar
- Pass blobUrl with extension to BlossomViewer for correct media preview
- Extended BlossomViewer.BlobDetailView to accept blobUrl prop
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* refactor: simplify ChatMediaRenderer styling
- All icons now size-3
- Use dotted underline only (cleaner inline appearance)
- All media types open in new tab on click
- Removed MediaDialog dependency
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* fix: inherit custom media renderer in nested RichText components
Previously, nested RichText components (like those in QuotedEvent/KindRenderer)
would always reset the media renderer context to null because the provider
used `renderMedia ?? null`. This caused embedded events in chat to use
the default media renderer instead of ChatMediaRenderer.
Now RichText inherits the parent media renderer when no explicit renderMedia
prop is passed, allowing custom renderers to work throughout the entire
rich text subtree including quoted events.
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* feat: improve ChatMediaRenderer with expandable inline media
- More compact representation: "icon truncated-hash blossom"
- Click filename to expand and show actual media inline (image/video/audio)
- Tooltip shows full filename and file size on hover
- Audio support with inline audio player when expanded
- Fixed BlossomViewer to detect media type from URL extension when
mimeType is not available from blob descriptor
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* feat: improve ChatMediaRenderer with better UX
- Click to expand shows MediaEmbed (not collapsible once expanded)
- Tooltip only shows when imeta is present, displays: alt, mime type,
dimensions, size, duration
- More compact representation: [icon] truncated-hash [blossom]
- Pass mediaType to BlossomViewer for correct preview rendering
- BlossomViewer now accepts mediaType hint for preview type detection
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* fix: blossom blob command now parses --type flag
- Added mediaType and blobUrl fields to BlossomCommandResult
- Blob subcommand now parses --type image|video|audio flag
- Fixed server URL overflow in blob detail view with truncation
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
* refactor: add global loadMedia setting with CompactMediaRenderer
- Add `loadMedia` setting to Appearance section (enabled by default)
- Create CompactMediaRenderer in src/components/nostr/ (renamed from ChatMediaRenderer)
- Link.tsx and Gallery.tsx now check the setting and use CompactMediaRenderer when disabled
- Tooltip format improved: "Field <value>" with field name and value side by side
- Shows: Hash, Size, Dimensions, Type, Duration, Alt
- Remove renderMedia prop from ChatViewer (now automatic based on setting)
- Delete old ChatMediaRenderer.tsx
This makes media rendering a site-wide setting rather than chat-specific.
https://claude.ai/code/session_01AeeN5d5EcVLGjZbGueZxaD
---------
Co-authored-by: Claude <noreply@anthropic.com>
* refactor: remove unused compactModeKinds from app state
The compactModeKinds feature was partially implemented but never used:
- Settings dialog allowed configuration but the value was never consumed
- REQ viewer uses --view flag for compact mode, not this state value
Removed:
- compactModeKinds from GrimoireState type and initial state
- setCompactModeKinds logic function and state hook callback
- Compact Events section from SettingsDialog (now shows placeholder)
- compactModeKinds from AppearanceSettings in settings service
- Migration now strips compactModeKinds if present instead of adding it
https://claude.ai/code/session_01DiWUxiS5BAzU9mrKvCUuMW
* chore: delete unused SettingsDialog component
The SettingsDialog was imported but never used - the Settings menu
item opens a window via addWindow("settings", ...) instead.
https://claude.ai/code/session_01DiWUxiS5BAzU9mrKvCUuMW
* refactor: simplify settings service to only used settings
Removed unused settings that were defined but never consumed:
- RelaySettings (fallbackRelays, discoveryRelays, outbox*, etc.)
- PrivacySettings (shareReadReceipts, blurWalletBalances, etc.)
- DatabaseSettings (maxEventsCached, autoCleanupDays, etc.)
- NotificationSettings (enabled, notifyOnMention, etc.)
- DeveloperSettings (debugMode, showEventIds, logLevel, etc.)
- Most of AppearanceSettings (theme, fontSizeMultiplier, etc.)
- Most of PostSettings (defaultRelayMode, customPostRelays)
Only kept settings that are actually used:
- post.includeClientTag
- appearance.showClientTags
Also simplified useSettings hook to match.
https://claude.ai/code/session_01DiWUxiS5BAzU9mrKvCUuMW
---------
Co-authored-by: Claude <noreply@anthropic.com>
* docs: add plan for repository tree visualization feature
Comprehensive plan covering:
- git-natural-api library analysis and API documentation
- useGitTree/useGitBlob hooks for fetching git data
- FileTreeView component using Radix Collapsible
- Shiki migration with on-demand language loading
- Multi-server fallback for redundant clone URLs
- Dexie caching for offline access
* docs: add comprehensive Shiki migration plan
Detailed plan for migrating from Prism.js to Shiki with lazy loading:
- Analysis of all 8 components using SyntaxHighlight
- Shiki service with singleton highlighter and on-demand language loading
- Custom Grimoire dark theme matching current Prism styles
- Language alias mapping for 50+ extensions
- React hook for async highlighting with loading states
- CSS theme preserving minimalistic dark aesthetics
- Migration steps with rollback plan
* feat: migrate syntax highlighting from Prism.js to Shiki
Replace Prism.js with Shiki for syntax highlighting with several key improvements:
- Lazy loading: Languages loaded on-demand via dynamic imports instead of bundling all upfront
- Broader language support: 200+ TextMate grammars vs 11 statically imported
- Singleton highlighter: Core languages (JS, TS, JSON, diff, bash) preloaded, others loaded on first use
New files:
- src/lib/shiki.ts: Shiki service with highlightCode(), normalizeLanguage(), language aliases
- src/hooks/useHighlightedCode.ts: React hook for async highlighting with loading states
- src/styles/shiki-theme.css: Grimoire dark theme matching previous minimalistic style
Updated components:
- SyntaxHighlight: Now uses Shiki with graceful loading/error states
- CodeSnippetRenderer/DetailRenderer: Simplified, removed manual language mapping
- MarkdownContent: Removed type casts, any language now supported
Removed:
- prismjs and @types/prismjs dependencies
- src/styles/prism-theme.css
* feat: add repository file tree visualization
Add file tree explorer to the Repository detail renderer (kind 30617)
using @fiatjaf/git-natural-api for fetching git trees via HTTP.
New files:
- src/lib/git-types.ts: TypeScript types for DirectoryTree, SelectedFile, etc.
- src/hooks/useGitTree.ts: Hook to fetch git repository tree from clone URLs
- Tries multiple clone URLs in sequence
- Uses getDirectoryTreeAt with filter capability when available
- Falls back to shallowCloneRepositoryAt otherwise
- src/hooks/useGitBlob.ts: Hook to fetch individual file content by hash
- Detects binary files
- Returns both raw Uint8Array and decoded text
- src/components/ui/FileTreeView.tsx: Recursive tree view component
- Collapsible directories with chevron icons
- File icons based on extension (code, json, text, image, etc.)
- Alphabetical sorting with directories first
- src/components/nostr/kinds/RepositoryFilesSection.tsx: Main integration
- Side-by-side tree and file preview layout
- Syntax-highlighted file content using existing SyntaxHighlight
- Binary file detection with appropriate UI
- Loading/error states
Modified:
- RepositoryDetailRenderer.tsx: Added RepositoryFilesSection below relays
Dependencies:
- Added @fiatjaf/git-natural-api from JSR
* fix: improve repository tree visualization UX
- Collapse directories by default in file tree
- Hide files section on tree loading error
- Add code-like skeleton loader with file header
- Fix syntax highlight size jump between loading/loaded states
- Replace purple accent with grayscale theme
- Preload Rust and Markdown languages for reliable highlighting
* refactor: improve git tree and syntax highlighting
- Remove shallow clone fallback from useGitTree, only use no-blobs fetch
Servers without filter capability are skipped instead of downloading blobs
- Add light theme support for Shiki syntax highlighting
Theme is automatically selected based on current color scheme
* fix: improve dark theme contrast for syntax highlighting
* fix: address code review issues
- useGitTree: use useStableArray for cloneUrls to fix dependency tracking
- useGitTree/useGitBlob: add isMounted checks to prevent state updates after unmount
- RepositoryFilesSection: remove unnecessary useMemo for language
- FileTreeView: use path instead of hash for React keys
- shiki: track failed languages to avoid repeated console warnings
* fix: improve dark theme contrast for syntax highlighting
- Add CSS variables for syntax highlighting instead of hardcoded colors
- Add --syntax-constant and --syntax-tag variables to light and dark themes
- Use high contrast colors for dark mode (bright green strings, purple keywords)
- Simplify Shiki transformer to output CSS classes instead of inline styles
- Remove unused parameters from transformer callback
* fix: restore syntax highlighting colors
Revert the CSS class-based approach which was failing to classify tokens.
Instead, keep Shiki's inline styles from the theme and only remove
backgrounds to let CSS handle those. The theme colors now provide
syntax highlighting directly.
* feat: add copy button and CSS variable-based syntax highlighting
- Add copy button next to file name in file viewer header (icon-only)
- Use Shiki's createCssVariablesTheme for proper theme integration
- Map Shiki CSS variables to our theme system variables
- Syntax highlighting now works correctly across all themes (light/dark)
* refactor: create IconCopyButton component and use CopyCheck consistently
- Add IconCopyButton component for reusable icon-only copy buttons
- Refactor RepositoryFilesSection to use IconCopyButton
- Replace Check with CopyCheck in ChatMessageContextMenu
- Replace Check with CopyCheck in BaseEventRenderer
- Use text-success instead of text-green-500 for consistency
* fix: add HTML, CSS, TOML to core languages and expand token mappings
- Add html, css, toml to CORE_LANGUAGES for eager loading
- Add variableDefaults to cssVarsTheme for proper initialization
- Expand shiki-theme.css with more token type mappings:
- HTML/XML: tag, attribute, attr-value
- CSS: selector, property
- Additional: variable, operator, number, boolean, regex, etc.
* fix: improve diff line spacing with flex layout
- Use flex-col with gap-0 on code element for tight line packing
- Reduce line-height from 1.5 to 1.4 for tighter spacing
- Add .line display:block with min-height for consistent sizing
- Simplify diff background styling (remove negative margin hack)
* fix: improve code block line spacing and wrap long lines
- Increase line-height from 1.4 to 1.5 for better readability
- Use pre-wrap instead of pre to allow long line wrapping
- Add overflow-wrap: break-word to break long URLs/strings
* chore: remove planning docs
* chore: update @fiatjaf/git-natural-api to 0.2.3
* fix: make code blocks horizontally scrollable with full-width diff backgrounds
- Use white-space: pre for horizontal scrolling instead of wrapping
- Add width: fit-content and min-width: 100% to code element
- Ensure diff line backgrounds extend full width when scrolling
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add "+ new tab" option to move window menu
Adds a "+ New tab" option at the top of the "Move to tab" submenu in the
window toolbar. This allows users to create a new tab and move the current
window to it in a single action, streamlining the workflow.
Changes:
- Add moveWindowToNewWorkspace function in logic.ts
- Expose moveWindowToNewWorkspace through useGrimoire hook
- Always show "Move to tab" menu (not just when multiple tabs exist)
- Add "+ New tab" option with Plus icon at top of submenu
https://claude.ai/code/session_01Hy2vYBooPyrF7ZJCodcCav
* refactor: simplify new tab menu item copy
- Change menu text from "+ New tab" to "New" (icon already shows +)
- Simplify toast to just "Moved to new tab"
https://claude.ai/code/session_01Hy2vYBooPyrF7ZJCodcCav
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(nwc): improve connection reliability and add health tracking
- Add connection status observable (disconnected/connecting/connected/error)
- Validate wallet connection on restore using support$ observable with 10s timeout
- Add notification subscription error recovery with exponential backoff (5 retries)
- Add retry logic for balance refresh (3 retries with backoff)
- Use library's support$ observable for wallet capabilities (cached by applesauce)
- Replace manual getInfo() calls with reactive support$ subscription
- Add visual connection status indicator in WalletViewer header
- Add reconnect button when connection is in error state
- Store network info in cached connection for display
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* feat(wallet): add copy NWC connection string button to header
Adds a copy button (Copy/Check icons) in the wallet header before the
refresh button that copies the NWC connection string to clipboard for
easy sharing or backup.
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* fix(wallet): use CopyCheck icon and fix transaction loading
- Change Check to CopyCheck icon for copy NWC string button
- Add walletMethods computed value that combines support$ observable
with cached info fallback from initial connection
- Fix transaction history not loading because support$ waits for
kind 13194 events which many NWC wallets don't publish
- The cached info from getInfo() RPC call is now used as fallback
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* refactor(nwc): simplify with derived state from observables
Production-ready refactor of NWC implementation:
nwc.ts:
- Add wallet$ observable for reactive wallet instance access
- Remove redundant subscribeToSupport() - only needed for validation
- Cleaner code organization with clear sections
useWallet.ts:
- All state derived from observables (no useState for wallet)
- Move walletMethods computation to hook (reusable)
- isConnected derived from connectionStatus
- Simplified from 240 to 170 lines
WalletViewer.tsx:
- Use walletMethods from hook instead of local useMemo
- Simpler connection state tracking via connectionStatus
- Remove redundant wallet variable from destructuring
- No color change on copy NWC string (per feedback)
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* refactor(wallet): use useCopy hook for clipboard operations
Replace manual clipboard state management with useCopy hook:
- copyInvoice/invoiceCopied for generated invoice
- copyRawTx/rawTxCopied for transaction JSON
- copyNwc/nwcCopied for NWC connection string
Benefits:
- Cleaner code (removed manual setTimeout calls)
- Automatic timeout cleanup on unmount
- Consistent copy behavior across all clipboard operations
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* feat(wallet): implement lazy-loaded transactions observable
- Add shared wallet types (Transaction, TransactionsState) in src/types/wallet.ts
- Add transactionsState$ observable to NWC service for shared tx state
- Implement loadTransactions, loadMoreTransactions, and retryLoadTransactions
- Auto-refresh transactions on payment notifications
- Simplify WalletViewer to use observable state instead of local state
- Remove manual transaction loading logic from component
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* fix(wallet): update balance observable on initial connect
- Call refreshBalance() in createWalletFromURI to fetch initial balance
- Update balance$ directly when ConnectWalletDialog gets balance
- Fixes issue where WalletViewer showed "-" after connecting while
user menu showed correct balance (different data sources)
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* refactor(wallet): use single data source for balance across UI
Remove fallback to nwcConnection.balance in user-menu - now both
WalletViewer and user-menu use balance$ observable as the single
source of truth for wallet balance.
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* fix(wallet): address code review issues and simplify user menu
- Fix memory leak: track retry timeout and clear on disconnect
- Add explicit WalletSupport type for support observable
- Add comments explaining balance refresh error handling behavior
- Add comment about restoreWallet not being awaited intentionally
- User menu now uses connectionStatus observable (shows connecting/error states)
- Remove wallet name display from user menu (simplifies UI)
- Remove unused walletServiceProfile hook and getWalletName function
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
* refactor(wallet): extract WalletConnectionStatus component
- Create reusable WalletConnectionStatus component for connection indicator
- Remove rounded borders from indicator (now square)
- Export getConnectionStatusColor helper for custom usage
- Use component in both user-menu and WalletViewer
- Supports size (sm/md), showLabel, and className props
https://claude.ai/code/session_01CnJgjFMvZHZWs2ujAiWAiQ
---------
Co-authored-by: Claude <noreply@anthropic.com>
When images are turned off in the RichText options, galleries now show
a single [gallery] placeholder instead of multiple [image] placeholders.
This provides a cleaner UI for users who have disabled image loading.
https://claude.ai/code/session_01LAhYP5iRQskJC4XWi6MHvf
Co-authored-by: Claude <noreply@anthropic.com>
* fix: hide tabbar scrollbar by setting width/height to 0
The no-scrollbar utility's display:none wasn't fully overriding the
global scrollbar styles that set width/height to 8px. Adding explicit
width:0 and height:0 ensures the scrollbar is completely hidden in
WebKit browsers.
https://claude.ai/code/session_018RiPf74GNf2oWcoYNRoZyx
* fix: use !important to override global scrollbar styles
The global * selector's scrollbar-width: thin was overriding the
no-scrollbar utility due to CSS cascade order. Adding !important
ensures the utility always wins.
https://claude.ai/code/session_018RiPf74GNf2oWcoYNRoZyx
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: build proper q-tag with relay hint and author pubkey for replies
When sending replies in NIP-29 and NIP-C7 adapters, now build the full
q-tag format per NIP-C7 spec: ["q", eventId, relayUrl, pubkey]
Previously only the event ID was included, making it harder for clients
to fetch the referenced event. Now:
- NIP-29: includes group relay URL and author pubkey
- NIP-C7: includes seen relay hint and author pubkey
https://claude.ai/code/session_01Jy51Ayk57fzaFuuFFm1j1K
* chore: remove unused NIP-C7 adapter
The NIP-C7 adapter was already disabled/commented out everywhere.
Removing the file to reduce dead code.
https://claude.ai/code/session_01Jy51Ayk57fzaFuuFFm1j1K
* chore: remove NIP-C7 references from docs and code
- Remove nip-c7 from ChatProtocol type
- Remove commented NIP-C7 adapter imports and switch cases
- Update comments to reference NIP-29 instead of NIP-C7
- Update kind 9 renderer docs to reference NIP-29
- Clean up chat-parser docs and error messages
https://claude.ai/code/session_01Jy51Ayk57fzaFuuFFm1j1K
---------
Co-authored-by: Claude <noreply@anthropic.com>
- Add useIsMobile hook for viewport detection
- TabBar: larger height (48px), disable reorder on mobile, hide workspace numbers
- Header buttons: larger touch targets for SpellbookDropdown, UserMenu, LayoutControls
- Window toolbar: larger buttons (40px) on mobile
- Mosaic dividers: wider (12px) on mobile for easier dragging
- CommandLauncher: larger items, footer text, hide kbd hints on mobile
- Editor suggestions: responsive widths, min-height 44px touch targets
- EventFooter: larger kind/relay buttons on mobile
- CodeCopyButton: larger padding and icon on mobile
- MembersDropdown: larger trigger and list items on mobile
All changes use mobile-first Tailwind (base styles for mobile, md: for desktop)
to meet Apple HIG 44px minimum touch target recommendation.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat: add -f (follow) option to req command
Add tail -f style auto-refresh behavior to the req command. When enabled,
new events are automatically displayed instead of being buffered behind
a "X new events" button.
The SVG path extended slightly beyond the viewBox (bezier control points
reached x=121.464 while viewBox ended at x=121), while the left edge was
flush at x=0. This caused uneven spacing.
Changed viewBox from "0 0 121 160" to "-0.5 0 122 160" to add equal
0.5px margins on both sides, properly centering the logo content.
https://claude.ai/code/session_019PGCQHRovoNE81udohhU3R
Co-authored-by: Claude <noreply@anthropic.com>
* fix: filter invalid relay URLs from event tags
Add validation to prevent invalid URLs from being used as relay hints.
The issue occurred when "r" tags containing non-relay URLs (like
https://(strangelove@basspistol.org/) were being extracted and used
as relay connection targets.
Changes:
- Add isValidRelayURL() helper to validate relay URLs (must have ws://
or wss:// protocol and valid URL structure)
- Update extractRelayContext() in loaders.ts to filter r-tags, e-tag
relay hints, and a-tag relay hints using the new validator
- Add comprehensive tests for isValidRelayURL()
https://claude.ai/code/session_01Ca2fKD2r4wHKRD8rcRohj9
* refactor: use applesauce isSafeRelayURL for relay URL validation
Refactor relay URL validation to use applesauce's isSafeRelayURL helper
which provides a fast regex-based check for valid websocket URLs.
Changes:
- Update isValidRelayURL in relay-url.ts to use isSafeRelayURL as fast
path, with URL constructor fallback for IP addresses
- Re-export isSafeRelayURL from relay-url.ts for convenience
- Update loaders.ts to use isSafeRelayURL directly from applesauce
- Add relay URL validation to:
- nostr-utils.ts: getEventPointerFromQTag (q-tag relay hints)
- zapstore-helpers.ts: getAppReferences (a-tag relay hints)
- nip89-helpers.ts: getHandlerReferences (a-tag relay hints)
- PublicChatsRenderer.tsx: extractGroups (group relay URLs)
This ensures consistent validation across all relay URL extraction points
using applesauce's battle-tested validation.
https://claude.ai/code/session_01Ca2fKD2r4wHKRD8rcRohj9
* chore: remove unused isSafeRelayURL re-export
The re-export was added but all consumers import directly from
applesauce-core/helpers/relays instead.
https://claude.ai/code/session_01Ca2fKD2r4wHKRD8rcRohj9
---------
Co-authored-by: Claude <noreply@anthropic.com>
The client tag was being added twice to spells:
1. Unconditionally in encodeSpell() in spell-conversion.ts
2. Conditionally in publish-spell.ts based on user settings
Removed the unconditional addition in encodeSpell() so the client tag
is only added once by publish-spell.ts when the user setting is enabled.
Co-authored-by: Claude <noreply@anthropic.com>
* feat: add Grimoire logo SVG and generate PWA assets
- Add logo.svg with the official Grimoire logo and gradient
- Create GrimoireLogo React component for use in the app
- Add scripts/generate-pwa-icons.mjs to generate all PWA icons from SVG
- Regenerate all favicon and PWA icons from the new logo
- Update mobile welcome screen to show the logo instead of text
* feat: use transparent backgrounds for PWA icons and add theme gradient option
- Update generate-pwa-icons.mjs to output PNGs with transparent backgrounds
- Add gradient prop to GrimoireLogo component ("original" or "theme")
- Theme gradient matches text-grimoire-gradient CSS (yellow -> orange -> purple -> cyan)
- Mobile welcome screen now uses theme gradient to match ASCII art
* feat: use original gradient for mobile welcome logo
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix: pass source event to RichText in highlight feed preview
The source event preview in HighlightRenderer was only passing the
content string to RichText, which meant custom emoji tags from the
source event weren't processed. Now passes the source event with
the preview content to enable proper emoji and tag-based rendering.
* refactor: use CSS truncation for highlight source preview
- Pass sourceEvent directly for notes instead of extracting content
- Only create synthetic event with title for articles
- CSS line-clamp-1 and overflow-hidden handle truncation
- Media and event embeds remain disabled
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(nip34): Add NIP-34 issue status renderers and locale-aware formatting
- Add IssueStatusRenderer for feed view (kinds 1630-1633: Open/Resolved/Closed/Draft)
- Add IssueStatusDetailRenderer for detail view with status badge and embedded issue
- Update IssueRenderer/IssueDetailRenderer to fetch and display current issue status
- Status validation respects issue author, repo owner, and maintainers
- Add status helper functions to nip34-helpers.ts (getStatusType, findCurrentStatus, etc.)
- Use parseReplaceableAddress from applesauce-core for coordinate parsing
- Expand formatTimestamp utility with 'long' and 'datetime' styles
- Fix locale-aware date formatting across all detail renderers
- Update CLAUDE.md with useLocale hook and formatTimestamp documentation
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* refactor(nip34): Use theme semantic colors for issue status
Replace hardcoded colors with theme semantic colors:
- Resolved/merged: accent (positive)
- Closed: destructive (negative)
- Draft: muted
- Open: neutral foreground
Also fixes import placement in nip34-helpers.ts.
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* fix(nip34): Use repository relays instead of AGGREGATOR_RELAYS
Status events for issues are now fetched from the relays configured
in the repository definition, not from hardcoded aggregator relays.
This respects the relay hints provided by repository maintainers for
better decentralization and reliability.
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* perf(nip34): Add memoization caching to helper functions
Use getOrComputeCachedValue from applesauce-core to cache computed
values on event objects. This prevents redundant computation when
helpers are called multiple times for the same event.
Also added documentation in CLAUDE.md about best practices for
writing helper libraries that compute data from Nostr events.
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* fix(nip34): Add relay fallback chain for status event fetching
Status events now use a fallback chain for relay selection:
1. Repository configured relays (from "relays" tag)
2. Repo author's outbox relays (from kind:10002)
3. AGGREGATOR_RELAYS as final fallback
This ensures status events can be fetched even when repository
doesn't have relays configured.
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* feat(nip34): Add status rendering to Patch and PR renderers
- PatchRenderer and PatchDetailRenderer now show merge/closed/draft status
- PullRequestRenderer and PullRequestDetailRenderer now show merge/closed/draft status
- Status events fetched from repository relays with author outbox fallback
- For patches and PRs, kind 1631 displays as "merged" instead of "resolved"
- Fixed destructive color contrast in dark theme (30.6% -> 50% lightness)
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* refactor(nip34): Extract StatusIndicator component, improve UI layout
- Create reusable StatusIndicator component for issues/patches/PRs
- Move status icon next to status text in feed renderers (not title)
- Place status badge below title in detail renderers
- Fix dark theme destructive color contrast (0 90% 65%)
- Remove duplicate getStatusIcon/getStatusColorClass functions
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* fix(nip34): Make status badge width fit content
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* fix(theme): Improve destructive color contrast on dark theme
Increase lightness from 65% to 70% for better readability.
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* fix(theme): Use lighter coral red for destructive on dark theme
Changed to 0 75% 75% (~#E89090) for better contrast against #020817 background.
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* docs: Fix applesauce helper documentation in CLAUDE.md
- Fix parseCoordinate -> parseReplaceableAddress (correct function name)
- Clarify getTagValue (applesauce) vs getTagValues (custom Grimoire)
- Add getOrComputeCachedValue to helpers list
- Improve code example with proper imports and patterns
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* fix(nip34): Render status event content as rich text
Use MarkdownContent component for status event content in
Issue, Patch, and PR detail renderers.
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* fix(nip34): Smaller status indicators, improve issue feed layout
- Use shared StatusIndicator in IssueStatusRenderer (smaller size)
- Render status event content as markdown
- Put status on its own line between title and repo in IssueRenderer
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
* fix(nip34): Use warning color for closed status instead of destructive
- Change closed status from red (destructive) to orange (warning)
- Improve dark theme status colors contrast (warning: 38 92% 60%)
- Less aggressive visual for closed issues/patches/PRs
https://claude.ai/code/session_01C6Lty4k9pKxdwnYUCcpzV2
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add NIP-56 Report renderer (kind 1984)
- Add nip56-helpers.ts with report parsing and type definitions
- Add ReportRenderer for displaying report events in feeds
- Support profile, event, and blob report targets
- Display report type with color-coded badges and icons
- Show embedded reported event when available
https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ
* refactor(nip56): apply applesauce caching and neutral styling
- Use getOrComputeCachedValue for report parsing (applesauce pattern)
- Rename parseReport to getReportInfo for consistency
- Use muted/neutral colors for all report type icons and badges
- Use QuotedEvent component for embedding reported events
- Remove unnecessary useMemo (helper caches internally)
https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ
* refactor(nip56): use collapsed quote and cleaner copy
- Use "Reported <username> for <reason>" format
- Remove redundant "Event by:" line for event reports
- Use depth=2 for QuotedEvent to show collapsed by default
- Content may be disturbing, user can expand on demand
https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ
* feat(nip56): hide preview and clickable header
- Add hidePreview prop to QuotedEvent for sensitive content
- Hide text preview when collapsed, show "Click to reveal content"
- Make report header clickable to open report detail
- UserName stops propagation so clicking username opens profile
https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ
* style(nip56): use dotted underline hover, reduce spacing
- Remove background highlight on hover
- Use underline dotted with cursor crosshair (consistent with app)
- Reduce gap between header and quoted event
https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ
* refactor(nip56): use RichText for comment and applesauce helpers
- Render report comments using RichText like kind 1 notes
- Use getTagValue/getTagValues helpers instead of direct tag access
- Add explanatory comments where direct tag access is still required
https://claude.ai/code/session_012ux81GyM8iZ1GLnKHC7esJ
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(nip88): add poll event renderers
Implement NIP-88 poll events support with:
- PollRenderer: Feed view for kind 1068 polls showing question, options, type
- PollDetailRenderer: Detail view with live vote counts and percentages
- PollResponseRenderer: Feed view for kind 1018 showing voted options
- nip88-helpers: Utilities for parsing poll data and counting votes
* fix(nip88): use RelayLink component and differentiate poll type icons
- Use ListCheck icon for single choice, ListChecks for multi choice
- Replace relay text spans with clickable RelayLink components
- Shorten "Vote on these relays" to "Relays"
* refactor(nip88): production readiness improvements
- Add symbol-based caching to all helper functions using getOrComputeCachedValue
- Use QuotedEvent component for embedded polls in PollResponseRenderer
- Simplify PollResponseRenderer by leveraging QuotedEvent's loading states
- Add clear documentation about what can/cannot be cached
* fix(nip88): restore option label resolution in PollResponseRenderer
---------
Co-authored-by: Claude <noreply@anthropic.com>
Add a search input to KindsViewer matching the style from NipsViewer.
Users can now filter kinds by number, name, or description. Supports
autofocus on mount, clear button, and Escape to clear.
Co-authored-by: Claude <noreply@anthropic.com>
The chat input now grows as you type, from 1 line up to approximately
3 lines before showing a scrollbar. This improves UX when composing
longer messages by providing better visibility of the content.
Co-authored-by: Claude <noreply@anthropic.com>