palette
OKLCH, theme-aware via color-mix()
Sparrow is a hosted, keyboard-first client for PointCast — El Segundo's ongoing broadcast of blocks across 10 channels. It pairs with Magpie, the local-first publisher Mike uses to push clips into the PointCast graph and its syndication pool (Mastodon · Farcaster · bitchat · Zora · Objkt · more).
Conic-gradient dial over the last 24 broadcasts, sliced by channel, with an ember needle that sweeps once every twelve seconds.
Every channel — Front Door, Court, Spinning, Good Feels, Garden, El Segundo, Faucet, Visit, Battler — as a tinted card with a live count.
A dozen latest blocks as channel-tinted cards with pixel stamp, ember №, signal-strength bars, dek, and mood chip. Click any receipt; it morphs into the reader.
Full-page reading surface in Sparrow chrome — Gloock title, italic Didone dek, readable body, channel aside with save + companions + external + canonical links.
Local-first via browser localStorage. Press S on any block to save; list lives at /sparrow/saved. Sparrow does not phone home.
Fuzzy search across routes, channels, and the 60 most recent blocks. Same shortcut macOS-wide — Ctrl+K on other platforms.
Each reel derives its own mood taxonomy from actual block data — chips only surface when they have results to show.
IntersectionObserver marks whichever receipt is centered in the viewport as "on-air" — ring highlight, and S targets that block by default.
Two OKLCH themes: blue-hour (default dark, twilight indigo + saffron ember) and dawn (light, bone paper + rust). Preference persists per browser.
Sparrow-branded Atom 1.0 at /sparrow/feed.xml and an agent-legible manifest at /sparrow.json describing every UI primitive, keyboard shortcut, and data source.
A 2-pixel tuning bar at the top rail advances as you scroll — ember → lilac → moss gradient, powered by CSS scroll-timeline. Respects prefers-reduced-motion.
Reel card → reader morph via Astro ClientRouter + transition:name. Back works the same way. No JS framework bundled.
Scoped service worker at /sparrow/sw.js precaches the shell + 9 channels + manifest + feed, then stale-while-revalidates. Block readers runtime-cache up to 48 entries. Offline fallback carries Sparrow chrome.
Installable via the browser install prompt. App shortcuts go to Front Door, your saved list, and this page. Runs in its own window, cached for cold reads.
Block readers log visits to localStorage. The reel softens titles you've already read and adds a small "read" chip — the saved star always wins if both apply.
The HUD grows an oxblood "offline" pill when navigator.onLine flips. Auto-clears when the network comes back.
A one-off overview at /sparrow/deck in 1980s Bell Labs / Xerox PARC styling — cream paper, oxblood stamp, EB Garamond + Courier Prime, numbered sections, ASCII system diagram, figure plates, and a prompt appendix for AI image generation.
Press ? anywhere to pop a grouped keyboard reference (Discovery / Reading / Display / Reader extras). Read-only — the palette is for doing, the cheatsheet is for remembering.
A thin ember bar below the top rail on /sparrow/b/<id> fills as you scroll through the article body, driven by CSS view-timeline. Degrades silently on older browsers.
Select any text in the reader body and an ember ✎ chip appears. Click it and the clipboard gets a formatted quote block: "…" — title · № · pointcast.xyz/b/<id>.
Hovering a receipt injects <link rel="prefetch"> for its reader; the reader itself prefetches prev + next during requestIdleCallback. Combined with the SW cache-first strategy, J/K paging is instant.
The first paragraph of every block reader opens with a Didone drop cap in ember; body paragraphs use text-wrap: pretty and hanging-punctuation for cleaner rags.
0 scrolls smoothly to the top, $ to the bottom. Works on every Sparrow surface — useful mid-article and at the bottom of long channel reels.
Sparrow.app — a macOS menu-bar ✦ that polls /sparrow/api/latest.json, pulses with a "new count" badge when blocks arrive, and fires notifications. Swift 5.9, AppKit + URLSession + UserNotifications, no external deps.
A summary-only JSON feed at /sparrow/api/latest.json — snake_case, 24-block window, 2-min cache. Shaped for lightweight clients: the native app, shell scripts, a Raspberry Pi blinkt, anything.
Three-chip reaction toolbar on every block reader — 🔥 lit, 🌿 evergreen, 💜 rare. Picks live in localStorage:sparrow:reactions for v0.7 (local-only). v0.8 fans them out as Nostr kind-7 events so counts aggregate across devices.
If a NIP-07 browser extension is installed (Alby, nos2x, Flamingo), a "connect signer" pill appears next to the reaction toolbar. Once connected, each pick signs a kind-7 event r-tagged to the block's canonical URL and broadcasts to a relay pool — default damus / primal / nos.lol, configurable via sparrow:nostr-relays.
Every reader opens a kind-7 subscription filtered by {kinds:[7], #r:[canonical-block-url]} against the relay pool. Counts paint on each chip as events arrive; dedup by event id; works without a signer since reading is public. Sockets close on beforeunload.
Toggling off a reaction you emitted fires a kind-5 delete event pointing at the original kind-7 event id (kept in sparrow:nostr-emitted). Optimistic: local state clears immediately, the retraction relays over best-effort.
One REQ per relay with every visible block URL in #r paints a compact "🔥 3 · 🌿 1" row into each receipt's footer. Shared dedupe state with the reader keeps the two views in sync.
Collapsible panel on every block reader. Subject + body → pc-ping-v1 POST to pointcast.xyz/api/ping with the parent block as sourceUrl, channel inherited, expand=true so cc stages it as a new block on the next tick. Magpie-routed multi-destination reply lands in v0.12.
The composer probes 127.0.0.1:38473/health on load. When Magpie is alive, a "magpie · connected · N ready" pill lights up moss and the row below paints a readiness chip for every destination Magpie can reach (PointCast, Mastodon, Farcaster, bitchat, Bluesky, Twitter, LinkedIn, Instagram, Zora, Objkt, OpenSea).
Destination chips are checkboxes; PointCast is locked on. Tick extras (Mastodon, Farcaster, bitchat, etc.) and submit POSTs to Magpie /compose with the full destinations[] list. Any /compose failure falls back gracefully to direct /api/ping so the reply always lands in PointCast. Magpie native endpoint is spec'd in the manifest.
The Swift counterpart to v0.13 landed in Magpie v0.14 — AppState.handleComposeRequest builds an ephemeral ClipItem (not persisted) + PublishDraft and fans out via PublisherRegistry, returning the same result envelope as /broadcast so Sparrow's parser is unchanged. Upgrade Magpie; next time you tick extras, the first hop succeeds.
When Magpie's peer-node is alive, sparrow:saved mirrors to it via POST /reader-state on every toggle (600ms debounce). Fresh tabs pull GET /reader-state.json on load and apply newest-wins by updated_at. Your saved receipts stay in sync across Sparrow tabs on the same machine — no sign-in, no server-of-record, no telemetry.
v0.16 extends the mirror beyond saved. writeVisited and writeReactions debounce into the same scheduleReaderMirror(); pulls apply newest-wins to visited IDs and the reactions map, then repaint .is-visited + rehydrate reaction chips. One POST carries all three keys; each key has its own updated_at so a stale tab can't clobber a live one.
Toolbar on /sparrow/saved. Export bundles the Sparrow Atom + nine channel RSS feeds + one outline per saved block into a sparrow-saved-<date>.opml download. Import DOMParses any OPML file and unions /b/<id> matches into sparrow:saved — additive, never destructive. Entirely client-side; nothing uploaded off-device.
Shared resolveMagpieOrigin() probes localStorage override → magpie.local:38473 → 127.0.0.1:38473. First /health responder wins and caches on window.__sparrow; all Magpie-bound fetches (mirror, bridge pill, composer) share the resolution. Once Magpie ships the Bonjour advertisement in v0.19, .local just starts working.
Magpie v0.19 attaches NWListener.service — type _magpie._tcp, name "Magpie", port 38473, TXT record carrying version + /health + schema id + composer + mirror endpoints. macOS mDNS responder makes magpie.local resolve to the host; Sparrow's v0.18 ladder picks it up on the second rung. dns-sd -B _magpie._tcp shows the active instance.
v0.20 ships a second peer-node inside the menu-bar companion: Sources/SparrowApp/SparrowServer.swift runs a loopback NWListener on 38474 exposing /health + /reader-state GET/POST with byte-identical newest-wins merge logic to Magpie. Advertises _sparrow._tcp via Bonjour. The web resolver ladder grows to 5 rungs (user override → magpie.local → 127.0.0.1:38473 → sparrow.local:38474 → 127.0.0.1:38474) and tags the resolved peer kind so the bridge pill can label it. Reader-state sync works with just Sparrow.app running — Magpie is no longer a mirror prerequisite.
v0.21 closes the multi-machine loop. With a NIP-07 signer that supports NIP-44 (nos2x, recent Alby), the HUD gains a "sync · on/off" pill. Turning it on publishes an encrypted kind-30078 addressable event (d-tag sparrow-reader-state-v1) carrying the same {saved, visited, reactions} blob — self-encrypted to your own npub so only your signer can decrypt. On load, Sparrow REQs the newest event across the relay pool and merges newest-wins per key. Runs side-by-side with the LAN peer-node mirror; both fire on the same debounce. 4s floor between relay pushes to keep bandwidth polite.
v0.22 adds a federated reading-list surface. Flip "publish my saved list publicly" and Sparrow emits a separate unencrypted kind-30078 event (d-tag sparrow-public-saved-v1, scope-limited to just the saved block ids + a small client profile — no visited state, no reactions). Add friends by pasting a hex pubkey; Sparrow REQs each friend's latest public list from the relay pool and renders their saved blocks with titles + channel chips resolved via a server-shipped lookup. Two distinct consents: cross-device sync and public list — turn either on without the other.
v0.23 smooths the federation rough edges. The /sparrow/friends form now accepts npub1… pasted from any Nostr client — a self-contained NIP-19 bech32 codec (inlined, no dependency) normalizes either input to hex. The HUD self-pubkey display also renders as short-npub. On load Sparrow REQs kind-0 metadata events for every friend, cached for 24h in sparrow:profiles; display_name or name auto-populates as an alias when the local one is empty. A 🛰 glyph appears next to names sourced from the relay so local aliases and federated names stay legible at a glance.
v0.24 finishes the friends surface. Profile pictures render as 28px circles on the friends list (18px inline on feed cards) with lazy loading, no referrer, and a quiet onerror-hide so bad URLs don't leave a torn-page glyph. NIP-05 verification round-trips the claimed `<user>@<domain>` against the domain's /.well-known/nostr.json and confirms it points back at the same pubkey — ✓ moss when verified, ! oxblood on mismatch. Results cache for 7 days. Press F anywhere in Sparrow to jump to /sparrow/friends; the palette + cheatsheet learn the same shortcut.
v0.25 lands a compact friends lane between the rosette and the 12-latest reel on /sparrow. Each row: avatar · name · "saved" · № id · title · channel chip — sorted by each friend's most-recent event (their freshest save), capped at 6 rows so the dashboard doesn't drift toward a wall. Shares the kind-30078 consumer flow with /sparrow/friends and reads the same sparrow:profiles cache so hydration stays one-pass; server inlines a block lookup so titles resolve without per-block fetches. A dismiss × button sets sparrow:friends-lane-hidden so the lane collapses until you turn it back on from /sparrow/friends. Empty state nudges you to add friends; lane hides entirely if nobody's followed yet.
v0.26 turns friends from a panel into a presence. SparrowLayout opens a persistent streaming kind-30078 REQ with since=bootTime, so only events published while the tab is open trigger a bottom-right "just saved" toast — avatar · name · № id · title. Up to 3 visible, 7s TTL; click opens the block, × dismisses. An opt-in Web Audio chime (two-note fifth, no asset) rings alongside the toast when sparrow:friends-chime-enabled is on. Per-friend mute lands as a new `muted` field on sparrow:friends entries — muted friends drop out of the feed, dashboard lane, subscription, and toasts without losing their alias. Global opt-out via sparrow:friends-motion-disabled for quiet sessions.
v0.27 adds a dedicated timeline over every public saved-list event from your followed (non-muted) npubs. Dual subscription: a bounded initial pull (limit 50) for history and a second `since: bootTime` stream kept open until beforeunload. New events splice at the top with a moss pulse animation and a "new" pill that fades after 12 seconds — the page earns its "history, live" feel without a full repaint. Each card carries avatar · name · "saved N blocks" · relative timestamp · 3 preview receipts · "+N more". Title resolution uses a server-shipped block lookup; profiles + avatars read the same sparrow:profiles cache the rest of the federation surface uses.
v0.28 summarizes. Three panels over the same kind-30078 corpus the activity timeline streams. Most co-saved surfaces blocks saved by two or more followed signers (count-sorted, top 12, with inline saver chips) — overlap is the signal. Recent adds lists friends who published a fresh saved list in the last 7 days, newest first. Channel distribution bars every saved block across the 9 channels, proportional, each row linking to its /sparrow/ch/<slug>. Client-side aggregation only; no new server endpoint, no background job.
v0.29 sharpens the federation surface. In /sparrow/signals each co-saved row now carries a ⭐ first-picker chip — the friend with the earliest created_at among current savers. A new ⤓ export JSON button dumps the whole recap as sparrow-signals-<date>.json (schema sparrow-signals-v1 · friends, relays, newest events, top co-saved with savers + picker attribution, notes documenting the caveats). Each /sparrow/ch/<slug> gains its own scoped friends panel above the main reel — count-sorted saves that intersect this channel, top 6, with first-picker attribution. Hides when no friends publish into the channel; opt-out via localStorage.
v0.30 adds co-presence and a scaffold for email digests. Opt in via /sparrow/friends and Sparrow publishes an ephemeral kind-20078 event tagged t:sparrow-presence every 60s while the tab is foregrounded — relays in the 20000-29999 NIP-16 range never persist these, so liveness is moot the instant it's stale. A streaming subscriber paints a fixed bottom-left "✦ here now" strip with avatars of friends seen in the last 90s. Separately, /sparrow/signals gets a weekly-digest opt-in panel — worker not live yet, but subscription intent is captured locally with a documented shape so the sidecar can pick up the contract when infra ships. Worker never holds secret material — signed-payload pointers only.
v0.31 closes the on-ramp gap. /sparrow/friends now opens with a live checklist — signer connected, public list on, at least one follow, ambient on — with ✓/○ ticks that repaint across tabs. Below it, an invite card builds a shareable link (/sparrow/friends?follow=<npub>) with one-click copy. The same URL, when opened on someone else's Sparrow, pre-fills the add-form and pulses the submit button so following is a single click. A starter seed card offers suggested pubkeys to one-click-follow so new users never land on an empty federation. OPML round-trip exports sparrow:friends as <outline type="nostr"> elements for portability between clients.
v0.32 backs the federation layer with persistence. POST /api/sparrow/digest-subscribe is a real Cloudflare Pages Function now — validates sparrow-digest-subscription-v1, returns 202 Accept, stores in SPARROW_DIGEST_KV when bound (still acks without, so the v0.30 client stops retrying). GET /sparrow/federation.json is a new editorial endpoint carrying the starter seeds (hex + alias + note) so curation is a data edit, not a code change; friends.astro fetches on boot with a tiny internal fallback. The reader-state mirror learns two new keys — friends + profiles — so a user who follows someone on laptop sees them on desktop; window.__sparrow.scheduleReaderMirror is exposed so route-local pages can trigger a push after mutating those keys.
v0.33 extends the federation layer into the native menu-bar companion. NostrRelayClient.swift (URLSessionWebSocketTask, zero dependencies) + FriendsService.swift (reads followed pubkeys from the SparrowServer reader-state mirror, subscribes to kind-20078 presence + kind-30078 saved events against the default relay pool) surface a live "✦ N friends here" menu item with aliases. The digest cron scaffold lands in workers/sparrow-digest/ — wrangler.toml (weekly Mon 08:00 UTC, MailChannels transport), src/index.ts (listAllSubscriptions + frequency gate + placeholder email + /dry-run), README documenting deploy + DNS lockdown + what ships in v0.34. DELETE /api/sparrow/digest-subscribe adds two unsubscribe modes: x-unsub-intent: local-clear (web-initiated, no token) and x-unsub-token (email-footer, 501 until v0.34).
v0.34 turns the placeholder unsubscribe path into a real signed URL. workers/sparrow-digest/src/signing.ts is a Web Crypto HMAC-SHA256 codec — token shape is <email>.<expires_at>.<hex-hmac>, 30-day TTL, constant-time compare; buildUnsubUrl helper composes the email-footer link. DELETE /api/sparrow/digest-subscribe now verifies against SPARROW_DIGEST_SIGNING_KEY (400 on malformed/bad-hmac, 410 on expired, 200 on verify + KV delete). Token arrives via either x-unsub-token header OR ?unsub_token query param so email-footer clicks work from any browser. The signing key must be bound to both the Pages Function AND the cron worker with the same value; rotating invalidates outstanding tokens, which is the right tradeoff for a 30-day TTL.
v0.35 makes the cron worker actually do work. workers/sparrow-digest/src/nostr.ts is a TypeScript port of NostrRelayClient.swift (Web WebSocket + JSON frames, no deps); collectFromRelay + collectAcrossRelays + newestPerAuthorByDTag helpers with a 6s relay timeout and 500-event safety cap. workers/sparrow-digest/src/send.ts extracts MailChannels transport with retry-with-jitter — 3 attempts max, 800ms / 1600ms / 3200ms exponential-jitter backoff, 5xx/429/408 retriable, 401/403/422 fail-fast without touching last_sent_at so ops sees them and the next cron tick retries. renderDigestEmail now fetches each subscriber's kind-3 contact list + their friends' kind-30078 public saved events and surfaces "N of M followed signers published public saved lists · K total saved-block references" in the email body (text + HTML mirrors). Subscribers without npub get the existing short prompt.
v0.36 hardens the cron path with a dead-letter bucket. workers/sparrow-digest/src/deadletter.ts carries a KV-backed failure counter (`fail:<email>`, 60-day TTL) + dead-letter record (`dl:<email>`, 1-year TTL). Threshold: 3 consecutive retriable failures OR any non-retriable failure (401/403/422) → dead-letter immediately. scheduled() skips dead-lettered subs every tick and clears the counter on success. Two new fetch routes, bearer-token gated via SPARROW_OPS_TOKEN: GET /ops/dead-letter lists every dead-letter record newest-first; POST /ops/release?email=<addr> clears both dl: and fail: entries (sub row untouched) so the next cron tick retries cleanly. Missing token → 503 ops-not-configured (intentional so a forgotten binding is obvious).
v0.37 ships items 1, 2, and 4 of the v0.36 HUD shortlist (docs/plans/2026-04-28-sparrow-hud.md). (1) The wordmark sub-line drops the hard-coded "pointcast reader · v0.x" label and now reads {lane} · v{SPARROW_VERSION} — lane comes from src/lib/sparrow-lane.ts which maps Astro.url.pathname to one of six lanes (reading, federation, session, artifact, capability, meta), each with its own tint via [data-sp-lane="…"] selectors. (2) A moss federation-pulse dot sits inside the wordmark, dim by default; it pulses for 800ms whenever the v0.30 ambient watcher's onEvent receives a friend's kind-20078 presence event (the watcher dispatches sparrow:fed-pulse, the dot listens — cheap to fire even on surfaces where no one's listening). (3) A density toggle button persists "comfortable" / "compact" under sparrow:density and writes <html data-density>; surfaces opt in via [data-density="compact"] selectors. Layout-level baseline tightens HUD chrome for free. SW_VERSION → sparrow-v0.37.0; SPARROW_VERSION exported from sparrow-lane so the sub-line and the lane lib never drift.
v0.38 adds the federation channel to TV mode. /sparrow/tv/friends is the same ten-foot ambient chrome as /sparrow/tv (slide rotation, channel-tinted radial wash, surface-detect, here-strip, vmin-scaled type), but the slide pool is hydrated from friends' kind-30078 sparrow-public-saved-v1 events instead of the broadcast itself. Pipeline: server pre-renders 12 fallback slides + a 60-block lookup map (id → title/dek/channel/type/mood/timestamp); client opens a WS REQ across the relay pool with { kinds:[30078], authors:<friends>, '#d':['sparrow-public-saved-v1'] }, collects newest-per-author, flattens saved IDs into a time-sorted slide list with friend-attribution chips ("✦ saved by Alice"), and replaces the fallback in place. Federation strip below the slides shows avatars of follows currently feeding the rotation + signer count. Empty state when no friends configured: a calm CTA pointing at /sparrow/friends. Privacy default: avatars hidden until sparrow:tv-private-ok="1" — count-only mode shows "N signers" without revealing who.
OKLCH, theme-aware via color-mix()
one OKLCH token per channel, computed from pointcast brand hexes
displayGloock
uiInter Tight
monoDeparture Mono
all animations respect prefers-reduced-motion