@blogic-cz/agent-tools 0.8.4 → 0.8.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -228,14 +228,24 @@ export const prThreadsCommand = Command.make(
228
228
  Flag.withDescription("Only show unresolved threads"),
229
229
  Flag.withDefault(true),
230
230
  ),
231
+ visibleOpenOnly: Flag.boolean("visible-open-only").pipe(
232
+ Flag.withDescription(
233
+ "Show threads that still look open to humans: unresolved threads plus resolved threads with no reply",
234
+ ),
235
+ Flag.withDefault(false),
236
+ ),
231
237
  },
232
- ({ format, pr, unresolvedOnly }) =>
238
+ ({ format, pr, unresolvedOnly, visibleOpenOnly }) =>
233
239
  Effect.gen(function* () {
234
240
  const prNumber = Option.getOrNull(pr);
235
- const threads = yield* fetchThreads(prNumber, unresolvedOnly);
241
+ const threads = yield* fetchThreads(prNumber, unresolvedOnly, visibleOpenOnly);
236
242
  yield* logFormatted(threads, format);
237
243
  }),
238
- ).pipe(Command.withDescription("Fetch review threads for a PR (unresolved by default)"));
244
+ ).pipe(
245
+ Command.withDescription(
246
+ "Fetch review threads for a PR (unresolved by default, or use --visible-open-only for reply-aware human-visible open items)",
247
+ ),
248
+ );
239
249
 
240
250
  export const prCommentsCommand = Command.make(
241
251
  "comments",
@@ -443,17 +453,18 @@ export const prReviewTriageCommand = Command.make(
443
453
  ({ format, pr }) =>
444
454
  Effect.gen(function* () {
445
455
  const prNumber = Option.getOrNull(pr);
446
- const [info, threads, summary, checks] = yield* Effect.all([
456
+ const [info, unresolvedThreads, visibleOpenThreads, summary, checks] = yield* Effect.all([
447
457
  viewPR(prNumber),
448
458
  fetchThreads(prNumber, true),
459
+ fetchThreads(prNumber, false, true),
449
460
  fetchDiscussionSummary(prNumber),
450
461
  fetchChecks(prNumber, false, false, 0),
451
462
  ]);
452
- yield* logFormatted({ info, unresolvedThreads: threads, summary, checks }, format);
463
+ yield* logFormatted({ info, unresolvedThreads, visibleOpenThreads, summary, checks }, format);
453
464
  }),
454
465
  ).pipe(
455
466
  Command.withDescription(
456
- "Composite: PR info + unresolved threads + discussion summary + checks status in one call",
467
+ "Composite: PR info + unresolved threads + visible-open threads + discussion summary + checks status in one call",
457
468
  ),
458
469
  );
459
470
 
@@ -236,6 +236,66 @@ const fetchAllThreadNodes = Effect.fn("pr.fetchAllThreadNodes")(function* (pr: n
236
236
  }
237
237
  });
238
238
 
239
+ const enrichThreads = (threads: ThreadNode[], reviewComments: ReviewComment[]): ReviewThread[] => {
240
+ const repliesByRootCommentId = new Map<number, ReviewComment[]>();
241
+
242
+ for (const comment of reviewComments) {
243
+ if (comment.inReplyToId === null) {
244
+ continue;
245
+ }
246
+
247
+ const replies = repliesByRootCommentId.get(comment.inReplyToId) ?? [];
248
+ replies.push(comment);
249
+ repliesByRootCommentId.set(comment.inReplyToId, replies);
250
+ }
251
+
252
+ return threads
253
+ .map((node) => {
254
+ const comment = node.comments.nodes[0];
255
+ if (!comment) {
256
+ return null;
257
+ }
258
+
259
+ const replies = repliesByRootCommentId.get(comment.databaseId) ?? [];
260
+ const lastReply = replies.reduce<ReviewComment | null>((latest, current) => {
261
+ if (latest === null) {
262
+ return current;
263
+ }
264
+
265
+ return new Date(current.createdAt).getTime() > new Date(latest.createdAt).getTime()
266
+ ? current
267
+ : latest;
268
+ }, null);
269
+ const hasReply = replies.length > 0;
270
+ const needsHumanReply = !hasReply;
271
+
272
+ return {
273
+ threadId: node.id,
274
+ commentId: comment.databaseId,
275
+ path: comment.path,
276
+ line: comment.line,
277
+ body: comment.body,
278
+ isResolved: node.isResolved,
279
+ hasReply,
280
+ replyCount: replies.length,
281
+ needsHumanReply,
282
+ isVisibleOpen: !node.isResolved || needsHumanReply,
283
+ lastReplyAuthor: lastReply?.author ?? null,
284
+ lastReplyAt: lastReply?.createdAt ?? null,
285
+ };
286
+ })
287
+ .filter((thread): thread is ReviewThread => thread !== null);
288
+ };
289
+
290
+ const fetchThreadState = Effect.fn("pr.fetchThreadState")(function* (pr: number) {
291
+ const [threads, reviewComments] = yield* Effect.all([
292
+ fetchAllThreadNodes(pr),
293
+ fetchComments(pr, null),
294
+ ]);
295
+
296
+ return enrichThreads(threads, reviewComments);
297
+ });
298
+
239
299
  // ---------------------------------------------------------------------------
240
300
  // Handlers
241
301
  // ---------------------------------------------------------------------------
@@ -255,29 +315,16 @@ const mapRawIssueComment = (comment: RawIssueComment): IssueComment => ({
255
315
  export const fetchThreads = Effect.fn("pr.fetchThreads")(function* (
256
316
  pr: number | null,
257
317
  unresolvedOnly: boolean,
318
+ visibleOpenOnly = false,
258
319
  ) {
259
320
  const resolvedPr = pr ?? (yield* viewPR(null)).number;
260
- const threads = yield* fetchAllThreadNodes(resolvedPr);
261
-
262
- const mapped: ReviewThread[] = threads
263
- .map((node) => {
264
- const comment = node.comments.nodes[0];
265
- if (!comment) {
266
- return null;
267
- }
321
+ const threads = yield* fetchThreadState(resolvedPr);
268
322
 
269
- return {
270
- threadId: node.id,
271
- commentId: comment.databaseId,
272
- path: comment.path,
273
- line: comment.line,
274
- body: comment.body,
275
- isResolved: node.isResolved,
276
- };
277
- })
278
- .filter((thread): thread is ReviewThread => thread !== null);
323
+ if (visibleOpenOnly) {
324
+ return threads.filter((thread) => thread.isVisibleOpen);
325
+ }
279
326
 
280
- return unresolvedOnly ? mapped.filter((t) => !t.isResolved) : mapped;
327
+ return unresolvedOnly ? threads.filter((thread) => !thread.isResolved) : threads;
281
328
  });
282
329
 
283
330
  /**
@@ -453,6 +500,7 @@ export const fetchDiscussionSummary = Effect.fn("pr.fetchDiscussionSummary")(fun
453
500
  latestIssueComment,
454
501
  reviewCommentsCount: reviewComments.length,
455
502
  reviewThreadsCount: threads.length,
503
+ visibleOpenReviewThreadsCount: threads.filter((thread) => thread.isVisibleOpen).length,
456
504
  repliedReviewThreadsCount: threads.length - unrepliedReviewThreadsCount,
457
505
  unrepliedReviewThreadsCount,
458
506
  resolvedUnrepliedReviewThreadsCount: threads.filter(
@@ -20,6 +20,12 @@ export type ReviewThread = {
20
20
  line: number;
21
21
  body: string;
22
22
  isResolved: boolean;
23
+ hasReply: boolean;
24
+ replyCount: number;
25
+ needsHumanReply: boolean;
26
+ isVisibleOpen: boolean;
27
+ lastReplyAuthor: string | null;
28
+ lastReplyAt: string | null;
23
29
  };
24
30
 
25
31
  export type ReviewComment = {