heartbeads 0.6.1 → 0.6.2

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 (85) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +2 -2
  3. package/.next/required-server-files.json +1 -1
  4. package/.next/server/app/_global-error.html +2 -2
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/api/beads/route.js.nft.json +1 -1
  20. package/.next/server/app/api/beads/stream/route.js.nft.json +1 -1
  21. package/.next/server/app/api/config/route.js.nft.json +1 -1
  22. package/.next/server/app/api/docs.html +2 -2
  23. package/.next/server/app/api/docs.rsc +1 -1
  24. package/.next/server/app/api/docs.segments/_full.segment.rsc +1 -1
  25. package/.next/server/app/api/docs.segments/_head.segment.rsc +1 -1
  26. package/.next/server/app/api/docs.segments/_index.segment.rsc +1 -1
  27. package/.next/server/app/api/docs.segments/_tree.segment.rsc +1 -1
  28. package/.next/server/app/api/docs.segments/api/docs/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/api/docs.segments/api/docs.segment.rsc +1 -1
  30. package/.next/server/app/api/docs.segments/api.segment.rsc +1 -1
  31. package/.next/server/app/api/v1/graph/route.js.nft.json +1 -1
  32. package/.next/server/app/api/v1/issues/[id]/route.js.nft.json +1 -1
  33. package/.next/server/app/api/v1/ready/route.js.nft.json +1 -1
  34. package/.next/server/app/index.html +1 -1
  35. package/.next/server/app/index.rsc +2 -2
  36. package/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  37. package/.next/server/app/index.segments/_full.segment.rsc +2 -2
  38. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  39. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  40. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  41. package/.next/server/app/login.html +1 -1
  42. package/.next/server/app/login.rsc +1 -1
  43. package/.next/server/app/login.segments/_full.segment.rsc +1 -1
  44. package/.next/server/app/login.segments/_head.segment.rsc +1 -1
  45. package/.next/server/app/login.segments/_index.segment.rsc +1 -1
  46. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  47. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +1 -1
  48. package/.next/server/app/login.segments/login.segment.rsc +1 -1
  49. package/.next/server/app/page_client-reference-manifest.js +1 -1
  50. package/.next/server/chunks/48cc8_next_dist_esm_build_templates_app-route_1d3b72ae.js +1 -1
  51. package/.next/server/chunks/48cc8_next_dist_esm_build_templates_app-route_1d3b72ae.js.map +1 -1
  52. package/.next/server/chunks/[root-of-the-server]__2d60806f._.js +2 -2
  53. package/.next/server/chunks/[root-of-the-server]__2d60806f._.js.map +1 -1
  54. package/.next/server/chunks/[root-of-the-server]__3cb3ed30._.js +1 -1
  55. package/.next/server/chunks/[root-of-the-server]__3cb3ed30._.js.map +1 -1
  56. package/.next/server/chunks/[root-of-the-server]__6125c4e2._.js +2 -2
  57. package/.next/server/chunks/[root-of-the-server]__6125c4e2._.js.map +1 -1
  58. package/.next/server/chunks/[root-of-the-server]__67c802f8._.js +2 -2
  59. package/.next/server/chunks/[root-of-the-server]__67c802f8._.js.map +1 -1
  60. package/.next/server/chunks/[root-of-the-server]__77e35ba8._.js +2 -2
  61. package/.next/server/chunks/[root-of-the-server]__77e35ba8._.js.map +1 -1
  62. package/.next/server/chunks/[root-of-the-server]__8261fec1._.js +1 -1
  63. package/.next/server/chunks/[root-of-the-server]__8261fec1._.js.map +1 -1
  64. package/.next/server/chunks/ssr/[root-of-the-server]__04e6e565._.js +2 -2
  65. package/.next/server/chunks/ssr/[root-of-the-server]__04e6e565._.js.map +1 -1
  66. package/.next/server/edge/chunks/48cc8_next_dist_5a0b8a90._.js +1 -1
  67. package/.next/server/edge/chunks/48cc8_next_dist_5a0b8a90._.js.map +1 -1
  68. package/.next/server/middleware/middleware-manifest.json +3 -3
  69. package/.next/server/middleware-manifest.json +3 -3
  70. package/.next/server/pages/404.html +1 -1
  71. package/.next/server/pages/500.html +2 -2
  72. package/.next/static/chunks/7e4fcecd4ca504d5.js +24 -0
  73. package/.next/static/zq8Ew2hci2YIJ61-i832n/_clientMiddlewareManifest.json +6 -0
  74. package/app/api/login/route.ts +15 -0
  75. package/app/page.tsx +46 -35
  76. package/components/BeadsGraph.tsx +15 -1
  77. package/components/NodeDetail.tsx +15 -7
  78. package/lib/activity.ts +22 -2
  79. package/lib/parse-beads.ts +24 -10
  80. package/lib/types.ts +82 -1
  81. package/package.json +1 -1
  82. package/.next/static/bc2EgBqgYtYOdWusC2URO/_clientMiddlewareManifest.json +0 -6
  83. package/.next/static/chunks/6cdcba7fb18f6f79.js +0 -24
  84. /package/.next/static/{bc2EgBqgYtYOdWusC2URO → zq8Ew2hci2YIJ61-i832n}/_buildManifest.js +0 -0
  85. /package/.next/static/{bc2EgBqgYtYOdWusC2URO → zq8Ew2hci2YIJ61-i832n}/_ssgManifest.js +0 -0
@@ -0,0 +1,6 @@
1
+ [
2
+ {
3
+ "regexp": "^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/((?!login|api\\/auth|api\\/oauth|_next\\/static|_next\\/image|favicon\\.ico|image\\.png|opengraph-image\\.png|twitter-image\\.png).*))(\\\\.json)?[\\/#\\?]?$",
4
+ "originalSource": "/((?!login|api/auth|api/oauth|_next/static|_next/image|favicon\\.ico|image\\.png|opengraph-image\\.png|twitter-image\\.png).*)"
5
+ }
6
+ ]
@@ -30,7 +30,22 @@ export async function POST(request: NextRequest) {
30
30
 
31
31
  return NextResponse.json({ redirectUrl: url.toString() });
32
32
  } catch (error) {
33
+ // Enhanced error logging for debugging
34
+ const errorObj = error as any;
33
35
  console.error("OAuth authorize failed:", error);
36
+ if (errorObj?.message) {
37
+ console.error("Error message:", errorObj.message);
38
+ }
39
+ if (errorObj?.cause) {
40
+ console.error("Error cause:", errorObj.cause);
41
+ }
42
+
43
+ // Check if error suggests OAuth endpoints are blocked by password middleware
44
+ const errorStr = errorObj?.message || String(error);
45
+ if (errorStr.includes('401') || errorStr.includes('Unauthorized') || errorStr.includes('client_metadata')) {
46
+ console.error("HINT: OAuth endpoints may be blocked by password middleware. Ensure /api/oauth/* routes are publicly accessible.");
47
+ }
48
+
34
49
  let errorMessage = "Couldn't initiate login";
35
50
 
36
51
  if (error instanceof OAuthResolverError) {
package/app/page.tsx CHANGED
@@ -434,8 +434,9 @@ export default function Home() {
434
434
  // Count per-person stats from nodes
435
435
  for (const node of nodes) {
436
436
  // Created: credit the creator
437
- if (node.createdBy) {
438
- const e = getOrCreate(node.createdBy);
437
+ const creator = node.createdBy || node.owner || node.assignee;
438
+ if (creator) {
439
+ const e = getOrCreate(creator);
439
440
  if (e) e.createdCount++;
440
441
  }
441
442
 
@@ -987,8 +988,9 @@ export default function Home() {
987
988
 
988
989
  // Count per-person stats from timeline-filtered nodes
989
990
  for (const node of nodes) {
990
- if (node.createdBy) {
991
- const e = getOrCreate(node.createdBy);
991
+ const creator = node.createdBy || node.owner || node.assignee;
992
+ if (creator) {
993
+ const e = getOrCreate(creator);
992
994
  if (e) e.createdCount++;
993
995
  }
994
996
  if (node.status === "closed" && node.assignee) {
@@ -1713,6 +1715,29 @@ export default function Home() {
1713
1715
  []
1714
1716
  );
1715
1717
 
1718
+ // Helper function to find the node ID where a contributor's avatar is displayed
1719
+ const findAvatarNodeId = useCallback(
1720
+ (handle: string): string | null => {
1721
+ // 1. Check claimed nodes first (claims take visual priority)
1722
+ for (const [nodeId, claim] of claimedNodeAvatars) {
1723
+ if (claim.handle === handle) return nodeId;
1724
+ }
1725
+ // 2. Fall back to assignee avatar node
1726
+ for (const [nodeId, info] of assigneeNodeAvatarsResolved) {
1727
+ if (info.handle === handle) return nodeId;
1728
+ }
1729
+ // 3. Last resort: any assigned open node, most recently updated
1730
+ if (data) {
1731
+ const fallback = data.graphData.nodes
1732
+ .filter((n) => n.assignee === handle && n.status !== "closed")
1733
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1734
+ if (fallback.length > 0) return fallback[0].id;
1735
+ }
1736
+ return null;
1737
+ },
1738
+ [claimedNodeAvatars, assigneeNodeAvatarsResolved, data]
1739
+ );
1740
+
1716
1741
  // Keyboard shortcut: Ctrl/Cmd+F to open search, Escape to close, Shift+0 to focus most recent node
1717
1742
  useEffect(() => {
1718
1743
  const handleKeyDown = (e: KeyboardEvent) => {
@@ -1755,44 +1780,16 @@ export default function Home() {
1755
1780
  const entry = timelineFilteredLeaderboard[rank - 1];
1756
1781
  if (!entry || !data) return;
1757
1782
 
1758
- // Find the node that actually displays this contributor's avatar
1759
- let targetNodeId: string | null = null;
1760
-
1761
- // 1. Check claimed nodes first (claims take visual priority)
1762
- for (const [nodeId, claim] of claimedNodeAvatars) {
1763
- if (claim.handle === entry.handle) {
1764
- targetNodeId = nodeId;
1765
- break;
1766
- }
1767
- }
1768
-
1769
- // 2. Fall back to assignee avatar node
1770
- if (!targetNodeId) {
1771
- for (const [nodeId, info] of assigneeNodeAvatarsResolved) {
1772
- if (info.handle === entry.handle) {
1773
- targetNodeId = nodeId;
1774
- break;
1775
- }
1776
- }
1777
- }
1778
-
1779
- // 3. Last resort: any assigned open node (in case avatar maps haven't populated yet)
1780
- if (!targetNodeId) {
1781
- const fallback = data.graphData.nodes
1782
- .filter(n => n.assignee === entry.handle && n.status !== 'closed')
1783
- .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
1784
- if (fallback.length > 0) targetNodeId = fallback[0].id;
1785
- }
1786
-
1783
+ const targetNodeId = findAvatarNodeId(entry.handle);
1787
1784
  if (targetNodeId) {
1788
- const node = data.graphData.nodes.find(n => n.id === targetNodeId);
1785
+ const node = data.graphData.nodes.find((n) => n.id === targetNodeId);
1789
1786
  if (node) graphRef.current?.focusNode(node);
1790
1787
  }
1791
1788
  }
1792
1789
  };
1793
1790
  window.addEventListener("keydown", handleKeyDown);
1794
1791
  return () => window.removeEventListener("keydown", handleKeyDown);
1795
- }, [searchOpen, timelineFilteredActivity, data, timelineFilteredLeaderboard, claimedNodeAvatars, assigneeNodeAvatarsResolved]);
1792
+ }, [searchOpen, timelineFilteredActivity, data, timelineFilteredLeaderboard, findAvatarNodeId]);
1796
1793
 
1797
1794
  // Handle search result selection via keyboard
1798
1795
  const handleSearchKeyDown = useCallback(
@@ -2523,6 +2520,9 @@ export default function Home() {
2523
2520
  setActivityPanelOpen(false);
2524
2521
  setHelpPanelOpen(false);
2525
2522
  setLeaderboardPanelOpen(false);
2523
+ // Zoom to the avatar node
2524
+ const nodeId = findAvatarNodeId(handle);
2525
+ if (nodeId) graphRef.current?.zoomToNode(nodeId);
2526
2526
  }}
2527
2527
  />
2528
2528
  </div>
@@ -2834,6 +2834,10 @@ export default function Home() {
2834
2834
  currentDid={session?.did}
2835
2835
  repoUrls={repoUrls}
2836
2836
  onOpenSettings={() => setSettingsModalOpen(true)}
2837
+ onProfileClick={(handle) => {
2838
+ setProfilePanelHandle(handle);
2839
+ setSelectedNode(null);
2840
+ }}
2837
2841
  />
2838
2842
 
2839
2843
  </div>
@@ -2893,6 +2897,10 @@ export default function Home() {
2893
2897
  currentDid={session?.did}
2894
2898
  repoUrls={repoUrls}
2895
2899
  onOpenSettings={() => setSettingsModalOpen(true)}
2900
+ onProfileClick={(handle) => {
2901
+ setProfilePanelHandle(handle);
2902
+ setSelectedNode(null);
2903
+ }}
2896
2904
  />
2897
2905
  </div>
2898
2906
  </div>
@@ -2950,6 +2958,9 @@ export default function Home() {
2950
2958
  setAllCommentsPanelOpen(false);
2951
2959
  setActivityPanelOpen(false);
2952
2960
  setHelpPanelOpen(false);
2961
+ // Zoom to the avatar node
2962
+ const nodeId = findAvatarNodeId(handle);
2963
+ if (nodeId) graphRef.current?.zoomToNode(nodeId);
2953
2964
  }}
2954
2965
  />
2955
2966
 
@@ -25,6 +25,7 @@ let _ForceGraph2DModule: React.ComponentType<any> | null = null;
25
25
  type LayoutMode = "force" | "dag" | "radial" | "cluster" | "spread";
26
26
  export interface BeadsGraphHandle {
27
27
  focusNode: (node: GraphNode) => void;
28
+ zoomToNode: (nodeId: string) => void;
28
29
  }
29
30
 
30
31
  interface BeadsGraphProps {
@@ -1064,8 +1065,21 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
1064
1065
  [onNodeClick, viewNodes]
1065
1066
  );
1066
1067
 
1068
+ // Zoom to node without selecting it (for profile panel navigation)
1069
+ const zoomToNode = useCallback(
1070
+ (nodeId: string) => {
1071
+ const fg = graphRef.current;
1072
+ if (!fg) return;
1073
+ const graphNode = viewNodes.find((n) => n.id === nodeId);
1074
+ if (!graphNode || graphNode.x === undefined || graphNode.y === undefined) return;
1075
+ fg.centerAt(graphNode.x, graphNode.y, 500);
1076
+ fg.zoom(2.5, 500);
1077
+ },
1078
+ [viewNodes]
1079
+ );
1080
+
1067
1081
  // Expose focusNode to parent via ref
1068
- useImperativeHandle(ref, () => ({ focusNode }), [focusNode]);
1082
+ useImperativeHandle(ref, () => ({ focusNode, zoomToNode }), [focusNode, zoomToNode]);
1069
1083
 
1070
1084
  // ForceGraph2D event handlers (extracted from inline lambdas for stable references)
1071
1085
  const handleForceGraphNodeHover = useCallback((node: any) => {
@@ -55,6 +55,7 @@ interface NodeDetailProps {
55
55
  currentDid?: string;
56
56
  repoUrls?: Record<string, string>;
57
57
  onOpenSettings?: () => void;
58
+ onProfileClick?: (handle: string) => void;
58
59
  }
59
60
 
60
61
  export default function NodeDetail({
@@ -70,6 +71,7 @@ export default function NodeDetail({
70
71
  currentDid,
71
72
  repoUrls,
72
73
  onOpenSettings,
74
+ onProfileClick,
73
75
  }: NodeDetailProps) {
74
76
  // Reply state — managed here so it's shared across the comment tree
75
77
  const [replyingToUri, setReplyingToUri] = useState<string | null>(null);
@@ -338,20 +340,26 @@ export default function NodeDetail({
338
340
  </span>
339
341
  </div>
340
342
  )}
341
- {node.owner && (
343
+ {(node.createdBy || node.owner || node.assignee) && (
342
344
  <div className="flex justify-between">
343
- <span>Owner</span>
344
- <span className="text-zinc-700 dark:text-zinc-300 font-medium truncate ml-2">
345
- {node.owner}
346
- </span>
345
+ <span>Created by</span>
346
+ <button
347
+ onClick={() => onProfileClick?.(node.createdBy || node.owner || node.assignee || "")}
348
+ className="text-zinc-700 dark:text-zinc-300 font-medium truncate ml-2 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors"
349
+ >
350
+ {node.createdBy || node.owner || node.assignee}
351
+ </button>
347
352
  </div>
348
353
  )}
349
354
  <div className="flex justify-between">
350
355
  <span>Assignee</span>
351
356
  {node.assignee ? (
352
- <span className="text-zinc-700 dark:text-zinc-300 font-medium truncate ml-2">
357
+ <button
358
+ onClick={() => onProfileClick?.(node.assignee!)}
359
+ className="text-zinc-700 dark:text-zinc-300 font-medium truncate ml-2 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors"
360
+ >
353
361
  {node.assignee}
354
- </span>
362
+ </button>
355
363
  ) : (
356
364
  <span className="text-zinc-400 dark:text-zinc-500 italic ml-2">
357
365
  No assignee
package/lib/activity.ts CHANGED
@@ -123,7 +123,13 @@ export function buildHistoricalFeed(
123
123
  time,
124
124
  nodeId: node.id,
125
125
  nodeTitle: node.title,
126
- actor: node.createdBy ? { handle: node.createdBy } : node.owner ? { handle: node.owner } : undefined,
126
+ actor: node.createdBy
127
+ ? { handle: node.createdBy }
128
+ : node.owner
129
+ ? { handle: node.owner }
130
+ : node.assignee
131
+ ? { handle: node.assignee }
132
+ : undefined,
127
133
  detail: node.issueType,
128
134
  meta: { issueType: node.issueType, prefix: node.prefix },
129
135
  });
@@ -140,7 +146,13 @@ export function buildHistoricalFeed(
140
146
  time,
141
147
  nodeId: node.id,
142
148
  nodeTitle: node.title,
143
- actor: node.owner ? { handle: node.owner } : node.createdBy ? { handle: node.createdBy } : undefined,
149
+ actor: node.owner
150
+ ? { handle: node.owner }
151
+ : node.createdBy
152
+ ? { handle: node.createdBy }
153
+ : node.assignee
154
+ ? { handle: node.assignee }
155
+ : undefined,
144
156
  detail: node.closeReason || "Closed",
145
157
  meta: { prefix: node.prefix },
146
158
  });
@@ -155,11 +167,13 @@ export function buildHistoricalFeed(
155
167
  if (!isNaN(time)) {
156
168
  const src = typeof link.source === "object" ? (link.source as { id: string }).id : link.source;
157
169
  const tgt = typeof link.target === "object" ? (link.target as { id: string }).id : link.target;
170
+ const sourceNode = nodes.find((n) => n.id === src);
158
171
  add({
159
172
  id: `link-added:${src}->${tgt}:${time}`,
160
173
  type: "link-added",
161
174
  time,
162
175
  nodeId: src,
176
+ actor: sourceNode?.assignee ? { handle: sourceNode.assignee } : undefined,
163
177
  detail: `${link.type} ${tgt}`,
164
178
  meta: { linkType: link.type, target: tgt },
165
179
  });
@@ -268,6 +282,7 @@ export function diffToActivityEvents(
268
282
  time: now,
269
283
  nodeId: id,
270
284
  nodeTitle: node?.title,
285
+ actor: node?.assignee ? { handle: node.assignee } : undefined,
271
286
  detail: node?.issueType || "task",
272
287
  meta: node ? { issueType: node.issueType, prefix: node.prefix } : undefined,
273
288
  });
@@ -275,11 +290,13 @@ export function diffToActivityEvents(
275
290
 
276
291
  // Removed nodes
277
292
  for (const id of diff.removedNodeIds) {
293
+ const node = nodeMap.get(id);
278
294
  events.push({
279
295
  id: `node-closed:${id}:${now}`,
280
296
  type: "node-closed",
281
297
  time: now,
282
298
  nodeId: id,
299
+ actor: node?.assignee ? { handle: node.assignee } : undefined,
283
300
  detail: "Removed",
284
301
  });
285
302
  }
@@ -314,6 +331,7 @@ export function diffToActivityEvents(
314
331
  time: now,
315
332
  nodeId: id,
316
333
  nodeTitle: node?.title,
334
+ actor: node?.assignee ? { handle: node.assignee } : undefined,
317
335
  detail: `${change.from} \u2192 ${change.to}`,
318
336
  meta: { field: change.field, from: change.from, to: change.to },
319
337
  });
@@ -326,11 +344,13 @@ export function diffToActivityEvents(
326
344
  const match = key.match(/^(.+)->(.+):(.+)$/);
327
345
  if (match) {
328
346
  const [, src, tgt, linkType] = match;
347
+ const sourceNode = nodeMap.get(src);
329
348
  events.push({
330
349
  id: `link-added:${key}:${now}`,
331
350
  type: "link-added",
332
351
  time: now,
333
352
  nodeId: src,
353
+ actor: sourceNode?.assignee ? { handle: sourceNode.assignee } : undefined,
334
354
  detail: `${linkType} ${tgt}`,
335
355
  meta: { linkType, target: tgt },
336
356
  });
@@ -84,17 +84,31 @@ export function parseIssuesJsonl(beadsDir?: string): BeadIssue[] {
84
84
  issues.push(...repoIssues);
85
85
  }
86
86
 
87
- // Deduplicate by issue ID (primary repo wins) and filter out tombstones.
88
- // bd delete marks issues with status "tombstone" rather than removing them
89
- // from the JSONL file we must exclude these from the graph.
90
- const seen = new Set<string>();
91
- return issues.filter((issue) => {
92
- if (seen.has(issue.id)) return false;
93
- seen.add(issue.id);
87
+ // Deduplicate by issue ID (primary repo wins for non-empty fields).
88
+ // When a duplicate is found, merge any empty fields from the duplicate into the first copy.
89
+ // This handles the case where the hub DB lost created_by/owner during import
90
+ // but individual repos have them.
91
+ const seen = new Map<string, BeadIssue>();
92
+ const result: BeadIssue[] = [];
93
+ for (const issue of issues) {
94
94
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
- if ((issue as any).status === "tombstone") return false;
96
- return true;
97
- });
95
+ if ((issue as any).status === "tombstone") continue;
96
+ const existing = seen.get(issue.id);
97
+ if (existing) {
98
+ // Merge: fill empty fields from duplicate
99
+ if (!existing.created_by && issue.created_by) existing.created_by = issue.created_by;
100
+ if (!existing.owner && issue.owner) existing.owner = issue.owner;
101
+ if (!existing.assignee && issue.assignee) existing.assignee = issue.assignee;
102
+ if (!existing.close_reason && issue.close_reason) existing.close_reason = issue.close_reason;
103
+ if (!existing.acceptance_criteria && issue.acceptance_criteria) existing.acceptance_criteria = issue.acceptance_criteria;
104
+ if (existing.estimated_minutes == null && issue.estimated_minutes != null) existing.estimated_minutes = issue.estimated_minutes;
105
+ if ((!existing.labels || existing.labels.length === 0) && issue.labels && issue.labels.length > 0) existing.labels = issue.labels;
106
+ } else {
107
+ seen.set(issue.id, issue);
108
+ result.push(issue);
109
+ }
110
+ }
111
+ return result;
98
112
  }
99
113
 
100
114
  /**
package/lib/types.ts CHANGED
@@ -161,8 +161,9 @@ export const PRIORITY_COLORS: Record<number, string> = {
161
161
  // Catppuccin Latte accent palette
162
162
  // ============================================================================
163
163
 
164
- /** Catppuccin Latte accent colors — 14 visually distinct, reordered for max contrast between adjacent indices */
164
+ /** Catppuccin Latte accent colors — 85 colors for collision-free prefix mapping */
165
165
  export const CATPPUCCIN_ACCENTS: string[] = [
166
+ // Original 14 Catppuccin Latte accents
166
167
  "#d20f39", // Red
167
168
  "#179299", // Teal
168
169
  "#fe640b", // Peach
@@ -177,11 +178,91 @@ export const CATPPUCCIN_ACCENTS: string[] = [
177
178
  "#7287fd", // Lavender
178
179
  "#dd7878", // Flamingo
179
180
  "#dc8a78", // Rosewater
181
+ // Extended palette (71 additional colors)
182
+ "#862d2d",
183
+ "#bf4040",
184
+ "#d27979",
185
+ "#931f1f",
186
+ "#d22d2d",
187
+ "#e06c6c",
188
+ "#a11212",
189
+ "#e61919",
190
+ "#ed5e5e",
191
+ "#86592d",
192
+ "#bf8040",
193
+ "#d2a679",
194
+ "#93591f",
195
+ "#d2802d",
196
+ "#e0a66c",
197
+ "#a15912",
198
+ "#e68019",
199
+ "#eda65e",
200
+ "#86862d",
201
+ "#bfbf40",
202
+ "#d2d279",
203
+ "#93931f",
204
+ "#d2d22d",
205
+ "#e0e06c",
206
+ "#a1a112",
207
+ "#e6e619",
208
+ "#eded5e",
209
+ "#59862d",
210
+ "#80bf40",
211
+ "#a6d279",
212
+ "#59931f",
213
+ "#80d22d",
214
+ "#a6e06c",
215
+ "#59a112",
216
+ "#80e619",
217
+ "#a6ed5e",
218
+ "#2d862d",
219
+ "#40bf40",
220
+ "#79d279",
221
+ "#1f931f",
222
+ "#2dd22d",
223
+ "#6ce06c",
224
+ "#12a112",
225
+ "#19e619",
226
+ "#5eed5e",
227
+ "#2d8659",
228
+ "#40bf80",
229
+ "#79d2a6",
230
+ "#1f9359",
231
+ "#2dd280",
232
+ "#6ce0a6",
233
+ "#12a159",
234
+ "#19e680",
235
+ "#5eeda6",
236
+ "#2d8686",
237
+ "#40bfbf",
238
+ "#79d2d2",
239
+ "#1f9393",
240
+ "#2dd2d2",
241
+ "#6ce0e0",
242
+ "#12a1a1",
243
+ "#19e6e6",
244
+ "#5eeded",
245
+ "#2d5986",
246
+ "#4080bf",
247
+ "#79a6d2",
248
+ "#1f5993",
249
+ "#2d80d2",
250
+ "#6ca6e0",
251
+ "#1259a1",
252
+ "#1980e6",
180
253
  ];
181
254
 
182
255
  export const CATPPUCCIN_ACCENT_NAMES: string[] = [
183
256
  "Red", "Teal", "Peach", "Blue", "Green", "Mauve", "Yellow",
184
257
  "Sapphire", "Pink", "Sky", "Maroon", "Lavender", "Flamingo", "Rosewater",
258
+ "Color1", "Color2", "Color3", "Color4", "Color5", "Color6", "Color7", "Color8", "Color9", "Color10",
259
+ "Color11", "Color12", "Color13", "Color14", "Color15", "Color16", "Color17", "Color18", "Color19", "Color20",
260
+ "Color21", "Color22", "Color23", "Color24", "Color25", "Color26", "Color27", "Color28", "Color29", "Color30",
261
+ "Color31", "Color32", "Color33", "Color34", "Color35", "Color36", "Color37", "Color38", "Color39", "Color40",
262
+ "Color41", "Color42", "Color43", "Color44", "Color45", "Color46", "Color47", "Color48", "Color49", "Color50",
263
+ "Color51", "Color52", "Color53", "Color54", "Color55", "Color56", "Color57", "Color58", "Color59", "Color60",
264
+ "Color61", "Color62", "Color63", "Color64", "Color65", "Color66", "Color67", "Color68", "Color69", "Color70",
265
+ "Color71",
185
266
  ];
186
267
 
187
268
  /** Catppuccin Latte Surface2 — used for "unassigned" / unknown values */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heartbeads",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Interactive dependency graph viewer for beads (bd) issues",
5
5
  "keywords": [
6
6
  "beads",
@@ -1,6 +0,0 @@
1
- [
2
- {
3
- "regexp": "^(?:\\/(_next\\/data\\/[^/]{1,}))?(?:\\/((?!login|api\\/auth|_next\\/static|_next\\/image|favicon\\.ico|image\\.png|opengraph-image\\.png|twitter-image\\.png).*))(\\\\.json)?[\\/#\\?]?$",
4
- "originalSource": "/((?!login|api/auth|_next/static|_next/image|favicon\\.ico|image\\.png|opengraph-image\\.png|twitter-image\\.png).*)"
5
- }
6
- ]