@difflab/pi 0.3.0-rc.202609201610.cddc7d6 → 0.3.0-rc.202609201720.4138cef

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.
@@ -16861,6 +16861,15 @@ function localResponseMarker(threadId) {
16861
16861
  function isLocalResponse(body) {
16862
16862
  return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
16863
16863
  }
16864
+ function parseReviewThreadAction(body) {
16865
+ const match = body.match(/^\[(REOPEN|RESOLVE)\]\s*/i);
16866
+ if (!match)
16867
+ return { body };
16868
+ return {
16869
+ action: match[1].toLowerCase(),
16870
+ body: body.slice(match[0].length)
16871
+ };
16872
+ }
16864
16873
  function dedupeFindings(findings) {
16865
16874
  const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
16866
16875
  const byKey = new Map;
@@ -17199,16 +17208,18 @@ function GitHubReviewBackend(vcs, number) {
17199
17208
  `body=${input.body}`
17200
17209
  ]);
17201
17210
  }
17202
- if (input.resolve) {
17203
- await ghChecked([
17204
- "api",
17205
- "graphql",
17206
- "-f",
17207
- `query=${GITHUB_RESOLVE_MUTATION}`,
17208
- "-f",
17209
- `threadId=${input.threadId}`
17210
- ]);
17211
- }
17211
+ if (input.resolve)
17212
+ await this.setResolved?.(input.threadId, true);
17213
+ },
17214
+ async setResolved(threadId, resolved) {
17215
+ await ghChecked([
17216
+ "api",
17217
+ "graphql",
17218
+ "-f",
17219
+ `query=${resolved ? GITHUB_RESOLVE_MUTATION : GITHUB_UNRESOLVE_MUTATION}`,
17220
+ "-f",
17221
+ `threadId=${threadId}`
17222
+ ]);
17212
17223
  },
17213
17224
  async publish(event) {
17214
17225
  const pending = await pendingReview();
@@ -17229,6 +17240,7 @@ var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$aft
17229
17240
  var GITHUB_ADD_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!,$line:Int!,$side:DiffSide!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,line:$line,side:$side}){thread{id}}}`;
17230
17241
  var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
17231
17242
  var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
17243
+ var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
17232
17244
 
17233
17245
  // src/review/gitlab-review-backend.ts
17234
17246
  function GitLabReviewBackend(vcs, number) {
@@ -17335,7 +17347,11 @@ function GitLabReviewBackend(vcs, number) {
17335
17347
  });
17336
17348
  }
17337
17349
  if (input.resolve)
17338
- await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=true`]);
17350
+ await this.setResolved?.(input.threadId, true);
17351
+ },
17352
+ async setResolved(threadId, resolved) {
17353
+ const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
17354
+ await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
17339
17355
  },
17340
17356
  async publish(event) {
17341
17357
  assertReviewEventSupported(vcs.provider, event);
@@ -17749,7 +17765,16 @@ function createReviewTools() {
17749
17765
  review.pr ? `PR/MR: #${review.pr.number} ${review.pr.url}` : "PR/MR: none",
17750
17766
  session ? `tuicr: ${session.slug} (${session.commentCount} comments)` : "tuicr: none"
17751
17767
  ].join(`
17752
- `), { ...review, env, store, session, baseRef, backend });
17768
+ `), {
17769
+ cwd: review.cwd,
17770
+ vcs: review.vcs,
17771
+ pr: review.pr,
17772
+ env,
17773
+ store,
17774
+ session,
17775
+ baseRef,
17776
+ backend
17777
+ });
17753
17778
  }
17754
17779
  }),
17755
17780
  defineTool4({
@@ -18236,7 +18261,12 @@ async function promoteLocalReview(review, remote, model) {
18236
18261
  author: localReviewAuthor(model)
18237
18262
  });
18238
18263
  const draft = await local.readDraft();
18239
- const comments = draft.comments.map((comment) => ({
18264
+ const remoteThreads = await remote.listThreads();
18265
+ const threadReplies = sessionIsWorkingTree ? [] : draft.comments.flatMap((comment) => {
18266
+ const thread = remoteThreads.find((candidate) => candidate.file === comment.file && candidate.line === comment.line && candidate.line !== undefined && !candidate.body.includes("Generated review by Diffpi using"));
18267
+ return thread ? [{ comment, thread }] : [];
18268
+ });
18269
+ const comments = draft.comments.filter(({ file, line }) => !threadReplies.some((reply) => reply.comment.file === file && reply.comment.line === line)).map((comment) => ({
18240
18270
  ...comment,
18241
18271
  body: withRemoteProvenance(comment.body, comment.author?.replace(/^Agent:\s*/, "") || model)
18242
18272
  }));
@@ -18252,7 +18282,7 @@ async function promoteLocalReview(review, remote, model) {
18252
18282
  const unpublished = unpublishedReviewComments(comments, known);
18253
18283
  if (unpublished.length > 0 || unpublishedBody)
18254
18284
  await remote.stage({ comments: unpublished, body: unpublishedBody });
18255
- const promotedReplies = await promoteLocalReplies(review, remote, publication, model, sessionIsWorkingTree);
18285
+ const promotedReplies = await promoteLocalReplies(review, remote, publication, model, sessionIsWorkingTree) + await promoteLocationReplies(remote, publication, threadReplies, model);
18256
18286
  return {
18257
18287
  publication,
18258
18288
  bodyFingerprints,
@@ -18262,6 +18292,31 @@ async function promoteLocalReview(review, remote, model) {
18262
18292
  promotedReplies
18263
18293
  };
18264
18294
  }
18295
+ async function promoteLocationReplies(remote, publication, replies, model) {
18296
+ let count = 0;
18297
+ for (const { comment, thread } of replies) {
18298
+ const parsed = parseReviewThreadAction(comment.body);
18299
+ const body = parsed.body.trim();
18300
+ const action = parsed.action;
18301
+ const fingerprint = reviewReplyFingerprint(thread.id, `${action ?? "reply"}\x00${body}`);
18302
+ if (publication.state.replies.includes(fingerprint))
18303
+ continue;
18304
+ const remoteBody = withRemoteProvenance(body, comment.author?.replace(/^Agent:\s*/, "") || model);
18305
+ if (body && !thread.replies?.some((reply) => reply === body || reply === remoteBody)) {
18306
+ await remote.reply({
18307
+ threadId: thread.id,
18308
+ body: remoteBody,
18309
+ resolve: false
18310
+ });
18311
+ }
18312
+ if (action && remote.setResolved)
18313
+ await remote.setResolved(thread.id, action === "resolve");
18314
+ publication.state.replies.push(fingerprint);
18315
+ await saveReviewPublicationState(publication.path, publication.state);
18316
+ count += 1;
18317
+ }
18318
+ return count;
18319
+ }
18265
18320
  async function promoteLocalReplies(review, remote, publication, model, workingTree) {
18266
18321
  const target = workingTree ? "uncommitted" : await reviewTargetId(review);
18267
18322
  const artifact = workingTree ? await latestThreadArtifact(review.cwd, review, target) : publication.state.overlayPath ?? await latestThreadArtifact(review.cwd, review, target);
package/dist/index.js CHANGED
@@ -1355,16 +1355,18 @@ function GitHubReviewBackend(vcs, number) {
1355
1355
  `body=${input.body}`
1356
1356
  ]);
1357
1357
  }
1358
- if (input.resolve) {
1359
- await ghChecked([
1360
- "api",
1361
- "graphql",
1362
- "-f",
1363
- `query=${GITHUB_RESOLVE_MUTATION}`,
1364
- "-f",
1365
- `threadId=${input.threadId}`
1366
- ]);
1367
- }
1358
+ if (input.resolve)
1359
+ await this.setResolved?.(input.threadId, true);
1360
+ },
1361
+ async setResolved(threadId, resolved) {
1362
+ await ghChecked([
1363
+ "api",
1364
+ "graphql",
1365
+ "-f",
1366
+ `query=${resolved ? GITHUB_RESOLVE_MUTATION : GITHUB_UNRESOLVE_MUTATION}`,
1367
+ "-f",
1368
+ `threadId=${threadId}`
1369
+ ]);
1368
1370
  },
1369
1371
  async publish(event) {
1370
1372
  const pending = await pendingReview();
@@ -1385,6 +1387,7 @@ var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$aft
1385
1387
  var GITHUB_ADD_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!,$line:Int!,$side:DiffSide!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,line:$line,side:$side}){thread{id}}}`;
1386
1388
  var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
1387
1389
  var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
1390
+ var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
1388
1391
 
1389
1392
  // src/review/gitlab-review-backend.ts
1390
1393
  function GitLabReviewBackend(vcs, number) {
@@ -1491,7 +1494,11 @@ function GitLabReviewBackend(vcs, number) {
1491
1494
  });
1492
1495
  }
1493
1496
  if (input.resolve)
1494
- await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=true`]);
1497
+ await this.setResolved?.(input.threadId, true);
1498
+ },
1499
+ async setResolved(threadId, resolved) {
1500
+ const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
1501
+ await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
1495
1502
  },
1496
1503
  async publish(event) {
1497
1504
  assertReviewEventSupported(vcs.provider, event);
@@ -1 +1 @@
1
- {"version":3,"file":"github-review-backend.d.ts","sourceRoot":"","sources":["../../src/review/github-review-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAEjE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CA2M/E;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,GACtB,MAAM,CAIR"}
1
+ {"version":3,"file":"github-review-backend.d.ts","sourceRoot":"","sources":["../../src/review/github-review-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE9C,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAEjE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CA4M/E;AAED,wBAAgB,8BAA8B,CAC5C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,GACtB,MAAM,CAIR"}
@@ -1 +1 @@
1
- {"version":3,"file":"gitlab-review-backend.d.ts","sourceRoot":"","sources":["../../src/review/gitlab-review-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE7D,OAAO,KAAK,EAAE,aAAa,EAAiB,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE7F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CA2H/E;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAM5F;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAYjE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAO1D"}
1
+ {"version":3,"file":"gitlab-review-backend.d.ts","sourceRoot":"","sources":["../../src/review/gitlab-review-backend.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAE7D,OAAO,KAAK,EAAE,aAAa,EAAiB,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE7F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CA+H/E;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAM5F;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAYjE;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAO1D"}
@@ -1,5 +1,5 @@
1
- export { dedupeFindings, findingSchema, findingsSchema, isLocalResponse, localResponseMarker, localReviewAuthor, reviewRecordName, reviewSlug, severitySchema, toReviewComments, withRemoteProvenance, yymmdd, } from './types';
2
- export type { Finding, LocalReviewBackendOptions, ReviewBackend, ReviewComment, ReviewDocInput, ReviewDraft, ReviewEvent, ReviewReply, ReviewSide, ReviewThreadArtifactOptions, ReviewThreadRecord, Severity, } from './types';
1
+ export { dedupeFindings, findingSchema, findingsSchema, isLocalResponse, localResponseMarker, parseReviewThreadAction, localReviewAuthor, reviewRecordName, reviewSlug, severitySchema, toReviewComments, withRemoteProvenance, yymmdd, } from './types';
2
+ export type { Finding, LocalReviewBackendOptions, ReviewBackend, ReviewComment, ReviewDocInput, ReviewDraft, ReviewEvent, ReviewReply, ReviewSide, ReviewThreadAction, ReviewThreadArtifactOptions, ReviewThreadRecord, Severity, } from './types';
3
3
  export { parseThreadArtifact, renderReviewDoc, renderThreadArtifact, upsertThreadReply } from './review-markdown';
4
4
  export { assertReviewEventSupported, createLocalReviewBackend, createRemoteReviewBackend, githubReviewSubmissionEndpoint, hasGitlabDraftNotes, parseGitlabDiffRefs, } from './review-backend';
5
5
  export type { GitlabDiffRefs } from './review-backend';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/review/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,MAAM,GACP,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,OAAO,EACP,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,cAAc,EACd,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,2BAA2B,EAC3B,kBAAkB,EAClB,QAAQ,GACT,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAClH,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,8BAA8B,EAC9B,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/review/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,aAAa,EACb,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,uBAAuB,EACvB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,oBAAoB,EACpB,MAAM,GACP,MAAM,SAAS,CAAC;AACjB,YAAY,EACV,OAAO,EACP,yBAAyB,EACzB,aAAa,EACb,aAAa,EACb,cAAc,EACd,WAAW,EACX,WAAW,EACX,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,2BAA2B,EAC3B,kBAAkB,EAClB,QAAQ,GACT,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAClH,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,yBAAyB,EACzB,8BAA8B,EAC9B,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -37,6 +37,7 @@ export interface ReviewBackend {
37
37
  readDraft(): Promise<ReviewDraft>;
38
38
  listThreads(): Promise<ReviewThreadRecord[]>;
39
39
  reply(input: ReviewReply): Promise<void>;
40
+ setResolved?(threadId: string, resolved: boolean): Promise<void>;
40
41
  publish(event: ReviewEvent): Promise<void>;
41
42
  }
42
43
  export interface LocalReviewBackendOptions {
@@ -100,6 +101,11 @@ export declare function yymmdd(date?: Date): string;
100
101
  export declare function reviewRecordName(target: string, date?: Date): string;
101
102
  export declare function localResponseMarker(threadId: string): string;
102
103
  export declare function isLocalResponse(body: string): boolean;
104
+ export type ReviewThreadAction = 'reopen' | 'resolve';
105
+ export declare function parseReviewThreadAction(body: string): {
106
+ action?: ReviewThreadAction;
107
+ body: string;
108
+ };
103
109
  export declare function dedupeFindings(findings: Finding[]): Finding[];
104
110
  export declare function toReviewComments(findings: Finding[], model?: string): ReviewComment[];
105
111
  export declare function withRemoteProvenance(body: string, model: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/review/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/review/types.ts"],"names":[],"mappings":"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"}
@@ -2852,6 +2852,15 @@ function localResponseMarker(threadId) {
2852
2852
  function isLocalResponse(body) {
2853
2853
  return /^<!-- diffpi(?:-local-response:[A-Za-z0-9_-]+)? -->\n/m.test(body);
2854
2854
  }
2855
+ function parseReviewThreadAction(body) {
2856
+ const match = body.match(/^\[(REOPEN|RESOLVE)\]\s*/i);
2857
+ if (!match)
2858
+ return { body };
2859
+ return {
2860
+ action: match[1].toLowerCase(),
2861
+ body: body.slice(match[0].length)
2862
+ };
2863
+ }
2855
2864
  function dedupeFindings(findings) {
2856
2865
  const rank = { BLOCKING: 3, CONSIDER: 2, NOTE: 1 };
2857
2866
  const byKey = new Map;
@@ -3190,16 +3199,18 @@ function GitHubReviewBackend(vcs, number) {
3190
3199
  `body=${input.body}`
3191
3200
  ]);
3192
3201
  }
3193
- if (input.resolve) {
3194
- await ghChecked([
3195
- "api",
3196
- "graphql",
3197
- "-f",
3198
- `query=${GITHUB_RESOLVE_MUTATION}`,
3199
- "-f",
3200
- `threadId=${input.threadId}`
3201
- ]);
3202
- }
3202
+ if (input.resolve)
3203
+ await this.setResolved?.(input.threadId, true);
3204
+ },
3205
+ async setResolved(threadId, resolved) {
3206
+ await ghChecked([
3207
+ "api",
3208
+ "graphql",
3209
+ "-f",
3210
+ `query=${resolved ? GITHUB_RESOLVE_MUTATION : GITHUB_UNRESOLVE_MUTATION}`,
3211
+ "-f",
3212
+ `threadId=${threadId}`
3213
+ ]);
3203
3214
  },
3204
3215
  async publish(event) {
3205
3216
  const pending = await pendingReview();
@@ -3220,6 +3231,7 @@ var GITHUB_THREADS_QUERY = `query($owner:String!,$repo:String!,$number:Int!,$aft
3220
3231
  var GITHUB_ADD_THREAD_MUTATION = `mutation($reviewId:ID!,$body:String!,$path:String!,$line:Int!,$side:DiffSide!){addPullRequestReviewThread(input:{pullRequestReviewId:$reviewId,body:$body,path:$path,line:$line,side:$side}){thread{id}}}`;
3221
3232
  var GITHUB_REPLY_MUTATION = `mutation($threadId:ID!,$body:String!){addPullRequestReviewThreadReply(input:{pullRequestReviewThreadId:$threadId,body:$body}){comment{id}}}`;
3222
3233
  var GITHUB_RESOLVE_MUTATION = `mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
3234
+ var GITHUB_UNRESOLVE_MUTATION = `mutation($threadId:ID!){unresolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}`;
3223
3235
 
3224
3236
  // src/review/gitlab-review-backend.ts
3225
3237
  function GitLabReviewBackend(vcs, number) {
@@ -3326,7 +3338,11 @@ function GitLabReviewBackend(vcs, number) {
3326
3338
  });
3327
3339
  }
3328
3340
  if (input.resolve)
3329
- await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=true`]);
3341
+ await this.setResolved?.(input.threadId, true);
3342
+ },
3343
+ async setResolved(threadId, resolved) {
3344
+ const endpoint = `${mergeRequestEndpoint()}/discussions/${encodeURIComponent(threadId)}`;
3345
+ await glabChecked(["api", "--method", "PUT", `${endpoint}?resolved=${resolved}`]);
3330
3346
  },
3331
3347
  async publish(event) {
3332
3348
  assertReviewEventSupported(vcs.provider, event);
@@ -3740,7 +3756,16 @@ function createReviewTools() {
3740
3756
  review.pr ? `PR/MR: #${review.pr.number} ${review.pr.url}` : "PR/MR: none",
3741
3757
  session ? `tuicr: ${session.slug} (${session.commentCount} comments)` : "tuicr: none"
3742
3758
  ].join(`
3743
- `), { ...review, env, store, session, baseRef, backend });
3759
+ `), {
3760
+ cwd: review.cwd,
3761
+ vcs: review.vcs,
3762
+ pr: review.pr,
3763
+ env,
3764
+ store,
3765
+ session,
3766
+ baseRef,
3767
+ backend
3768
+ });
3744
3769
  }
3745
3770
  }),
3746
3771
  defineTool4({
@@ -4227,7 +4252,12 @@ async function promoteLocalReview(review, remote, model) {
4227
4252
  author: localReviewAuthor(model)
4228
4253
  });
4229
4254
  const draft = await local.readDraft();
4230
- const comments = draft.comments.map((comment) => ({
4255
+ const remoteThreads = await remote.listThreads();
4256
+ const threadReplies = sessionIsWorkingTree ? [] : draft.comments.flatMap((comment) => {
4257
+ const thread = remoteThreads.find((candidate) => candidate.file === comment.file && candidate.line === comment.line && candidate.line !== undefined && !candidate.body.includes("Generated review by Diffpi using"));
4258
+ return thread ? [{ comment, thread }] : [];
4259
+ });
4260
+ const comments = draft.comments.filter(({ file, line }) => !threadReplies.some((reply) => reply.comment.file === file && reply.comment.line === line)).map((comment) => ({
4231
4261
  ...comment,
4232
4262
  body: withRemoteProvenance(comment.body, comment.author?.replace(/^Agent:\s*/, "") || model)
4233
4263
  }));
@@ -4243,7 +4273,7 @@ async function promoteLocalReview(review, remote, model) {
4243
4273
  const unpublished = unpublishedReviewComments(comments, known);
4244
4274
  if (unpublished.length > 0 || unpublishedBody)
4245
4275
  await remote.stage({ comments: unpublished, body: unpublishedBody });
4246
- const promotedReplies = await promoteLocalReplies(review, remote, publication, model, sessionIsWorkingTree);
4276
+ const promotedReplies = await promoteLocalReplies(review, remote, publication, model, sessionIsWorkingTree) + await promoteLocationReplies(remote, publication, threadReplies, model);
4247
4277
  return {
4248
4278
  publication,
4249
4279
  bodyFingerprints,
@@ -4253,6 +4283,31 @@ async function promoteLocalReview(review, remote, model) {
4253
4283
  promotedReplies
4254
4284
  };
4255
4285
  }
4286
+ async function promoteLocationReplies(remote, publication, replies, model) {
4287
+ let count = 0;
4288
+ for (const { comment, thread } of replies) {
4289
+ const parsed = parseReviewThreadAction(comment.body);
4290
+ const body = parsed.body.trim();
4291
+ const action = parsed.action;
4292
+ const fingerprint = reviewReplyFingerprint(thread.id, `${action ?? "reply"}\x00${body}`);
4293
+ if (publication.state.replies.includes(fingerprint))
4294
+ continue;
4295
+ const remoteBody = withRemoteProvenance(body, comment.author?.replace(/^Agent:\s*/, "") || model);
4296
+ if (body && !thread.replies?.some((reply) => reply === body || reply === remoteBody)) {
4297
+ await remote.reply({
4298
+ threadId: thread.id,
4299
+ body: remoteBody,
4300
+ resolve: false
4301
+ });
4302
+ }
4303
+ if (action && remote.setResolved)
4304
+ await remote.setResolved(thread.id, action === "resolve");
4305
+ publication.state.replies.push(fingerprint);
4306
+ await saveReviewPublicationState(publication.path, publication.state);
4307
+ count += 1;
4308
+ }
4309
+ return count;
4310
+ }
4256
4311
  async function promoteLocalReplies(review, remote, publication, model, workingTree) {
4257
4312
  const target = workingTree ? "uncommitted" : await reviewTargetId(review);
4258
4313
  const artifact = workingTree ? await latestThreadArtifact(review.cwd, review, target) : publication.state.overlayPath ?? await latestThreadArtifact(review.cwd, review, target);
@@ -1 +1 @@
1
- {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/tools/review.ts"],"names":[],"mappings":"AAGA,OAAO,EAAqC,KAAK,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAMzG,OAAO,EAuBL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAC;AAwEnB,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAExF;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAalE;AAID,wBAAgB,iBAAiB,IAAI,SAAS,cAAc,EAAE,CAyb7D;AAID,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,SAAS,kBAAkB,EAAE,GACrC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAwB/B"}
1
+ {"version":3,"file":"review.d.ts","sourceRoot":"","sources":["../../src/tools/review.ts"],"names":[],"mappings":"AAGA,OAAO,EAAqC,KAAK,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAMzG,OAAO,EAwBL,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACxB,MAAM,WAAW,CAAC;AAwEnB,wBAAgB,cAAc,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAExF;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAalE;AAID,wBAAgB,iBAAiB,IAAI,SAAS,cAAc,EAAE,CAkc7D;AAID,wBAAsB,uBAAuB,CAC3C,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,SAAS,kBAAkB,EAAE,GACrC,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAwB/B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@difflab/pi",
3
- "version": "0.3.0-rc.202609201610.cddc7d6",
3
+ "version": "0.3.0-rc.202609201720.4138cef",
4
4
  "description": "Tools and skills for the pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,7 +2,7 @@
2
2
 
3
3
  1. Parse an optional PR/MR id or URL, `--local`, and one status: `--comment`, `--approve`, `--request-changes`, or `--close`. The default is comment.
4
4
  2. Call `review_context` with the target and backend selection.
5
- 3. For `--local`, call `review_publish` with `local: true`. The tool reads the exact tuicr session, promotes its unpublished line comments to a pending forge review, follows the stored local overlay path, posts unpublished replies to their remote threads, and then publishes the selected status. Question replies remain open. The tool does not post an overall review comment.
5
+ 3. For `--local`, call `review_publish` with `local: true`. The tool reads the exact tuicr session. A local line comment on the same file and line as an existing remote thread is treated as a response, not a new comment. A leading `[REOPEN]` or `[RESOLVE]` changes the thread state; the marker is removed from the response body. Other unpublished line comments become new review comments. The tool follows the stored local overlay path, posts unpublished replies to their remote threads, and then publishes the selected status. Question replies remain open. The tool does not post an overall review comment.
6
6
  4. For a remote review, call `review_publish` without `local`. The existing pending forge comments already contain provenance, so the tool only publishes them with the selected status and no overall comment.
7
7
  5. Comment, approve, and request-changes mark a draft PR/MR ready before publication. Close publishes pending work as comment status and then closes the PR/MR. GitLab rejects request-changes because it has no equivalent review state.
8
- 6. Report promoted comment and reply counts, the public status, and the final PR/MR state. Publishing never merges.
8
+ 6. Report promoted comment and reply counts, the public status, and the final PR/MR state. Publishing never merges. If a response cannot be matched by file and line, report it as a new comment rather than silently attaching it to the wrong thread.