heartbeads
v0.6.6
Published
Interactive dependency graph viewer for beads (bd) issues
Maintainers
Readme
heartbeads
Interactive dependency graph viewer for beads (bd) issues.

See your entire project's issues, epics, and dependencies as a live, explorable graph. heartbeads auto-discovers your .beads/ directory and renders everything instantly -- no configuration needed.
Get Started
Run this command inside a repo with a .beads folder
npx heartbeads . Features
Graph Visualization
- 5 graph layouts -- Switch between layout modes via the toolbar:
- Force -- Organic force simulation with collision avoidance and variable link distances
- DAG -- Clean top-down directed acyclic graph with topological layering
- Radial -- Concentric rings by dependency depth. Root nodes (no blockers) sit at center, deeper dependencies on outer rings. Ring spacing scales with node count.
- Cluster -- Groups nodes spatially by project prefix. Each prefix gets its own cluster center arranged in a circle. Cross-project dependencies stretch visibly between clusters.
- Spread -- Like Force but maximally spaced for readability and screenshots. Stronger repulsion, wider link distances.
- Collapse / Expand -- Collapse all epics at once with a single button, or right-click individual epics to collapse/uncollapse them. Shows collapsed task count on each epic node.
- Visual encoding -- Node size = dependency importance (connection count), fill color = configurable via legend (see below), ring color = project prefix (Catppuccin Latte palette). Larger nodes are more connected; epics get a size boost.
- Legend color modes -- Switch node fill color between 5 modes via the bottom-right legend panel:
- Status (default) -- Open (green), In Progress (amber), Blocked (red), Deferred (zinc), Closed (emerald)
- Priority -- P0 Critical (red), P1 High (orange), P2 Medium (blue), P3 Low (zinc), P4 Backlog (zinc-300)
- Owner -- Color by
createdByfield using Catppuccin Latte accent palette (14 colors) - Assignee -- Color by
assigneefield using Catppuccin Latte accent palette - Prefix -- Color by project prefix using Catppuccin Latte accent palette
- Catppuccin Latte palette -- All prefix-colored elements (node rings, cluster circles, tooltip accent bars, prefix fill mode) use the Catppuccin Latte accent palette (14 saturated colors optimized for light backgrounds). Person/prefix mapping is deterministic via FNV-1a hash.
- Dependency arrows -- Solid emerald arrows with flow particles for blocking relationships, dashed zinc lines for parent-child hierarchy. Curved links with configurable curvature.
- Semantic zoom -- When zooming out, individual nodes smoothly fade and are replaced by epic cluster labels at each cluster's centroid. Clusters show the epic title, ID, and member count, surrounded by a dashed circle in the project's prefix color. Cluster visibility can be toggled with the "Clusters" button in the top-left toolbar.
- Spawn & exit animations -- New nodes pop in with an overshoot easing (easeOutBack), removed nodes shrink out, and status changes trigger a ripple animation. New links flash bright emerald on arrival.
- Hover tooltips -- Hover over any node to see a tooltip card with the project prefix, issue ID, title, creation date, blocker list, priority, owner, and assignee. Smart viewport clamping (prefers above cursor, flips below).
- Resizable minimap -- Always-visible minimap (bottom-left) showing all nodes, links, claimed avatars, and the current viewport rectangle. Click to navigate. Drag the top, right, or top-right corner handles to resize (100-500px wide, 80-400px tall).
Live Updates
- SSE streaming -- File watchers detect changes to
issues.jsonl(and all additional repo JSONL files) and push updates via Server-Sent Events. No refresh needed. - Diff/merge pipeline -- Incoming data is diffed against current state (
diffBeadsData) and merged with position preservation (mergeBeadsData) so the graph layout doesn't reset. Animation metadata (_spawnTime,_removeTime,_changedAt) is stamped during merge.
Timeline Replay
- Step-based playback -- Replay the entire history of your project as a step-by-step animation. Each step corresponds to one temporal event (issue creation, dependency addition).
- Play/pause controls -- Play, pause, and scrub through the timeline with a slider.
- Speed toggle -- 1x, 2x, 4x playback speed (2 seconds per event at 1x).
- Uses the same diff/merge pipeline as live updates, so nodes get proper positions and spawn animations during replay.
Search
- Smart search --
Cmd/Ctrl+Fto fuzzy search across all issues by ID, title, project prefix, owner, assignee, or commenter username. Keyboard navigation through results.
Right-Click Context Menu
- Multi-action menu -- Right-click any node to open a context menu with actions:
- Show description -- Opens a full-screen modal with the issue's markdown description
- Add comment -- Opens the comment tooltip for posting
- Claim task -- Posts a claim comment (
@handle) to mark yourself as working on the issue (only shown when authenticated and node is unclaimed) - Unclaim task -- Removes your claim from the node (only shown when you are the claimant)
- Collapse/Uncollapse epic -- Toggle individual epic collapse on right-click (only shown on epic nodes)
Mobile Responsive
- Hamburger menu -- On mobile viewports (<=768px), the nav pills are replaced with a hamburger menu that opens a slide-in drawer from the right with all navigation items.
- Double-tap context menu -- Since right-click doesn't exist on mobile, double-tap a node to open a bottom action sheet with the same actions as the desktop context menu.
- Touch-optimized -- Hover tooltips suppressed on mobile (no flicker), comment tooltips render as bottom sheets, text-selection TTS tooltip disabled, activity overlay compact mode (3 events).
- Bottom drawer -- Node detail slides up as a bottom drawer instead of a sidebar on mobile.
Task Claiming
- Claim tasks -- Right-click a node and select "Claim task" to mark yourself as working on it. Posts a special
@handlecomment via ATProto. - Avatar badges -- Claimed nodes display the claimant's circular avatar at the bottom-right of the node on the graph canvas. Avatars are drawn at constant screen-space size and also appear on the minimap.
- Avatar hover tooltip -- Hover over a claimed node's avatar to see the claimant's profile picture, handle, and when they claimed it (relative time).
- Optimistic UI -- Claims and unclaims update immediately in the UI before the server round-trip completes. Optimistic claims show the avatar instantly; optimistic unclaims suppress it instantly.
- Unclaim -- Remove your claim via the context menu. Deletes the underlying ATProto comment record.
Node Detail Sidebar
- Click-to-inspect -- Click any node to open a detail sidebar with:
- Issue ID, title, type icon, status/priority/prefix badges
- GitHub repository link (auto-detected from git remote, shown as clickable badge + URL)
- Metrics grid: blocks count, dependent count
- Blocker and dependent lists with clickable navigation
- Dates: created, updated, closed (with hour:minute precision)
- Owner attribution
- Full description rendered as Markdown (with GFM support)
- Copy-to-clipboard button for descriptions (copies raw markdown with a header showing project prefix, issue ID, and GitHub repo URL)
- Threaded comment section (see below)
- Description modal -- "View in window" opens a full-screen modal with the rendered description and a copy button. Also accessible via right-click context menu "Show description".
- Mobile drawer -- On small screens, the detail panel slides up as a bottom drawer instead of a sidebar.
ATProto Authentication & Comments
- OAuth 2.0 login -- Sign in with your Bluesky/ATProto account via OAuth 2.0 with PKCE. Avatar and handle shown in the header. Dual mode: public client for dev, confidential (ES256 JWK) for production.
- Comment annotations -- Right-click any node to post a comment via ATProto (
org.impactindexer.review.commentlexicon). Comments stored on the AT Protocol, fetched from the Hypergoat GraphQL indexer. - Threaded replies -- Comments support nested replies (
replyTofield). Root comments sorted newest-first, replies sorted chronologically. Rendered with indentation (ml-4 pl-3 border-l). - Likes -- Heart-toggle likes on comments (
org.impactindexer.review.likelexicon). Rose-colored when liked by the current user. - Delete -- Delete your own comments and likes.
- Comment badges -- Nodes with comments show a red notification badge with the comment count on the graph canvas.
- All Comments panel -- Slide-in sidebar showing all comments across all nodes with threaded replies, clickable node navigation pills, and like/delete actions.
Multi-Repo Support
- Automatic aggregation -- If your
.beads/config.yamllists additional repositories, heartbeads loads all of them into a unified graph. - Per-project colors -- Each project prefix gets a deterministic color from the Catppuccin Latte palette (14 accent colors). Shown as node rings, prefix badges, cluster borders, and tooltip accent bars.
- GitHub repo links -- Auto-detects git remote URLs for each repository (primary + additional). Shown in the node detail card as a clickable GitHub link. Also included in copied description text.
Header / Navbar
- Frosted glass header --
bg-white/95 backdrop-blur-smsticky header (adapts tobg-black/95in dark mode). Inspired by plresearch.org. - Animated heartbeat logo -- An animated ECG/heartbeat trace SVG that continuously pulses — representing the living heartbeat of your project.
- Pill navigation -- Replay and Comments toggle buttons styled as rounded-full pill items. Auth button with rounded avatar dropdown.
- Centered search -- Rounded-full search bar with keyboard shortcut hint and dropdown results panel.
- Mobile hamburger menu -- On small screens, nav pills collapse into a hamburger menu with a slide-in drawer.
Dark Mode
- Toggle -- Click the moon/sun icon in the header to switch between light and dark themes. Placed between the settings gear and sign-in button.
- Persistent preference -- Your theme choice is saved to localStorage (
heartbeads-themekey) and persists across sessions and page reloads. - System preference -- On first visit (no saved preference), heartbeads automatically follows your operating system's light/dark setting via
prefers-color-scheme. - Full coverage -- The entire UI adapts: canvas background (pure black), graph links, node rendering, all sidebar panels, overlays, modals, tooltips, the search bar, header, leaderboard, and the
/api/docspage. - Tailwind dark: strategy -- Uses Tailwind's
darkMode: "class"strategy. TheuseThemehook inhooks/useTheme.tsmanages thedarkclass on<html>and syncs to localStorage.
Info Panel & Legend
- Bottom-right info panel -- Shows issue count, dependency count, project count, color mode selector (Status / Priority / Owner / Assignee / Prefix), dynamic legend dots for the active mode, and visual encoding hints. Hidden during timeline replay. Legend shows only items present in visible nodes for owner/assignee/prefix modes.
Quick Start
cd ~/my-project
npx heartbeads@latestheartbeads walks up from your current directory to find .beads/, just like git finds .git/.
With an explicit path
npx heartbeads@latest --beads-dir ~/projects/my-project/.beadsMulti-repo aggregation
If your .beads/config.yaml lists additional repositories, heartbeads automatically loads all of them:
# .beads/config.yaml
repos:
additional:
- ../backend
- ../frontend
- ../shared-libCLI Reference
heartbeads [options]
Options:
--port <number> Port to serve on (default: 3000)
--beads-dir <path> Explicit .beads/ directory path
--password <string> Require a password to access the dashboard and API
--dev Run in development mode (hot reload)
--help, -h Show this help message
Environment:
BEADS_DIR Override .beads/ discovery (same as --beads-dir)
HEARTBEADS_PASSWORD Require a password (same as --password)
Examples:
npx heartbeads@latest # Auto-discover from cwd
npx heartbeads@latest --port 4000 # Custom port
npx heartbeads@latest --beads-dir ~/projects/hub/.beads # Explicit path
npx heartbeads@latest --password secret # Password-protected
BEADS_DIR=../.beads npx heartbeads@latest --dev # Dev mode with env varPublic API
Heartbeads exposes a read-only REST API for AI agents, CI/CD bots, and integrations. All endpoints return JSON with CORS headers (Access-Control-Allow-Origin: *).
When running with --password, API requests require authentication via one of:
- Bearer token:
curl -H "Authorization: Bearer <password>" ... - Query param:
curl "...?token=<gate-token>"(HMAC token from/api/auth)
Without --password, all endpoints are open (no auth required).
GET /api/v1/graph -- Full project snapshot
Returns the entire beads graph with issues, dependencies, comments, claims, and recent activity in a single response.
# Full snapshot (issues + comments + activity)
curl http://localhost:3000/api/v1/graph
# Only open issues, skip comments for speed
curl "http://localhost:3000/api/v1/graph?status=open&include="
# Open + in_progress with activity feed capped at 20
curl "http://localhost:3000/api/v1/graph?status=open,in_progress&include=comments,activity&limit=20"
# Filter by repo prefix
curl "http://localhost:3000/api/v1/graph?prefix=beads-map"| Param | Description | Example |
|-------|-------------|---------|
| status | Filter by status (comma-separated) | open,in_progress |
| priority | Filter by priority (comma-separated) | 0,1 |
| prefix | Filter by repo prefix | beads-map |
| include | Opt-in expensive fields (default: all) | comments,activity |
| limit | Activity feed cap (default 50, max 200) | 20 |
Response includes: project metadata, issues[] with comments and claims, dependencies[], stats, activity[], and _meta.
GET /api/v1/issues/:id -- Single issue detail
Returns one issue with full description, threaded comments, and enriched blockers/dependents (with title + status).
curl http://localhost:3000/api/v1/issues/beads-map-abc
# Returns 404 with hint if not found
curl http://localhost:3000/api/v1/issues/nonexistentGET /api/v1/ready -- Actionable issues
Returns issues ready to work on: open or in_progress with no unresolved blockers. Sorted by priority (critical first), then by age (oldest first).
# All actionable issues
curl http://localhost:3000/api/v1/ready
# Only unclaimed bugs
curl "http://localhost:3000/api/v1/ready?unclaimed=true&type=bug"
# Limit to top 10
curl "http://localhost:3000/api/v1/ready?limit=10"| Param | Description | Example |
|-------|-------------|---------|
| unclaimed | Only unclaimed issues | true |
| type | Filter by issue type (comma-separated) | bug,feature |
| assignee | Filter by assignee | alice |
| prefix | Filter by repo prefix | beads-map |
| limit | Max issues returned | 20 |
Response includes: issues[] with comments and claims, stats summary (total_ready, unclaimed, by_priority, by_type), and _meta.
Development
git clone https://github.com/GainForest/heartbeads.git
cd heartbeads
pnpm install
# Run in dev mode against a beads project
BEADS_DIR=~/path/to/.beads pnpm dev
# Build for production
pnpm buildQuality gate
pnpm build must pass with zero errors before committing. There are no tests yet -- the build is the sole gate.
Architecture
heartbeads/
├── middleware.ts # Password gate: auth check on every request (Edge Runtime)
├── app/
│ ├── page.tsx # Main page: SSE wiring, merge logic, search, layout, comments, claims
│ ├── layout.tsx # Root layout, wraps in AuthProvider
│ ├── not-found.tsx # Custom 404 page with heartbeat logo
│ ├── login/page.tsx # Password login page (shown when --password flag is set)
│ ├── globals.css # Timeline slider styles, markdown prose, scrollbar
│ └── api/
│ ├── beads/
│ │ ├── route.ts # GET /api/beads -- one-shot full data load
│ │ └── stream/route.ts # GET /api/beads/stream -- SSE live updates
│ ├── auth/route.ts # GET/POST/DELETE /api/auth -- dashboard password gate
│ ├── v1/
│ │ ├── graph/route.ts # GET /api/v1/graph -- full project snapshot for AI agents
│ │ ├── issues/[id]/ # GET /api/v1/issues/:id -- single issue detail
│ │ └── ready/route.ts # GET /api/v1/ready -- actionable issues
│ ├── config/route.ts # GET /api/config -- project name, repo count, repo URLs
│ ├── login/route.ts # POST /api/login -- initiate ATProto OAuth
│ ├── logout/route.ts # POST /api/logout -- clear session
│ ├── status/route.ts # GET /api/status -- current auth state
│ ├── records/route.ts # POST/PUT/DELETE /api/records -- ATProto record CRUD
│ └── oauth/
│ ├── callback/route.ts # OAuth callback handler
│ ├── client-metadata.json/ # OAuth client metadata
│ └── jwks.json/route.ts # JSON Web Key Set
├── components/
│ ├── AllCommentsPanel.tsx # Slide-in panel: all comments across nodes, threaded
│ ├── AuthButton.tsx # Sign-in modal + avatar dropdown (rounded-full pill style)
│ ├── BeadsGraph.tsx # Force graph: paintNode/paintLink, minimap, semantic zoom, avatars
│ ├── BeadsLogo.tsx # Animated heartbeat (ECG) SVG logo
│ ├── BeadTooltip.tsx # Hover tooltip: prefix, ID, title, date, blockers, priority, owner
│ ├── CommentTooltip.tsx # Floating right-click comment tooltip (bottom sheet on mobile)
│ ├── ContextMenu.tsx # Right-click context menu: description, comment, claim/unclaim
│ ├── DescriptionModal.tsx # Full-screen markdown description modal with copy button
│ ├── HeartIcon.tsx # Shared heart SVG (outline/filled)
│ ├── MobileActionSheet.tsx # Mobile bottom action sheet (double-tap context menu)
│ ├── NodeDetail.tsx # Sidebar detail: metadata, deps, comments, repo link
│ ├── TimelineBar.tsx # Timeline replay: play/pause, scrubber, speed toggle
│ ├── GraphStats.tsx # Issue count statistics widget (unused, inlined in BeadsGraph)
│ └── StatusLegend.tsx # Color legend for statuses (unused, inlined in BeadsGraph)
├── hooks/
│ ├── useBeadsComments.ts # React hook wrapper for lib/comments.ts
│ ├── useIsMobile.ts # Mobile viewport detection hook (<=768px)
│ └── useTheme.ts # Dark mode toggle hook (localStorage + prefers-color-scheme)
├── lib/
│ ├── comments.ts # Shared comment fetching, threading, claim detection
│ ├── gate.ts # Password gate: HMAC token generation/validation
│ ├── api-helpers.ts # CORS headers, JSON response builders for /api/v1/*
│ ├── auth.tsx # AuthProvider context + useAuth hook
│ ├── auth/client.ts # OAuth client factory (public/confidential mode)
│ ├── agent.ts # Authenticated ATProto agent from session
│ ├── session.ts # iron-session encrypted cookie setup
│ ├── env.ts # Environment variable validation
│ ├── discover.ts # .beads/ auto-discovery + git remote URL detection
│ ├── parse-beads.ts # JSONL parser, multi-repo hydration, graph builder
│ ├── types.ts # GraphNode, GraphLink, ColorMode, Catppuccin palette, color helpers
│ ├── diff-beads.ts # Diff engine for detecting added/removed/changed nodes/links
│ ├── watch-beads.ts # fs.watch wrapper with debounce for JSONL file changes
│ ├── settings.ts # ElevenLabs TTS settings (localStorage)
│ ├── tts.ts # Text-to-speech with ElevenLabs API, alignment cache
│ ├── activity.ts # Activity feed event types and builders
│ ├── timeline.ts # buildTimelineEvents + filterDataAtTime for replay
│ └── utils.ts # formatRelativeTime, buildDescriptionCopyText, shared utilities
├── bin/
│ └── heartbeads.mjs # CLI entry point
├── scripts/
│ └── generate-jwk.js # ES256 JWK key generation for OAuth
└── public/
└── image.png # Screenshot for READMEData flow
- Discovery --
lib/discover.tswalks up fromcwd(or readsBEADS_DIR) to find.beads/. Also detects git remote URLs for all repos. - Parsing --
lib/parse-beads.tsreadsissues.jsonlfrom the primary repo and any additional repos inconfig.yaml, deduplicates, extracts dependencies, and builds a graph structure with computed fields (blocker/dependent counts, prefix colors). - Rendering --
components/BeadsGraph.tsxusesreact-force-graph-2dwith fully custom canvas rendering (paintNode,paintLink). All transient state (hover, selection, comments, claimed avatars) stored in refs to avoid re-rendering the force simulation. - Live updates --
app/api/beads/stream/route.tsopens an SSE connection, watches all JSONL files, and pushes full data on each change. The client diffs and merges with position preservation. - Timeline --
lib/timeline.tsextracts sorted temporal events from the data. During replay,filterDataAtTime()produces a time-slice that flows through the same diff/merge pipeline as live updates. - Comments --
lib/comments.tsfetches from the Hypergoat GraphQL indexer, resolves Bluesky profiles, and builds threaded trees. Used by both the React hook (hooks/useBeadsComments.ts) and the public API routes (/api/v1/*). - Public API --
/api/v1/graph,/api/v1/issues/:id, and/api/v1/readycombine beads data + comments + activity into enriched JSON for AI agents. No auth required, CORS enabled. - Claims -- A claim is a comment with text
@handle(starts with@, no spaces). TheclaimedNodeAvatarsuseMemo inpage.tsxscans all comments to build aMap<nodeId, claimInfo>. Avatars are drawn on canvas nodes and the minimap bypaintNodeviaclaimedNodeAvatarsRef.
Key design decisions
- paintNode/paintLink use refs, not props -- Callbacks have
[]dependencies and read fromselectedNodeRef,hoveredNodeRef,claimedNodeAvatarsRef, etc. This prevents re-creating the ForceGraph component on every interaction. - Position preservation is critical --
react-force-graph-2dmutates node objects in-place (x,y,vx,vy). The merge logic copies these from old nodes to new nodes to prevent layout resets. - Animation metadata convention -- Fields prefixed with
_on nodes/links (_spawnTime,_removeTime,_changedAt,_prevStatus) are transient, set bymergeBeadsData(), consumed bypaintNode/paintLink, and garbage-collected after 600ms. - Bootstrap trick -- The graph starts in DAG mode for 15ms to spread nodes into good positions, then auto-switches to Force mode. This gives the organic layout a clean starting arrangement.
- Link source/target normalization --
filterDataAtTime()must spread new link objects with stringsource/target(not the original mutated object refs from d3-force). Without this, links draw to wrong positions during timeline replay. - Optimistic claim/unclaim -- Two state variables:
optimisticClaims(Map) for immediate avatar display,optimisticUnclaims(Set) for immediate avatar suppression. Both reconciled with comment-derived data inclaimedNodeAvatarsuseMemo. - Avatar image cache -- Module-level
avatarImageCacheinBeadsGraph.tsx. NocrossOriginattribute on Image elements (Bluesky CDN CORS issue).getAvatarImage()returns cachedHTMLImageElementor starts loading and returns null. - Portal for modals --
DescriptionModalusescreatePortal(jsx, document.body)withz-[100]to escape any parent stacking contexts.
Tech Stack
- Next.js 14 (App Router)
- react-force-graph-2d for canvas-based graph rendering
- d3-force (forceCollide, forceRadial, forceX, forceY for layout modes)
- Catppuccin Latte accent palette for prefix/person coloring (14 colors)
- Tailwind CSS for styling
- TypeScript throughout
- @atproto/oauth-client-node + @atproto/api for ATProto authentication and record CRUD
- iron-session for encrypted cookie session management
- react-markdown + remark-gfm for description rendering
- Node.js
fs.watchfor file change detection - Server-Sent Events for real-time streaming
License
MIT
