beads-map 0.2.0 → 0.2.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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +2 -2
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/api/beads.body +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +6 -6
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/page-13ee27a84e4a0c70.js +1 -0
- package/.next/static/css/dbf588b653aa4019.css +3 -0
- package/app/page.tsx +118 -6
- package/bin/beads-map.mjs +32 -0
- package/components/ActivityItem.tsx +326 -0
- package/components/ActivityOverlay.tsx +123 -0
- package/components/ActivityPanel.tsx +345 -0
- package/components/AllCommentsPanel.tsx +9 -4
- package/components/AuthButton.tsx +7 -2
- package/components/BeadsGraph.tsx +11 -5
- package/components/CommentTooltip.tsx +7 -2
- package/components/NodeDetail.tsx +14 -4
- package/lib/activity.ts +377 -0
- package/lib/diff-beads.ts +3 -0
- package/package.json +1 -1
- package/.next/static/chunks/app/page-f36cdcae49f1d2af.js +0 -1
- package/.next/static/css/a4e34aaaa51183d9.css +0 -3
- /package/.next/static/{YVdbDxCehgqcYmLncYRFB → dxp53pVl-eTmydUx_hpyJ}/_buildManifest.js +0 -0
- /package/.next/static/{YVdbDxCehgqcYmLncYRFB → dxp53pVl-eTmydUx_hpyJ}/_ssgManifest.js +0 -0
package/lib/activity.ts
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity feed: unified event type and builders for historical + real-time events.
|
|
3
|
+
*
|
|
4
|
+
* Data sources:
|
|
5
|
+
* 1. Issue JSONL timestamps (created_at, closed_at, updated_at)
|
|
6
|
+
* 2. Dependency timestamps (link created_at)
|
|
7
|
+
* 3. ATProto comments, claims, likes (from useBeadsComments)
|
|
8
|
+
* 4. Real-time SSE diffs (BeadsDiff from diffBeadsData)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { GraphNode, GraphLink } from "./types";
|
|
12
|
+
import type { BeadsDiff, NodeChange } from "./diff-beads";
|
|
13
|
+
import type { BeadsComment } from "@/hooks/useBeadsComments";
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export type ActivityEventType =
|
|
20
|
+
| "node-created"
|
|
21
|
+
| "node-closed"
|
|
22
|
+
| "node-status-changed"
|
|
23
|
+
| "node-priority-changed"
|
|
24
|
+
| "node-title-changed"
|
|
25
|
+
| "node-owner-changed"
|
|
26
|
+
| "link-added"
|
|
27
|
+
| "link-removed"
|
|
28
|
+
| "comment-added"
|
|
29
|
+
| "reply-added"
|
|
30
|
+
| "task-claimed"
|
|
31
|
+
| "task-unclaimed"
|
|
32
|
+
| "like-added";
|
|
33
|
+
|
|
34
|
+
export interface ActivityActor {
|
|
35
|
+
handle: string;
|
|
36
|
+
avatar?: string;
|
|
37
|
+
did?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ActivityEvent {
|
|
41
|
+
/** Unique key for React rendering and deduplication: `${type}:${nodeId}:${time}` */
|
|
42
|
+
id: string;
|
|
43
|
+
type: ActivityEventType;
|
|
44
|
+
/** Unix milliseconds, for sorting */
|
|
45
|
+
time: number;
|
|
46
|
+
/** Which issue this event relates to */
|
|
47
|
+
nodeId: string;
|
|
48
|
+
/** Issue title for display (may be undefined for deleted nodes) */
|
|
49
|
+
nodeTitle?: string;
|
|
50
|
+
/** Who performed the action (for comments, claims, likes) */
|
|
51
|
+
actor?: ActivityActor;
|
|
52
|
+
/** Human-readable detail: e.g. "open -> in_progress", comment text preview, link target */
|
|
53
|
+
detail?: string;
|
|
54
|
+
/** Extra structured context */
|
|
55
|
+
meta?: Record<string, string>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Filter category for the UI */
|
|
59
|
+
export type ActivityFilterCategory =
|
|
60
|
+
| "issues"
|
|
61
|
+
| "deps"
|
|
62
|
+
| "comments"
|
|
63
|
+
| "claims"
|
|
64
|
+
| "likes";
|
|
65
|
+
|
|
66
|
+
/** Map event types to filter categories */
|
|
67
|
+
export function getEventCategory(type: ActivityEventType): ActivityFilterCategory {
|
|
68
|
+
switch (type) {
|
|
69
|
+
case "node-created":
|
|
70
|
+
case "node-closed":
|
|
71
|
+
case "node-status-changed":
|
|
72
|
+
case "node-priority-changed":
|
|
73
|
+
case "node-title-changed":
|
|
74
|
+
case "node-owner-changed":
|
|
75
|
+
return "issues";
|
|
76
|
+
case "link-added":
|
|
77
|
+
case "link-removed":
|
|
78
|
+
return "deps";
|
|
79
|
+
case "comment-added":
|
|
80
|
+
case "reply-added":
|
|
81
|
+
return "comments";
|
|
82
|
+
case "task-claimed":
|
|
83
|
+
case "task-unclaimed":
|
|
84
|
+
return "claims";
|
|
85
|
+
case "like-added":
|
|
86
|
+
return "likes";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Historical feed builder
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build the full historical activity feed from existing data.
|
|
96
|
+
* Called once on load and when allComments changes.
|
|
97
|
+
*/
|
|
98
|
+
export function buildHistoricalFeed(
|
|
99
|
+
nodes: GraphNode[],
|
|
100
|
+
links: GraphLink[],
|
|
101
|
+
allComments: BeadsComment[] | null
|
|
102
|
+
): ActivityEvent[] {
|
|
103
|
+
const events: ActivityEvent[] = [];
|
|
104
|
+
const seen = new Set<string>();
|
|
105
|
+
|
|
106
|
+
function add(event: ActivityEvent) {
|
|
107
|
+
if (seen.has(event.id)) return;
|
|
108
|
+
seen.add(event.id);
|
|
109
|
+
events.push(event);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Issue lifecycle events ---
|
|
113
|
+
for (const node of nodes) {
|
|
114
|
+
// Created
|
|
115
|
+
if (node.createdAt) {
|
|
116
|
+
const time = new Date(node.createdAt).getTime();
|
|
117
|
+
if (!isNaN(time)) {
|
|
118
|
+
add({
|
|
119
|
+
id: `node-created:${node.id}:${time}`,
|
|
120
|
+
type: "node-created",
|
|
121
|
+
time,
|
|
122
|
+
nodeId: node.id,
|
|
123
|
+
nodeTitle: node.title,
|
|
124
|
+
detail: node.issueType,
|
|
125
|
+
meta: { issueType: node.issueType, prefix: node.prefix },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Closed
|
|
131
|
+
if (node.closedAt) {
|
|
132
|
+
const time = new Date(node.closedAt).getTime();
|
|
133
|
+
if (!isNaN(time)) {
|
|
134
|
+
add({
|
|
135
|
+
id: `node-closed:${node.id}:${time}`,
|
|
136
|
+
type: "node-closed",
|
|
137
|
+
time,
|
|
138
|
+
nodeId: node.id,
|
|
139
|
+
nodeTitle: node.title,
|
|
140
|
+
detail: node.closeReason || "Closed",
|
|
141
|
+
meta: { prefix: node.prefix },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Dependency events ---
|
|
148
|
+
for (const link of links) {
|
|
149
|
+
if (link.createdAt) {
|
|
150
|
+
const time = new Date(link.createdAt).getTime();
|
|
151
|
+
if (!isNaN(time)) {
|
|
152
|
+
const src = typeof link.source === "object" ? (link.source as { id: string }).id : link.source;
|
|
153
|
+
const tgt = typeof link.target === "object" ? (link.target as { id: string }).id : link.target;
|
|
154
|
+
add({
|
|
155
|
+
id: `link-added:${src}->${tgt}:${time}`,
|
|
156
|
+
type: "link-added",
|
|
157
|
+
time,
|
|
158
|
+
nodeId: src,
|
|
159
|
+
detail: `${link.type} ${tgt}`,
|
|
160
|
+
meta: { linkType: link.type, target: tgt },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Comment, claim, and like events ---
|
|
167
|
+
if (allComments) {
|
|
168
|
+
for (const comment of allComments) {
|
|
169
|
+
const time = new Date(comment.createdAt).getTime();
|
|
170
|
+
if (isNaN(time)) continue;
|
|
171
|
+
|
|
172
|
+
const actor: ActivityActor = {
|
|
173
|
+
handle: comment.handle,
|
|
174
|
+
avatar: comment.avatar,
|
|
175
|
+
did: comment.did,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const isClaim =
|
|
179
|
+
comment.text.startsWith("@") &&
|
|
180
|
+
comment.text.trim().indexOf(" ") === -1;
|
|
181
|
+
|
|
182
|
+
if (isClaim) {
|
|
183
|
+
add({
|
|
184
|
+
id: `task-claimed:${comment.nodeId}:${time}`,
|
|
185
|
+
type: "task-claimed",
|
|
186
|
+
time,
|
|
187
|
+
nodeId: comment.nodeId,
|
|
188
|
+
actor,
|
|
189
|
+
detail: comment.text,
|
|
190
|
+
});
|
|
191
|
+
} else if (comment.replyTo) {
|
|
192
|
+
add({
|
|
193
|
+
id: `reply-added:${comment.nodeId}:${comment.rkey}`,
|
|
194
|
+
type: "reply-added",
|
|
195
|
+
time,
|
|
196
|
+
nodeId: comment.nodeId,
|
|
197
|
+
actor,
|
|
198
|
+
detail:
|
|
199
|
+
comment.text.length > 80
|
|
200
|
+
? comment.text.slice(0, 80) + "..."
|
|
201
|
+
: comment.text,
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
add({
|
|
205
|
+
id: `comment-added:${comment.nodeId}:${comment.rkey}`,
|
|
206
|
+
type: "comment-added",
|
|
207
|
+
time,
|
|
208
|
+
nodeId: comment.nodeId,
|
|
209
|
+
actor,
|
|
210
|
+
detail:
|
|
211
|
+
comment.text.length > 80
|
|
212
|
+
? comment.text.slice(0, 80) + "..."
|
|
213
|
+
: comment.text,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Likes on this comment
|
|
218
|
+
for (const like of comment.likes) {
|
|
219
|
+
const likeTime = new Date(like.createdAt).getTime();
|
|
220
|
+
if (isNaN(likeTime)) continue;
|
|
221
|
+
add({
|
|
222
|
+
id: `like-added:${comment.nodeId}:${like.rkey}`,
|
|
223
|
+
type: "like-added",
|
|
224
|
+
time: likeTime,
|
|
225
|
+
nodeId: comment.nodeId,
|
|
226
|
+
actor: {
|
|
227
|
+
handle: like.handle,
|
|
228
|
+
avatar: like.avatar,
|
|
229
|
+
did: like.did,
|
|
230
|
+
},
|
|
231
|
+
detail: `Liked comment by ${comment.handle}`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Sort newest-first
|
|
238
|
+
events.sort((a, b) => b.time - a.time);
|
|
239
|
+
return events;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// Real-time diff -> events converter
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Convert a BeadsDiff into ActivityEvent items.
|
|
248
|
+
* Called on each SSE message after diffBeadsData().
|
|
249
|
+
*/
|
|
250
|
+
export function diffToActivityEvents(
|
|
251
|
+
diff: BeadsDiff,
|
|
252
|
+
nodes: GraphNode[]
|
|
253
|
+
): ActivityEvent[] {
|
|
254
|
+
const events: ActivityEvent[] = [];
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
257
|
+
|
|
258
|
+
// Added nodes
|
|
259
|
+
for (const id of diff.addedNodeIds) {
|
|
260
|
+
const node = nodeMap.get(id);
|
|
261
|
+
events.push({
|
|
262
|
+
id: `node-created:${id}:${now}`,
|
|
263
|
+
type: "node-created",
|
|
264
|
+
time: now,
|
|
265
|
+
nodeId: id,
|
|
266
|
+
nodeTitle: node?.title,
|
|
267
|
+
detail: node?.issueType || "task",
|
|
268
|
+
meta: node ? { issueType: node.issueType, prefix: node.prefix } : undefined,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Removed nodes
|
|
273
|
+
for (const id of diff.removedNodeIds) {
|
|
274
|
+
events.push({
|
|
275
|
+
id: `node-closed:${id}:${now}`,
|
|
276
|
+
type: "node-closed",
|
|
277
|
+
time: now,
|
|
278
|
+
nodeId: id,
|
|
279
|
+
detail: "Removed",
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Changed nodes
|
|
284
|
+
for (const [id, changes] of diff.changedNodes) {
|
|
285
|
+
const node = nodeMap.get(id);
|
|
286
|
+
for (const change of changes) {
|
|
287
|
+
let type: ActivityEventType;
|
|
288
|
+
switch (change.field) {
|
|
289
|
+
case "status":
|
|
290
|
+
type = "node-status-changed";
|
|
291
|
+
break;
|
|
292
|
+
case "priority":
|
|
293
|
+
type = "node-priority-changed";
|
|
294
|
+
break;
|
|
295
|
+
case "title":
|
|
296
|
+
type = "node-title-changed";
|
|
297
|
+
break;
|
|
298
|
+
case "owner":
|
|
299
|
+
type = "node-owner-changed";
|
|
300
|
+
break;
|
|
301
|
+
default:
|
|
302
|
+
type = "node-status-changed"; // fallback
|
|
303
|
+
}
|
|
304
|
+
events.push({
|
|
305
|
+
id: `${type}:${id}:${now}:${change.field}`,
|
|
306
|
+
type,
|
|
307
|
+
time: now,
|
|
308
|
+
nodeId: id,
|
|
309
|
+
nodeTitle: node?.title,
|
|
310
|
+
detail: `${change.from} \u2192 ${change.to}`,
|
|
311
|
+
meta: { field: change.field, from: change.from, to: change.to },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Added links
|
|
317
|
+
for (const key of diff.addedLinkKeys) {
|
|
318
|
+
// key format: "source->target:type"
|
|
319
|
+
const match = key.match(/^(.+)->(.+):(.+)$/);
|
|
320
|
+
if (match) {
|
|
321
|
+
const [, src, tgt, linkType] = match;
|
|
322
|
+
events.push({
|
|
323
|
+
id: `link-added:${key}:${now}`,
|
|
324
|
+
type: "link-added",
|
|
325
|
+
time: now,
|
|
326
|
+
nodeId: src,
|
|
327
|
+
detail: `${linkType} ${tgt}`,
|
|
328
|
+
meta: { linkType, target: tgt },
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Removed links
|
|
334
|
+
for (const key of diff.removedLinkKeys) {
|
|
335
|
+
const match = key.match(/^(.+)->(.+):(.+)$/);
|
|
336
|
+
if (match) {
|
|
337
|
+
const [, src, tgt, linkType] = match;
|
|
338
|
+
events.push({
|
|
339
|
+
id: `link-removed:${key}:${now}`,
|
|
340
|
+
type: "link-removed",
|
|
341
|
+
time: now,
|
|
342
|
+
nodeId: src,
|
|
343
|
+
detail: `${linkType} ${tgt}`,
|
|
344
|
+
meta: { linkType, target: tgt },
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return events;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ============================================================================
|
|
353
|
+
// Feed management helpers
|
|
354
|
+
// ============================================================================
|
|
355
|
+
|
|
356
|
+
/** Maximum events to keep in the feed */
|
|
357
|
+
export const MAX_FEED_SIZE = 200;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Merge new events into an existing feed, deduplicating by event ID.
|
|
361
|
+
* Returns a new array sorted newest-first, capped at MAX_FEED_SIZE.
|
|
362
|
+
*/
|
|
363
|
+
export function mergeFeedEvents(
|
|
364
|
+
existing: ActivityEvent[],
|
|
365
|
+
incoming: ActivityEvent[]
|
|
366
|
+
): ActivityEvent[] {
|
|
367
|
+
const seen = new Set(existing.map((e) => e.id));
|
|
368
|
+
const merged = [...existing];
|
|
369
|
+
for (const event of incoming) {
|
|
370
|
+
if (!seen.has(event.id)) {
|
|
371
|
+
seen.add(event.id);
|
|
372
|
+
merged.push(event);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
merged.sort((a, b) => b.time - a.time);
|
|
376
|
+
return merged.slice(0, MAX_FEED_SIZE);
|
|
377
|
+
}
|
package/lib/diff-beads.ts
CHANGED
|
@@ -82,6 +82,9 @@ export function diffBeadsData(
|
|
|
82
82
|
if (old.title !== node.title) {
|
|
83
83
|
changes.push({ field: "title", from: old.title, to: node.title });
|
|
84
84
|
}
|
|
85
|
+
if ((old.owner || "") !== (node.owner || "")) {
|
|
86
|
+
changes.push({ field: "owner", from: old.owner || "", to: node.owner || "" });
|
|
87
|
+
}
|
|
85
88
|
if (changes.length > 0) {
|
|
86
89
|
changedNodes.set(id, changes);
|
|
87
90
|
}
|