heartbeads 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-build-manifest.json +49 -0
  3. package/.next/app-path-routes-manifest.json +1 -0
  4. package/.next/build-manifest.json +32 -0
  5. package/.next/export-marker.json +1 -0
  6. package/.next/images-manifest.json +1 -0
  7. package/.next/next-minimal-server.js.nft.json +1 -0
  8. package/.next/next-server.js.nft.json +1 -0
  9. package/.next/package.json +1 -0
  10. package/.next/prerender-manifest.json +1 -0
  11. package/.next/react-loadable-manifest.json +8 -0
  12. package/.next/required-server-files.json +1 -0
  13. package/.next/routes-manifest.json +1 -0
  14. package/.next/server/app/_not-found/page.js +1 -0
  15. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  17. package/.next/server/app/_not-found.html +1 -0
  18. package/.next/server/app/_not-found.meta +6 -0
  19. package/.next/server/app/_not-found.rsc +9 -0
  20. package/.next/server/app/api/auth/route.js +1 -0
  21. package/.next/server/app/api/auth/route.js.nft.json +1 -0
  22. package/.next/server/app/api/beads/route.js +8 -0
  23. package/.next/server/app/api/beads/route.js.nft.json +1 -0
  24. package/.next/server/app/api/beads/stream/route.js +10 -0
  25. package/.next/server/app/api/beads/stream/route.js.nft.json +1 -0
  26. package/.next/server/app/api/beads.body +1 -0
  27. package/.next/server/app/api/beads.meta +1 -0
  28. package/.next/server/app/api/config/route.js +8 -0
  29. package/.next/server/app/api/config/route.js.nft.json +1 -0
  30. package/.next/server/app/api/docs/page.js +120 -0
  31. package/.next/server/app/api/docs/page.js.nft.json +1 -0
  32. package/.next/server/app/api/docs/page_client-reference-manifest.js +1 -0
  33. package/.next/server/app/api/docs.html +120 -0
  34. package/.next/server/app/api/docs.meta +5 -0
  35. package/.next/server/app/api/docs.rsc +70 -0
  36. package/.next/server/app/api/login/route.js +1 -0
  37. package/.next/server/app/api/login/route.js.nft.json +1 -0
  38. package/.next/server/app/api/logout/route.js +1 -0
  39. package/.next/server/app/api/logout/route.js.nft.json +1 -0
  40. package/.next/server/app/api/oauth/callback/route.js +1 -0
  41. package/.next/server/app/api/oauth/callback/route.js.nft.json +1 -0
  42. package/.next/server/app/api/oauth/client-metadata.json/route.js +1 -0
  43. package/.next/server/app/api/oauth/client-metadata.json/route.js.nft.json +1 -0
  44. package/.next/server/app/api/oauth/jwks.json/route.js +1 -0
  45. package/.next/server/app/api/oauth/jwks.json/route.js.nft.json +1 -0
  46. package/.next/server/app/api/records/route.js +1 -0
  47. package/.next/server/app/api/records/route.js.nft.json +1 -0
  48. package/.next/server/app/api/status/route.js +1 -0
  49. package/.next/server/app/api/status/route.js.nft.json +1 -0
  50. package/.next/server/app/api/v1/graph/route.js +1 -0
  51. package/.next/server/app/api/v1/graph/route.js.nft.json +1 -0
  52. package/.next/server/app/api/v1/issues/[id]/route.js +1 -0
  53. package/.next/server/app/api/v1/issues/[id]/route.js.nft.json +1 -0
  54. package/.next/server/app/api/v1/ready/route.js +1 -0
  55. package/.next/server/app/api/v1/ready/route.js.nft.json +1 -0
  56. package/.next/server/app/index.html +1 -0
  57. package/.next/server/app/index.meta +5 -0
  58. package/.next/server/app/index.rsc +9 -0
  59. package/.next/server/app/login/page.js +1 -0
  60. package/.next/server/app/login/page.js.nft.json +1 -0
  61. package/.next/server/app/login/page_client-reference-manifest.js +1 -0
  62. package/.next/server/app/login.html +1 -0
  63. package/.next/server/app/login.meta +5 -0
  64. package/.next/server/app/login.rsc +9 -0
  65. package/.next/server/app/opengraph-image.png/route.js +1 -0
  66. package/.next/server/app/opengraph-image.png/route.js.nft.json +1 -0
  67. package/.next/server/app/opengraph-image.png.body +0 -0
  68. package/.next/server/app/opengraph-image.png.meta +1 -0
  69. package/.next/server/app/page.js +24 -0
  70. package/.next/server/app/page.js.nft.json +1 -0
  71. package/.next/server/app/page_client-reference-manifest.js +1 -0
  72. package/.next/server/app/twitter-image.png/route.js +1 -0
  73. package/.next/server/app/twitter-image.png/route.js.nft.json +1 -0
  74. package/.next/server/app/twitter-image.png.body +0 -0
  75. package/.next/server/app/twitter-image.png.meta +1 -0
  76. package/.next/server/app-paths-manifest.json +22 -0
  77. package/.next/server/chunks/247.js +12 -0
  78. package/.next/server/chunks/29.js +1 -0
  79. package/.next/server/chunks/343.js +1 -0
  80. package/.next/server/chunks/460.js +12 -0
  81. package/.next/server/chunks/533.js +38 -0
  82. package/.next/server/chunks/542.js +27 -0
  83. package/.next/server/chunks/590.js +6 -0
  84. package/.next/server/chunks/615.js +15 -0
  85. package/.next/server/chunks/696.js +25 -0
  86. package/.next/server/chunks/719.js +2 -0
  87. package/.next/server/chunks/739.js +1 -0
  88. package/.next/server/chunks/950.js +2 -0
  89. package/.next/server/chunks/font-manifest.json +1 -0
  90. package/.next/server/edge-runtime-webpack.js +2 -0
  91. package/.next/server/edge-runtime-webpack.js.map +1 -0
  92. package/.next/server/font-manifest.json +1 -0
  93. package/.next/server/functions-config-manifest.json +1 -0
  94. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  95. package/.next/server/middleware-build-manifest.js +1 -0
  96. package/.next/server/middleware-manifest.json +32 -0
  97. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  98. package/.next/server/middleware.js +14 -0
  99. package/.next/server/middleware.js.map +1 -0
  100. package/.next/server/next-font-manifest.js +1 -0
  101. package/.next/server/next-font-manifest.json +1 -0
  102. package/.next/server/pages/404.html +1 -0
  103. package/.next/server/pages/500.html +1 -0
  104. package/.next/server/pages/_app.js +1 -0
  105. package/.next/server/pages/_app.js.nft.json +1 -0
  106. package/.next/server/pages/_document.js +1 -0
  107. package/.next/server/pages/_document.js.nft.json +1 -0
  108. package/.next/server/pages/_error.js +1 -0
  109. package/.next/server/pages/_error.js.nft.json +1 -0
  110. package/.next/server/pages-manifest.json +1 -0
  111. package/.next/server/server-reference-manifest.js +1 -0
  112. package/.next/server/server-reference-manifest.json +1 -0
  113. package/.next/server/webpack-runtime.js +1 -0
  114. package/.next/static/chunks/149.a3e3a5dc03e21086.js +1 -0
  115. package/.next/static/chunks/2200cc46-7c93a0e00b0bb825.js +1 -0
  116. package/.next/static/chunks/788-aa413085174e935a.js +1 -0
  117. package/.next/static/chunks/945-3ff1d381a0af1ecd.js +2 -0
  118. package/.next/static/chunks/971-bb44d52bcd9ee2a9.js +1 -0
  119. package/.next/static/chunks/app/_not-found/page-200b7a7a6cfc29df.js +1 -0
  120. package/.next/static/chunks/app/api/docs/page-1dc18f40154cdce6.js +1 -0
  121. package/.next/static/chunks/app/layout-13e3cdaaa416edb6.js +1 -0
  122. package/.next/static/chunks/app/login/page-60d930d64f021753.js +1 -0
  123. package/.next/static/chunks/app/not-found-ae1139bed2018dd8.js +1 -0
  124. package/.next/static/chunks/app/page-583300dd8af66e5a.js +1 -0
  125. package/.next/static/chunks/framework-6e06c675866dc992.js +1 -0
  126. package/.next/static/chunks/main-app-8b0c4a1007dbb7f4.js +1 -0
  127. package/.next/static/chunks/main-e680fb049d7426e1.js +1 -0
  128. package/.next/static/chunks/pages/_app-0c3037849002a4aa.js +1 -0
  129. package/.next/static/chunks/pages/_error-a647cd2c75dc4dc7.js +1 -0
  130. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  131. package/.next/static/chunks/webpack-117444a4bfe51057.js +1 -0
  132. package/.next/static/css/8c1b520a38ba4ccd.css +3 -0
  133. package/.next/static/vFM69sDrBUf_9ULwPmVAE/_buildManifest.js +1 -0
  134. package/.next/static/vFM69sDrBUf_9ULwPmVAE/_ssgManifest.js +1 -0
  135. package/README.md +389 -0
  136. package/app/api/auth/route.ts +103 -0
  137. package/app/api/beads/route.ts +27 -0
  138. package/app/api/beads/stream/route.ts +83 -0
  139. package/app/api/config/route.ts +48 -0
  140. package/app/api/docs/page.tsx +497 -0
  141. package/app/api/login/route.ts +42 -0
  142. package/app/api/logout/route.ts +14 -0
  143. package/app/api/oauth/callback/route.ts +97 -0
  144. package/app/api/oauth/client-metadata.json/route.ts +33 -0
  145. package/app/api/oauth/jwks.json/route.ts +32 -0
  146. package/app/api/records/route.ts +168 -0
  147. package/app/api/status/route.ts +25 -0
  148. package/app/api/v1/graph/route.ts +251 -0
  149. package/app/api/v1/issues/[id]/route.ts +158 -0
  150. package/app/api/v1/ready/route.ts +229 -0
  151. package/app/globals.css +230 -0
  152. package/app/layout.tsx +51 -0
  153. package/app/login/page.tsx +164 -0
  154. package/app/not-found.tsx +91 -0
  155. package/app/opengraph-image.png +0 -0
  156. package/app/page.tsx +2041 -0
  157. package/app/twitter-image.png +0 -0
  158. package/bin/heartbeads.mjs +225 -0
  159. package/components/ActivityItem.tsx +326 -0
  160. package/components/ActivityOverlay.tsx +125 -0
  161. package/components/ActivityPanel.tsx +345 -0
  162. package/components/AllCommentsPanel.tsx +270 -0
  163. package/components/AuthButton.tsx +202 -0
  164. package/components/BeadTooltip.tsx +246 -0
  165. package/components/BeadsGraph.tsx +2493 -0
  166. package/components/BeadsLogo.tsx +94 -0
  167. package/components/CommentTooltip.tsx +338 -0
  168. package/components/ContextMenu.tsx +272 -0
  169. package/components/DescriptionModal.tsx +595 -0
  170. package/components/GraphStats.tsx +121 -0
  171. package/components/HeartIcon.tsx +33 -0
  172. package/components/HelpPanel.tsx +339 -0
  173. package/components/MobileActionSheet.tsx +255 -0
  174. package/components/NodeDetail.tsx +793 -0
  175. package/components/SettingsModal.tsx +315 -0
  176. package/components/StatusLegend.tsx +99 -0
  177. package/components/TimelineBar.tsx +116 -0
  178. package/components/TutorialOverlay.tsx +235 -0
  179. package/hooks/useBeadsComments.ts +81 -0
  180. package/hooks/useIsMobile.ts +19 -0
  181. package/lib/activity.ts +377 -0
  182. package/lib/agent.ts +29 -0
  183. package/lib/api-helpers.ts +46 -0
  184. package/lib/auth/client.ts +221 -0
  185. package/lib/auth.tsx +159 -0
  186. package/lib/comments.ts +413 -0
  187. package/lib/diff-beads.ts +128 -0
  188. package/lib/discover.ts +228 -0
  189. package/lib/env.ts +33 -0
  190. package/lib/gate.ts +55 -0
  191. package/lib/parse-beads.ts +234 -0
  192. package/lib/session.ts +52 -0
  193. package/lib/settings.ts +42 -0
  194. package/lib/timeline.ts +138 -0
  195. package/lib/tts.ts +397 -0
  196. package/lib/types.ts +271 -0
  197. package/lib/utils.ts +48 -0
  198. package/lib/watch-beads.ts +97 -0
  199. package/next.config.mjs +4 -0
  200. package/package.json +81 -0
  201. package/postcss.config.mjs +9 -0
  202. package/public/image.png +0 -0
  203. package/scripts/generate-jwk.js +38 -0
  204. package/tailwind.config.ts +41 -0
  205. package/tsconfig.json +24 -0
package/README.md ADDED
@@ -0,0 +1,389 @@
1
+ # heartbeads
2
+
3
+ **Interactive dependency graph viewer for [beads](https://github.com/GainForest/beads) (bd) issues.**
4
+
5
+ ![heartbeads showing a multi-project dependency graph](public/image.png)
6
+
7
+ 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.
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ ### Graph Visualization
14
+
15
+ - **5 graph layouts** -- Switch between layout modes via the toolbar:
16
+ - **Force** -- Organic force simulation with collision avoidance and variable link distances
17
+ - **DAG** -- Clean top-down directed acyclic graph with topological layering
18
+ - **Radial** -- Concentric rings by dependency depth. Root nodes (no blockers) sit at center, deeper dependencies on outer rings. Ring spacing scales with node count.
19
+ - **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.
20
+ - **Spread** -- Like Force but maximally spaced for readability and screenshots. Stronger repulsion, wider link distances.
21
+ - **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.
22
+ - **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.
23
+ - **Legend color modes** -- Switch node fill color between 5 modes via the bottom-right legend panel:
24
+ - **Status** (default) -- Open (green), In Progress (amber), Blocked (red), Deferred (zinc), Closed (emerald)
25
+ - **Priority** -- P0 Critical (red), P1 High (orange), P2 Medium (blue), P3 Low (zinc), P4 Backlog (zinc-300)
26
+ - **Owner** -- Color by `createdBy` field using Catppuccin Latte accent palette (14 colors)
27
+ - **Assignee** -- Color by `assignee` field using Catppuccin Latte accent palette
28
+ - **Prefix** -- Color by project prefix using Catppuccin Latte accent palette
29
+ - **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.
30
+ - **Dependency arrows** -- Solid emerald arrows with flow particles for blocking relationships, dashed zinc lines for parent-child hierarchy. Curved links with configurable curvature.
31
+ - **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.
32
+ - **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.
33
+ - **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).
34
+ - **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).
35
+
36
+ ### Live Updates
37
+
38
+ - **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.
39
+ - **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.
40
+
41
+ ### Timeline Replay
42
+
43
+ - **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).
44
+ - **Play/pause controls** -- Play, pause, and scrub through the timeline with a slider.
45
+ - **Speed toggle** -- 1x, 2x, 4x playback speed (2 seconds per event at 1x).
46
+ - **Uses the same diff/merge pipeline** as live updates, so nodes get proper positions and spawn animations during replay.
47
+
48
+ ### Search
49
+
50
+ - **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.
51
+
52
+ ### Right-Click Context Menu
53
+
54
+ - **Multi-action menu** -- Right-click any node to open a context menu with actions:
55
+ - **Show description** -- Opens a full-screen modal with the issue's markdown description
56
+ - **Add comment** -- Opens the comment tooltip for posting
57
+ - **Claim task** -- Posts a claim comment (`@handle`) to mark yourself as working on the issue (only shown when authenticated and node is unclaimed)
58
+ - **Unclaim task** -- Removes your claim from the node (only shown when you are the claimant)
59
+ - **Collapse/Uncollapse epic** -- Toggle individual epic collapse on right-click (only shown on epic nodes)
60
+
61
+ ### Mobile Responsive
62
+
63
+ - **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.
64
+ - **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.
65
+ - **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).
66
+ - **Bottom drawer** -- Node detail slides up as a bottom drawer instead of a sidebar on mobile.
67
+
68
+ ### Task Claiming
69
+
70
+ - **Claim tasks** -- Right-click a node and select "Claim task" to mark yourself as working on it. Posts a special `@handle` comment via ATProto.
71
+ - **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.
72
+ - **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).
73
+ - **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.
74
+ - **Unclaim** -- Remove your claim via the context menu. Deletes the underlying ATProto comment record.
75
+
76
+ ### Node Detail Sidebar
77
+
78
+ - **Click-to-inspect** -- Click any node to open a detail sidebar with:
79
+ - Issue ID, title, type icon, status/priority/prefix badges
80
+ - GitHub repository link (auto-detected from git remote, shown as clickable badge + URL)
81
+ - Metrics grid: blocks count, dependent count
82
+ - Blocker and dependent lists with clickable navigation
83
+ - Dates: created, updated, closed (with hour:minute precision)
84
+ - Owner attribution
85
+ - Full description rendered as Markdown (with GFM support)
86
+ - Copy-to-clipboard button for descriptions (copies raw markdown with a header showing project prefix, issue ID, and GitHub repo URL)
87
+ - Threaded comment section (see below)
88
+ - **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".
89
+ - **Mobile drawer** -- On small screens, the detail panel slides up as a bottom drawer instead of a sidebar.
90
+
91
+ ### ATProto Authentication & Comments
92
+
93
+ - **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.
94
+ - **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.
95
+ - **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`).
96
+ - **Likes** -- Heart-toggle likes on comments (`org.impactindexer.review.like` lexicon). Rose-colored when liked by the current user.
97
+ - **Delete** -- Delete your own comments and likes.
98
+ - **Comment badges** -- Nodes with comments show a red notification badge with the comment count on the graph canvas.
99
+ - **All Comments panel** -- Slide-in sidebar showing all comments across all nodes with threaded replies, clickable node navigation pills, and like/delete actions.
100
+
101
+ ### Multi-Repo Support
102
+
103
+ - **Automatic aggregation** -- If your `.beads/config.yaml` lists additional repositories, heartbeads loads all of them into a unified graph.
104
+ - **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.
105
+ - **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.
106
+
107
+ ### Header / Navbar
108
+
109
+ - **Frosted glass header** -- `bg-white/95 backdrop-blur-sm` sticky header inspired by [plresearch.org](https://plresearch.org).
110
+ - **Animated heartbeat logo** -- An animated ECG/heartbeat trace SVG that continuously pulses — representing the living heartbeat of your project.
111
+ - **Pill navigation** -- Replay and Comments toggle buttons styled as rounded-full pill items. Auth button with rounded avatar dropdown.
112
+ - **Centered search** -- Rounded-full search bar with keyboard shortcut hint and dropdown results panel.
113
+ - **Mobile hamburger menu** -- On small screens, nav pills collapse into a hamburger menu with a slide-in drawer.
114
+
115
+ ### Info Panel & Legend
116
+
117
+ - **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.
118
+
119
+ ---
120
+
121
+ ## Quick Start
122
+
123
+ ```bash
124
+ cd ~/my-project
125
+ npx heartbeads@latest
126
+ ```
127
+
128
+ heartbeads walks up from your current directory to find `.beads/`, just like `git` finds `.git/`.
129
+
130
+ ### With an explicit path
131
+
132
+ ```bash
133
+ npx heartbeads@latest --beads-dir ~/projects/my-project/.beads
134
+ ```
135
+
136
+ ### Multi-repo aggregation
137
+
138
+ If your `.beads/config.yaml` lists additional repositories, heartbeads automatically loads all of them:
139
+
140
+ ```yaml
141
+ # .beads/config.yaml
142
+ repos:
143
+ additional:
144
+ - ../backend
145
+ - ../frontend
146
+ - ../shared-lib
147
+ ```
148
+
149
+ ---
150
+
151
+ ## CLI Reference
152
+
153
+ ```
154
+ heartbeads [options]
155
+
156
+ Options:
157
+ --port <number> Port to serve on (default: 3000)
158
+ --beads-dir <path> Explicit .beads/ directory path
159
+ --password <string> Require a password to access the dashboard and API
160
+ --dev Run in development mode (hot reload)
161
+ --help, -h Show this help message
162
+
163
+ Environment:
164
+ BEADS_DIR Override .beads/ discovery (same as --beads-dir)
165
+ HEARTBEADS_PASSWORD Require a password (same as --password)
166
+
167
+ Examples:
168
+ npx heartbeads@latest # Auto-discover from cwd
169
+ npx heartbeads@latest --port 4000 # Custom port
170
+ npx heartbeads@latest --beads-dir ~/projects/hub/.beads # Explicit path
171
+ npx heartbeads@latest --password secret # Password-protected
172
+ BEADS_DIR=../.beads npx heartbeads@latest --dev # Dev mode with env var
173
+ ```
174
+
175
+ ---
176
+
177
+ ## Public API
178
+
179
+ 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: *`).
180
+
181
+ When running with `--password`, API requests require authentication via one of:
182
+ - **Bearer token:** `curl -H "Authorization: Bearer <password>" ...`
183
+ - **Query param:** `curl "...?token=<password>"`
184
+
185
+ Without `--password`, all endpoints are open (no auth required).
186
+
187
+ ### `GET /api/v1/graph` -- Full project snapshot
188
+
189
+ Returns the entire beads graph with issues, dependencies, comments, claims, and recent activity in a single response.
190
+
191
+ ```bash
192
+ # Full snapshot (issues + comments + activity)
193
+ curl http://localhost:3000/api/v1/graph
194
+
195
+ # Only open issues, skip comments for speed
196
+ curl "http://localhost:3000/api/v1/graph?status=open&include="
197
+
198
+ # Open + in_progress with activity feed capped at 20
199
+ curl "http://localhost:3000/api/v1/graph?status=open,in_progress&include=comments,activity&limit=20"
200
+
201
+ # Filter by repo prefix
202
+ curl "http://localhost:3000/api/v1/graph?prefix=beads-map"
203
+ ```
204
+
205
+ | Param | Description | Example |
206
+ |-------|-------------|---------|
207
+ | `status` | Filter by status (comma-separated) | `open,in_progress` |
208
+ | `priority` | Filter by priority (comma-separated) | `0,1` |
209
+ | `prefix` | Filter by repo prefix | `beads-map` |
210
+ | `include` | Opt-in expensive fields (default: all) | `comments,activity` |
211
+ | `limit` | Activity feed cap (default 50, max 200) | `20` |
212
+
213
+ Response includes: `project` metadata, `issues[]` with comments and claims, `dependencies[]`, `stats`, `activity[]`, and `_meta`.
214
+
215
+ ### `GET /api/v1/issues/:id` -- Single issue detail
216
+
217
+ Returns one issue with full description, threaded comments, and enriched blockers/dependents (with title + status).
218
+
219
+ ```bash
220
+ curl http://localhost:3000/api/v1/issues/beads-map-abc
221
+
222
+ # Returns 404 with hint if not found
223
+ curl http://localhost:3000/api/v1/issues/nonexistent
224
+ ```
225
+
226
+ ### `GET /api/v1/ready` -- Actionable issues
227
+
228
+ Returns issues ready to work on: open or in_progress with no unresolved blockers. Sorted by priority (critical first), then by age (oldest first).
229
+
230
+ ```bash
231
+ # All actionable issues
232
+ curl http://localhost:3000/api/v1/ready
233
+
234
+ # Only unclaimed bugs
235
+ curl "http://localhost:3000/api/v1/ready?unclaimed=true&type=bug"
236
+
237
+ # Limit to top 10
238
+ curl "http://localhost:3000/api/v1/ready?limit=10"
239
+ ```
240
+
241
+ | Param | Description | Example |
242
+ |-------|-------------|---------|
243
+ | `unclaimed` | Only unclaimed issues | `true` |
244
+ | `type` | Filter by issue type (comma-separated) | `bug,feature` |
245
+ | `assignee` | Filter by assignee | `alice` |
246
+ | `prefix` | Filter by repo prefix | `beads-map` |
247
+ | `limit` | Max issues returned | `20` |
248
+
249
+ Response includes: `issues[]` with comments and claims, `stats` summary (total_ready, unclaimed, by_priority, by_type), and `_meta`.
250
+
251
+ ---
252
+
253
+ ## Development
254
+
255
+ ```bash
256
+ git clone https://github.com/GainForest/heartbeads.git
257
+ cd heartbeads
258
+ pnpm install
259
+
260
+ # Run in dev mode against a beads project
261
+ BEADS_DIR=~/path/to/.beads pnpm dev
262
+
263
+ # Build for production
264
+ pnpm build
265
+ ```
266
+
267
+ ### Quality gate
268
+
269
+ `pnpm build` must pass with zero errors before committing. There are no tests yet -- the build is the sole gate.
270
+
271
+ ---
272
+
273
+ ## Architecture
274
+
275
+ ```
276
+ heartbeads/
277
+ ├── middleware.ts # Password gate: auth check on every request (Edge Runtime)
278
+ ├── app/
279
+ │ ├── page.tsx # Main page: SSE wiring, merge logic, search, layout, comments, claims
280
+ │ ├── layout.tsx # Root layout, wraps in AuthProvider
281
+ │ ├── not-found.tsx # Custom 404 page with heartbeat logo
282
+ │ ├── login/page.tsx # Password login page (shown when --password flag is set)
283
+ │ ├── globals.css # Timeline slider styles, markdown prose, scrollbar
284
+ │ └── api/
285
+ │ ├── beads/
286
+ │ │ ├── route.ts # GET /api/beads -- one-shot full data load
287
+ │ │ └── stream/route.ts # GET /api/beads/stream -- SSE live updates
288
+ │ ├── auth/route.ts # GET/POST/DELETE /api/auth -- dashboard password gate
289
+ │ ├── v1/
290
+ │ │ ├── graph/route.ts # GET /api/v1/graph -- full project snapshot for AI agents
291
+ │ │ ├── issues/[id]/ # GET /api/v1/issues/:id -- single issue detail
292
+ │ │ └── ready/route.ts # GET /api/v1/ready -- actionable issues
293
+ │ ├── config/route.ts # GET /api/config -- project name, repo count, repo URLs
294
+ │ ├── login/route.ts # POST /api/login -- initiate ATProto OAuth
295
+ │ ├── logout/route.ts # POST /api/logout -- clear session
296
+ │ ├── status/route.ts # GET /api/status -- current auth state
297
+ │ ├── records/route.ts # POST/PUT/DELETE /api/records -- ATProto record CRUD
298
+ │ └── oauth/
299
+ │ ├── callback/route.ts # OAuth callback handler
300
+ │ ├── client-metadata.json/ # OAuth client metadata
301
+ │ └── jwks.json/route.ts # JSON Web Key Set
302
+ ├── components/
303
+ │ ├── AllCommentsPanel.tsx # Slide-in panel: all comments across nodes, threaded
304
+ │ ├── AuthButton.tsx # Sign-in modal + avatar dropdown (rounded-full pill style)
305
+ │ ├── BeadsGraph.tsx # Force graph: paintNode/paintLink, minimap, semantic zoom, avatars
306
+ │ ├── BeadsLogo.tsx # Animated heartbeat (ECG) SVG logo
307
+ │ ├── BeadTooltip.tsx # Hover tooltip: prefix, ID, title, date, blockers, priority, owner
308
+ │ ├── CommentTooltip.tsx # Floating right-click comment tooltip (bottom sheet on mobile)
309
+ │ ├── ContextMenu.tsx # Right-click context menu: description, comment, claim/unclaim
310
+ │ ├── DescriptionModal.tsx # Full-screen markdown description modal with copy button
311
+ │ ├── HeartIcon.tsx # Shared heart SVG (outline/filled)
312
+ │ ├── MobileActionSheet.tsx # Mobile bottom action sheet (double-tap context menu)
313
+ │ ├── NodeDetail.tsx # Sidebar detail: metadata, deps, comments, repo link
314
+ │ ├── TimelineBar.tsx # Timeline replay: play/pause, scrubber, speed toggle
315
+ │ ├── GraphStats.tsx # Issue count statistics widget (unused, inlined in BeadsGraph)
316
+ │ └── StatusLegend.tsx # Color legend for statuses (unused, inlined in BeadsGraph)
317
+ ├── hooks/
318
+ │ ├── useBeadsComments.ts # React hook wrapper for lib/comments.ts
319
+ │ └── useIsMobile.ts # Mobile viewport detection hook (<=768px)
320
+ ├── lib/
321
+ │ ├── comments.ts # Shared comment fetching, threading, claim detection
322
+ │ ├── gate.ts # Password gate: HMAC token generation/validation
323
+ │ ├── api-helpers.ts # CORS headers, JSON response builders for /api/v1/*
324
+ │ ├── auth.tsx # AuthProvider context + useAuth hook
325
+ │ ├── auth/client.ts # OAuth client factory (public/confidential mode)
326
+ │ ├── agent.ts # Authenticated ATProto agent from session
327
+ │ ├── session.ts # iron-session encrypted cookie setup
328
+ │ ├── env.ts # Environment variable validation
329
+ │ ├── discover.ts # .beads/ auto-discovery + git remote URL detection
330
+ │ ├── parse-beads.ts # JSONL parser, multi-repo hydration, graph builder
331
+ │ ├── types.ts # GraphNode, GraphLink, ColorMode, Catppuccin palette, color helpers
332
+ │ ├── diff-beads.ts # Diff engine for detecting added/removed/changed nodes/links
333
+ │ ├── watch-beads.ts # fs.watch wrapper with debounce for JSONL file changes
334
+ │ ├── settings.ts # ElevenLabs TTS settings (localStorage)
335
+ │ ├── tts.ts # Text-to-speech with ElevenLabs API, alignment cache
336
+ │ ├── activity.ts # Activity feed event types and builders
337
+ │ ├── timeline.ts # buildTimelineEvents + filterDataAtTime for replay
338
+ │ └── utils.ts # formatRelativeTime, buildDescriptionCopyText, shared utilities
339
+ ├── bin/
340
+ │ └── heartbeads.mjs # CLI entry point
341
+ ├── scripts/
342
+ │ └── generate-jwk.js # ES256 JWK key generation for OAuth
343
+ └── public/
344
+ └── image.png # Screenshot for README
345
+ ```
346
+
347
+ ### Data flow
348
+
349
+ 1. **Discovery** -- `lib/discover.ts` walks up from `cwd` (or reads `BEADS_DIR`) to find `.beads/`. Also detects git remote URLs for all repos.
350
+ 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).
351
+ 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.
352
+ 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.
353
+ 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.
354
+ 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/*`).
355
+ 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.
356
+ 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`.
357
+
358
+ ### Key design decisions
359
+
360
+ - **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.
361
+ - **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.
362
+ - **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.
363
+ - **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.
364
+ - **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.
365
+ - **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.
366
+ - **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.
367
+ - **Portal for modals** -- `DescriptionModal` uses `createPortal(jsx, document.body)` with `z-[100]` to escape any parent stacking contexts.
368
+
369
+ ---
370
+
371
+ ## Tech Stack
372
+
373
+ - [Next.js 14](https://nextjs.org/) (App Router)
374
+ - [react-force-graph-2d](https://github.com/vasturiano/react-force-graph) for canvas-based graph rendering
375
+ - [d3-force](https://github.com/d3/d3-force) (forceCollide, forceRadial, forceX, forceY for layout modes)
376
+ - [Catppuccin Latte](https://github.com/catppuccin/palette) accent palette for prefix/person coloring (14 colors)
377
+ - [Tailwind CSS](https://tailwindcss.com/) for styling
378
+ - [TypeScript](https://www.typescriptlang.org/) throughout
379
+ - [@atproto/oauth-client-node](https://github.com/bluesky-social/atproto) + [@atproto/api](https://github.com/bluesky-social/atproto) for ATProto authentication and record CRUD
380
+ - [iron-session](https://github.com/vvo/iron-session) for encrypted cookie session management
381
+ - [react-markdown](https://github.com/remarkjs/react-markdown) + [remark-gfm](https://github.com/remarkjs/remark-gfm) for description rendering
382
+ - Node.js `fs.watch` for file change detection
383
+ - Server-Sent Events for real-time streaming
384
+
385
+ ---
386
+
387
+ ## License
388
+
389
+ MIT
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Dashboard password authentication API.
3
+ *
4
+ * GET /api/auth — Check if password protection is active and if the request is authenticated
5
+ * POST /api/auth — Validate password and set auth cookie
6
+ * DELETE /api/auth — Clear auth cookie (lock dashboard)
7
+ *
8
+ * This is separate from ATProto OAuth (Sign In button in navbar).
9
+ * ATProto is for identity (comments, likes, claims).
10
+ * This gate controls who can view the dashboard and access the API.
11
+ */
12
+
13
+ import { NextResponse } from "next/server";
14
+ import { cookies } from "next/headers";
15
+ import { env } from "@/lib/env";
16
+ import {
17
+ COOKIE_NAME,
18
+ generateGateToken,
19
+ validateGateToken,
20
+ comparePassword,
21
+ } from "@/lib/gate";
22
+
23
+ export const dynamic = "force-dynamic";
24
+
25
+ const isProduction = process.env.NODE_ENV === "production";
26
+
27
+ export async function GET() {
28
+ const password = env.HEARTBEADS_PASSWORD;
29
+
30
+ if (!password) {
31
+ return NextResponse.json({ protected: false });
32
+ }
33
+
34
+ const cookieStore = await cookies();
35
+ const token = cookieStore.get(COOKIE_NAME)?.value;
36
+ const authenticated = token ? validateGateToken(token, password) : false;
37
+
38
+ return NextResponse.json({ protected: true, authenticated });
39
+ }
40
+
41
+ export async function POST(request: Request) {
42
+ const password = env.HEARTBEADS_PASSWORD;
43
+
44
+ if (!password) {
45
+ return NextResponse.json(
46
+ { error: "Password protection is not enabled" },
47
+ { status: 400 }
48
+ );
49
+ }
50
+
51
+ let body: { password?: string };
52
+ try {
53
+ body = await request.json();
54
+ } catch {
55
+ return NextResponse.json(
56
+ { error: "Invalid request body" },
57
+ { status: 400 }
58
+ );
59
+ }
60
+
61
+ const provided = body.password;
62
+ if (!provided || typeof provided !== "string") {
63
+ return NextResponse.json(
64
+ { error: "Password is required" },
65
+ { status: 400 }
66
+ );
67
+ }
68
+
69
+ if (!comparePassword(provided, password)) {
70
+ return NextResponse.json(
71
+ { error: "Invalid password" },
72
+ { status: 401 }
73
+ );
74
+ }
75
+
76
+ // Password matches — set the gate cookie
77
+ const token = generateGateToken(password);
78
+ const response = NextResponse.json({ success: true });
79
+
80
+ response.cookies.set(COOKIE_NAME, token, {
81
+ httpOnly: true,
82
+ sameSite: "lax",
83
+ secure: isProduction,
84
+ path: "/",
85
+ // Session cookie — no maxAge, expires when browser closes
86
+ });
87
+
88
+ return response;
89
+ }
90
+
91
+ export async function DELETE() {
92
+ const response = NextResponse.json({ success: true });
93
+
94
+ response.cookies.set(COOKIE_NAME, "", {
95
+ httpOnly: true,
96
+ sameSite: "lax",
97
+ secure: isProduction,
98
+ path: "/",
99
+ maxAge: 0, // Expire immediately
100
+ });
101
+
102
+ return response;
103
+ }
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from "next/server";
2
+ import { loadBeadsData } from "@/lib/parse-beads";
3
+ import { discoverBeadsDir } from "@/lib/discover";
4
+
5
+ export async function GET() {
6
+ try {
7
+ const { beadsDir } = discoverBeadsDir();
8
+ const data = loadBeadsData(beadsDir);
9
+ return NextResponse.json(data);
10
+ } catch (error: any) {
11
+ // Distinguish discovery errors (404) from data loading errors (500)
12
+ if (error?.message?.includes("No .beads/ directory found")) {
13
+ return NextResponse.json(
14
+ {
15
+ error: "No .beads directory found",
16
+ hint: "Run bd init in your project, then start heartbeads from that directory",
17
+ },
18
+ { status: 404 }
19
+ );
20
+ }
21
+ console.error("Failed to load beads data:", error);
22
+ return NextResponse.json(
23
+ { error: "Failed to load beads data" },
24
+ { status: 500 }
25
+ );
26
+ }
27
+ }
@@ -0,0 +1,83 @@
1
+ import { discoverBeadsDir } from "@/lib/discover";
2
+ import { loadBeadsData } from "@/lib/parse-beads";
3
+ import { watchBeadsFiles } from "@/lib/watch-beads";
4
+
5
+ // Prevent Next.js from statically optimizing this route
6
+ export const dynamic = "force-dynamic";
7
+
8
+ export async function GET(request: Request) {
9
+ let cleanup: (() => void) | null = null;
10
+
11
+ const stream = new ReadableStream({
12
+ start(controller) {
13
+ const encoder = new TextEncoder();
14
+
15
+ function send(data: unknown) {
16
+ try {
17
+ controller.enqueue(
18
+ encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
19
+ );
20
+ } catch {
21
+ // Stream closed — cleanup will handle
22
+ }
23
+ }
24
+
25
+ try {
26
+ const { beadsDir } = discoverBeadsDir();
27
+
28
+ // Send initial data
29
+ const initialData = loadBeadsData(beadsDir);
30
+ send(initialData);
31
+
32
+ // Watch for changes and push updates
33
+ cleanup = watchBeadsFiles(beadsDir, () => {
34
+ try {
35
+ const newData = loadBeadsData(beadsDir);
36
+ send(newData);
37
+ } catch (err) {
38
+ console.error("Failed to reload beads data:", err);
39
+ }
40
+ });
41
+
42
+ // Heartbeat every 30s to keep connection alive through proxies/firewalls
43
+ const heartbeat = setInterval(() => {
44
+ try {
45
+ controller.enqueue(encoder.encode(": heartbeat\n\n"));
46
+ } catch {
47
+ clearInterval(heartbeat);
48
+ }
49
+ }, 30000);
50
+
51
+ // Clean up when client disconnects
52
+ request.signal.addEventListener("abort", () => {
53
+ clearInterval(heartbeat);
54
+ if (cleanup) cleanup();
55
+ try {
56
+ controller.close();
57
+ } catch {
58
+ /* already closed */
59
+ }
60
+ });
61
+ } catch (err: unknown) {
62
+ // Discovery failed — send error and close
63
+ const message =
64
+ err instanceof Error ? err.message : "Unknown error";
65
+ send({ error: message });
66
+ controller.close();
67
+ }
68
+ },
69
+
70
+ cancel() {
71
+ if (cleanup) cleanup();
72
+ },
73
+ });
74
+
75
+ return new Response(stream, {
76
+ headers: {
77
+ "Content-Type": "text/event-stream",
78
+ "Cache-Control": "no-cache, no-transform",
79
+ Connection: "keep-alive",
80
+ "X-Accel-Buffering": "no", // Disable Nginx buffering
81
+ },
82
+ });
83
+ }
@@ -0,0 +1,48 @@
1
+ import { NextResponse } from "next/server";
2
+ import { discoverBeadsDir, getRepoUrls } from "@/lib/discover";
3
+ import { readFileSync, existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { parse as parseYaml } from "yaml";
6
+
7
+ export const dynamic = "force-dynamic";
8
+
9
+ export async function GET() {
10
+ try {
11
+ const discovery = discoverBeadsDir();
12
+
13
+ // Read repos list from config.yaml
14
+ let repos: string[] = ["."];
15
+ const configPath = join(discovery.beadsDir, "config.yaml");
16
+ try {
17
+ if (existsSync(configPath)) {
18
+ const content = readFileSync(configPath, "utf-8");
19
+ const config = parseYaml(content);
20
+ const additional = config?.repos?.additional;
21
+ if (Array.isArray(additional)) {
22
+ repos = [config?.repos?.primary || ".", ...additional];
23
+ }
24
+ }
25
+ } catch {
26
+ // config.yaml unreadable — use defaults
27
+ }
28
+
29
+ // Build prefix → GitHub URL mapping from git remotes
30
+ const repoUrls = getRepoUrls(discovery.beadsDir);
31
+
32
+ return NextResponse.json({
33
+ name: discovery.issuePrefix || discovery.repoName,
34
+ prefix: discovery.issuePrefix || null,
35
+ repoCount: 1 + discovery.additionalRepos,
36
+ repos,
37
+ repoUrls,
38
+ });
39
+ } catch {
40
+ // No .beads/ found — return safe defaults
41
+ return NextResponse.json({
42
+ name: "Beads",
43
+ prefix: null,
44
+ repoCount: 0,
45
+ repos: [],
46
+ });
47
+ }
48
+ }