The main element was using h-full min-h-0 which doesn't work because
the parent wrapper div has no fixed height. Restore the original
ai-chat-height class which uses calc(100dvh - ...) for an absolute
height constraint. Keep overflow-hidden to prevent outer page scroll.
CreditsBadge now sits left of the model dropdown in the controls row
instead of in the PageHeader, so it stays on the same line on small
screens. Added overflow-hidden to the main container so only the
ScrollArea chat pane scrolls, eliminating the extra space below the
input that the user could scroll to reveal.
search_users resolves names to Nostr pubkeys with a two-phase search:
contacts first (fetch kind:3, then kind:0 metadata matching the query),
then NIP-50 relay search as fallback. Returns up to 5 matches with
pubkey, name, display_name, nip05, and about.
The tool call loop now supports multiple rounds (up to 10): after each
tool execution, the follow-up request includes tools so the AI can
chain calls (e.g. search_users -> create_spell). The loop breaks when
the AI responds with plain text instead of another tool call.
Add getCredits() to useShakespeare hook (GET /v1/credits with NIP-98
auth). CreditsBadge component uses TanStack Query to fetch and
auto-refresh the balance every 30s, formatted as USD currency. Shown
in the PageHeader next to the AI Chat title.
The star button now compares sidebar items by decoded event ID instead
of nevent string equality, so it correctly detects spells added via
different paths (three-dot menu vs star button vs paste). When removing,
it uses the matched sidebar entry's actual string.
Also adds toast notifications for add/remove, and replaces the ghost
button hover with a star-fill hover effect using Tailwind group.
neventEncode({ id, author }) omits the kind, so when the sidebar
decodes it and the relay fetch hasn't completed (or fails), the kind
falls back to 1 (text note) showing the wrong icon. Including the
kind in the encoding lets the sidebar resolve the correct icon and
path immediately from the decode without waiting for the fetch.
Fixed in both SpellRunPage (star button) and NoteMoreMenu (three-dot
menu 'Add to sidebar').
Show a star icon in the spell summary card that adds/removes the
spell from the sidebar. Filled primary star when in sidebar, muted
outline when not. Uses the same nostr: URI pattern as NoteMoreMenu.
Messages survive page navigation and browser refresh. The session is
loaded from localStorage on mount and saved on every change. The clear
button removes the stored session. Timestamps are serialized as ISO
strings and NostrEvent objects serialize as plain JSON.
Teach Dork how to translate natural language into spell parameters:
runtime variables ($me, $contacts), relative timestamps, common
event kinds, tag filters vs NIP-50 search, and translation examples.
References NIP-A7 spell spec.
Add a generic nostrEvent field to DisplayMessage so any event published
by a tool can be rendered in the chat. Uses NoteCard with compact mode,
which automatically dispatches to the right content component for the
event kind (SpellContent for kind 777, etc.). Extensible to future
tools that publish other event kinds.
Build the tool execution handler for create_spell: converts structured
parameters into kind:777 tags, generates an ephemeral keypair (like
Shakespeare's NostrPublishEventsTool), signs and publishes the spell
event plus a minimal kind:0 profile to relays. The executor returns
the published NostrEvent on the DisplayMessage for inline rendering.
Also makes executeToolCall async and updates the tool call loop to
await each execution sequentially.
Define the OpenAI function calling schema that lets Dork create
kind:777 spell events from natural language requests. Parameters
map to the spell tag schema: name, kinds, authors, tag_filters,
since/until, limit, search, and relays. Includes examples and
documentation of runtime variables ($me, $contacts) and relative
timestamp syntax in the tool description.
The nevent encoding may not include a kind (e.g. nak v0.18.6 doesn't
encode it). Refactor EventSidebarContent to use the kind returned by
useNostrEventSidebar after fetching the event, so the wand icon and
/spells/run/ path resolve correctly regardless of nevent encoding.
Replace the bookmark-based spell tab system in Feed.tsx with sidebar
integration. Spell events (kind 777) added to the sidebar via nostr:
URIs now render with the wand icon, display the spell name, and
navigate to /spells/run/<nevent>. Active-route highlighting matches
the spell run page by comparing decoded event IDs.
Hatching ceremony: escalating haptics on each crack click (light → medium
→ heavy → success notification on hatch). Egg tap-to-wiggle in feeds and
posts: light impact on each user-initiated tap. Auto-wiggle intervals are
excluded to avoid unwanted vibration.
The popover emoji picker (both quick presets and full picker) was
publishing reactions internally without triggering haptic feedback.
Add impactLight() at the top of publishReaction() so every emoji
selection path gets tactile feedback.
Install @capacitor/haptics and add a centralized haptics utility
(src/lib/haptics.ts) that uses the native taptic engine on iOS/Android
and falls back to navigator.vibrate() on web.
Haptics added to:
- Switch component (covers 36+ toggle switches app-wide)
- PullToRefresh threshold (covers 15+ pages)
- MobileBottomNav tab taps
- ReactionButton (like/unlike, double-click heart)
- RepostMenu (repost/undo repost)
- ZapDialog button press + payment success (NWC and WebLN)
- FollowButton and ProfilePage follow toggle
- ComposeBox (post, voice message, and poll publish success)
- NoteMoreMenu (bookmark, pin, mute)
- VinesFeedPage reaction and repost buttons
- ProfileReactionButton and ExternalReactionButton
- NoteCard share button
- BlobbiRoomShell swipe navigation
Replaces raw navigator.vibrate() calls in GameControls and
SendAnimation with the new cross-platform haptics utility, fixing
haptic feedback on iOS where the Vibration API is not available.
Radix Dialog's DismissableLayer sets pointer-events: none on
document.body when the modal is open. Since the dropdowns are portaled
to document.body, they inherit this and silently swallow all mouse
events. Adding pointer-events-auto restores click delivery.
The autocomplete dropdowns are portaled to document.body to escape
overflow clipping, which places them outside the Radix Dialog DOM tree.
Clicks on them were treated as 'interact outside' the dialog, preventing
mouse selection of emoji and mention suggestions.
Add data-autocomplete-dropdown attribute to both dropdown containers and
check for it in handleInteractOutside to prevent modal dismissal.
Zap receipts embedded via nostr:nevent1 references were falling through
to the generic EmbeddedNoteCard, which rendered the raw JSON content of
the zap request. Add a dedicated EmbeddedZapCard that extracts and
displays the sender, amount, and message using the existing zap utility
functions. Forwards disableHoverCards to prevent nested hover cards.
- Add disableMediaEmbeds prop to NoteContent that suppresses images,
galleries, and video/audio inside embedded quotes while preserving
link preview cards and lightning invoices
- Render inline fallback links for nevent/naddr references when
disableNoteEmbeds is true, instead of returning null and leaving
invisible gaps in quoted text
- Restore tag-based title/description fallback for events with empty
content (articles, custom addressable kinds) so they don't render
blank cards
- Migrate EmbeddedProfileBadgesCard to useProfileUrl for consistent
profile link routing
- Fix stateful global regex bug (IMETA_MEDIA_URL_REGEX) causing every
other URL to be misclassified when used with .test() in loops; add
non-global IMETA_MEDIA_URL_TEST_REGEX for safe .test() calls
- Rewrite EmbeddedNoteCard to render content via NoteContent (same as
NoteCard) with a 260px height cap instead of reimplementing URL
parsing and content truncation
- Pass disableEmbeds to NoteContent inside quotes to prevent recursive
nostr:nevent/note references from spawning nested EmbeddedNote
components
- Add overflow-aware 'Read more' toggle inline with attachment chips;
fade gradient only renders when content actually overflows
- Add BlobbiStateCard rendering for kind 31124 in both EmbeddedNote
and EmbeddedNaddr
- Extract EmbeddedCardShell with shared clickable card wrapper and
author row, deduplicating ~150 lines across EmbeddedNoteCard,
EmbeddedNaddrCard, and EmbeddedBlobbiCard
- Fix ComposeBox media URL detection using the same regex fix
- Fix EmbeddedNaddr profile links to use useProfileUrl instead of
hardcoded npub paths
Video/audio/webxdc URLs were silently stripped from NoteContent's token
stream and rendered by parent components after NoteContent. When a quote
post's nostr: URI appeared at the end of the content, media was placed
after the quote embed instead of before it.
Render all media inline within NoteContent at their original content
position via a new media-embed token type. Remove the now-unused
NoteMedia component and the separate media rendering in NoteCard,
PostDetailPage, and ComposeBox.
Also:
- Append media-embed tokens for imeta-declared media not in content
(gated to text note kinds 1/11/1111 only)
- Sanitize imeta-sourced URLs via sanitizeUrl()
- Skip useAuthor query when no media-embed tokens exist
- Memoize author display name derivation
Each poop cleaned awards 5 XP to the companion's experience tag.
Multiple pickups are debounced into a single Nostr publish (1.5s
after the last cleanup) to avoid excessive relay traffic. Uses
ensureCanonicalBeforeAction for fresh-read safety.
- Wire useAwardDailyXp into BlobbiDashboard so daily mission XP is
actually persisted (was exported but never called)
- Rewrite useAwardDailyXp to use fetchFreshEvent + prev pattern
instead of reading from stale TanStack Query cache
- Remove misleading '+XP' toast from poop cleanup; delegate to parent
via onPoopCleaned callback with honest 'Cleaned up!' message
- Fix room-config.ts comment to accurately describe room tag status
(read on mount, not yet written back on room change)
- Make handleOpenShopFromAction navigate to kitchen room instead of
silently closing the modal
- Reset ItemCarousel index to 0 when items array changes to prevent
out-of-bounds access
- Derive KitchenBar foodEntries from foodItems memo instead of
duplicating getLiveShopItems().filter(food)
- CareBar Treat button: memoize treat item, show its name as label,
handle missing item gracefully
- Fix useItemCooldown: remove module-level side-effect subscription,
use proper useSyncExternalStore subscribe contract
- Sanitize AI tool background_url with sanitizeUrl() to prevent CSS injection
- Replace 'as unknown as' and 'as Partial<Record>' type escapes with proper
ChatCompletionTool, ChatCompletionResponseMessage, and ChatCompletionToolCall
types in useShakespeare
- BlueskyWidget: throw on !res.ok so useQuery retry works; type response
- WikipediaWidget: add explicit isError state instead of masking as 'no article'
- Pass prev (profileEvent) to publishEvent on KIND_BLOBBONAUT_PROFILE mutations
in BlobbiWidget and BlobbiPage to preserve published_at
- Add profileEvent field to EnsureCanonicalResult interface
- useEncryptedSettings: fetch fresh event from relays before mutation instead
of reading from stale TanStack cache (cross-device safety)
- Sanitize imeta URLs at the parse layer in PhotoWidget (parseFirstPhoto)
- Sanitize all URLs from Nostr event tags in musicHelpers (parseMusicTrack,
parseMusicPlaylist): audio URL, artwork, video, playlist artwork
- Fix stale-read-then-write in handleSetAsCompanion (BlobbiWidget + BlobbiPage):
use ensureCanonicalBeforeAction to fetch fresh profile from relays instead of
reading profile.allTags from TanStack Query cache
- Pass prev to publishEvent for KIND_BLOBBI_STATE (addressable kind 31124) in
both BlobbiWidget and BlobbiPage handleRest to preserve published_at
- Fix usePublishStatus: fetch previous kind 30315 event before publishing to
preserve published_at per addressable event convention
For fillHeight widgets, WidgetCard now renders content in a plain
fixed-height div instead of a ScrollArea, so the widget's internal
flex layout can properly fill the container with messages scrolling
above and input pinned at the bottom.
Remove the 'Full chat' link since the widget header already links
to /ai-chat.
Move the 'out exploring' UI into src/blobbi/ui/BlobbiAwayState.tsx with
size presets ('md' for page, 'sm' for widget). Both BlobbiPage and
BlobbiWidget now import from the shared component instead of rendering
the away state inline.
Move the animated Dork face (the <[o_o]> thinking animation) into
src/components/DorkThinking.tsx with a className prop for sizing.
Both AIChatPage and AIChatWidget now import from the shared component.
The widget uses text-[10px] for a compact fit inside the chat bubble.
Add fillHeight property to WidgetDefinition. When true, WidgetCard uses
a fixed height instead of max-height on the ScrollArea, allowing the
widget's internal flex layout to properly fill the container. The AI chat
widget's messages area now scrolls correctly at a fixed height instead
of awkwardly growing with content.
The widget was hardcoding 'shakespeare' as the model name, which is
not a valid model ID. Now fetches available models from the API and
uses the cheapest one as default, matching how AIChatPage works.
Uses useBlobbiCompanionData() to detect if this Blobbi is the active
floating companion (same check as BlobbiPage). When active, hides the
visual and stat wheels, showing a Footprints icon + 'Out exploring
with you' message + gradient 'Bring home' button instead.