# heartbeads

**Interactive dependency graph viewer for [beads](https://github.com/GainForest/beads) (bd) issues.**

![heartbeads showing a multi-project dependency graph](public/image.png)

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

```bash
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](https://github.com/catppuccin/palette) 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 `createdBy` field using Catppuccin Latte accent palette (14 colors)
  - **Assignee** -- Color by `assignee` field 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](https://github.com/catppuccin/palette) 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+F` to 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 `@handle` comment 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.comment` lexicon). Comments stored on the AT Protocol, fetched from the Hypergoat GraphQL indexer.
- **Threaded replies** -- Comments support nested replies (`replyTo` field). 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.like` lexicon). 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.yaml` lists 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-sm` sticky header (adapts to `bg-black/95` in dark mode). Inspired by [plresearch.org](https://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-theme` key) 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/docs` page.
- **Tailwind dark: strategy** -- Uses Tailwind's `darkMode: "class"` strategy. The `useTheme` hook in `hooks/useTheme.ts` manages the `dark` class 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

```bash
cd ~/my-project
npx heartbeads@latest
```

heartbeads walks up from your current directory to find `.beads/`, just like `git` finds `.git/`.

### With an explicit path

```bash
npx heartbeads@latest --beads-dir ~/projects/my-project/.beads
```

### Multi-repo aggregation

If your `.beads/config.yaml` lists additional repositories, heartbeads automatically loads all of them:

```yaml
# .beads/config.yaml
repos:
  additional:
    - ../backend
    - ../frontend
    - ../shared-lib
```

---

## CLI 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 var
```

---

## Public 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.

```bash
# 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).

```bash
curl http://localhost:3000/api/v1/issues/beads-map-abc

# Returns 404 with hint if not found
curl http://localhost:3000/api/v1/issues/nonexistent
```

### `GET /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).

```bash
# 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

```bash
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 build
```

### Quality 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 README
```

### Data flow

1. **Discovery** -- `lib/discover.ts` walks up from `cwd` (or reads `BEADS_DIR`) to find `.beads/`. Also detects git remote URLs for all repos.
2. **Parsing** -- `lib/parse-beads.ts` reads `issues.jsonl` from the primary repo and any additional repos in `config.yaml`, deduplicates, extracts dependencies, and builds a graph structure with computed fields (blocker/dependent counts, prefix colors).
3. **Rendering** -- `components/BeadsGraph.tsx` uses `react-force-graph-2d` with fully custom canvas rendering (`paintNode`, `paintLink`). All transient state (hover, selection, comments, claimed avatars) stored in refs to avoid re-rendering the force simulation.
4. **Live updates** -- `app/api/beads/stream/route.ts` opens an SSE connection, watches all JSONL files, and pushes full data on each change. The client diffs and merges with position preservation.
5. **Timeline** -- `lib/timeline.ts` extracts sorted temporal events from the data. During replay, `filterDataAtTime()` produces a time-slice that flows through the same diff/merge pipeline as live updates.
6. **Comments** -- `lib/comments.ts` fetches 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/*`).
7. **Public API** -- `/api/v1/graph`, `/api/v1/issues/:id`, and `/api/v1/ready` combine beads data + comments + activity into enriched JSON for AI agents. No auth required, CORS enabled.
7. **Claims** -- A claim is a comment with text `@handle` (starts with `@`, no spaces). The `claimedNodeAvatars` useMemo in `page.tsx` scans all comments to build a `Map<nodeId, claimInfo>`. Avatars are drawn on canvas nodes and the minimap by `paintNode` via `claimedNodeAvatarsRef`.

### Key design decisions

- **paintNode/paintLink use refs, not props** -- Callbacks have `[]` dependencies and read from `selectedNodeRef`, `hoveredNodeRef`, `claimedNodeAvatarsRef`, etc. This prevents re-creating the ForceGraph component on every interaction.
- **Position preservation is critical** -- `react-force-graph-2d` mutates 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 by `mergeBeadsData()`, consumed by `paintNode`/`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 string `source`/`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 in `claimedNodeAvatars` useMemo.
- **Avatar image cache** -- Module-level `avatarImageCache` in `BeadsGraph.tsx`. No `crossOrigin` attribute on Image elements (Bluesky CDN CORS issue). `getAvatarImage()` returns cached `HTMLImageElement` or starts loading and returns null.
- **Portal for modals** -- `DescriptionModal` uses `createPortal(jsx, document.body)` with `z-[100]` to escape any parent stacking contexts.

---

## Tech Stack

- [Next.js 14](https://nextjs.org/) (App Router)
- [react-force-graph-2d](https://github.com/vasturiano/react-force-graph) for canvas-based graph rendering
- [d3-force](https://github.com/d3/d3-force) (forceCollide, forceRadial, forceX, forceY for layout modes)
- [Catppuccin Latte](https://github.com/catppuccin/palette) accent palette for prefix/person coloring (14 colors)
- [Tailwind CSS](https://tailwindcss.com/) for styling
- [TypeScript](https://www.typescriptlang.org/) throughout
- [@atproto/oauth-client-node](https://github.com/bluesky-social/atproto) + [@atproto/api](https://github.com/bluesky-social/atproto) for ATProto authentication and record CRUD
- [iron-session](https://github.com/vvo/iron-session) for encrypted cookie session management
- [react-markdown](https://github.com/remarkjs/react-markdown) + [remark-gfm](https://github.com/remarkjs/remark-gfm) for description rendering
- Node.js `fs.watch` for file change detection
- Server-Sent Events for real-time streaming

---

## License

MIT
