diffprism 1.4.1 → 1.5.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.
package/README.md CHANGED
@@ -79,7 +79,7 @@ Reviews are **one per repo**: opening a review for a repo that already has one u
79
79
  |------|---------|
80
80
  | `annotate` | Post findings inline on the diff; `warning` flags the session for attention |
81
81
  | `get_review_comments` | Read every thread on the session; `awaiting_reply` narrows to unanswered ones |
82
- | `reply` | Reply to a thread answer the reviewer's question on a line |
82
+ | `reply` | Reply to a thread, then keep listening for what the reviewer does next |
83
83
  | `wait_for_comments` | Block until the reviewer writes something the agent hasn't answered |
84
84
  | `get_review_state` | Session status, attention and new-changes flags, and annotations |
85
85
  | `get_user_focus` | What file/line the user is currently viewing in the browser |
@@ -89,7 +89,7 @@ Reviews are **one per repo**: opening a review for a repo that already has one u
89
89
 
90
90
  In a local review, **Ask agent now** in a line's comment form asks the agent while you're still reviewing: the agent's wait for your decision ends with your question, it answers in the thread, and goes back to waiting.
91
91
 
92
- PR reviews are opened with `diffprism review <PR URL>` or the dashboard — not by `open_review` — and your AI then works inside them with the tools above. Click a line to ask the agent about it; an agent listening with `wait_for_comments` answers in the thread. Nothing starts an agent for you: if none has picked a question up, the thread says so and names the session to ask Claude Code about.
92
+ PR reviews are opened with `diffprism review <PR URL>` or the dashboard — not by `open_review` — and your AI then works inside them with the tools above. Click a line to ask the agent about it; an agent listening with `wait_for_comments` answers in the thread, and stays listening for your next question until you tell it to stop. Nothing starts an agent for you: if none has picked a question up, the thread says so and names the session to ask Claude Code about. That one prompt is enough — the agent is handed the checkout, branch, PR and the code you asked about along with the question.
93
93
 
94
94
  When you're done, **Approve**, **Request changes** or **Comment** from the bar at the bottom: DiffPrism posts it to GitHub as a pull request review. Your threads are a conversation with the agent, so none of them goes to GitHub unless you tick it; a ticked thread posts your opening message as an inline review comment. The token comes from `GITHUB_TOKEN`, `gh auth token`, or `~/.diffprism/config.json`.
95
95
 
package/dist/bin.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-RFDSC6MC.js";
6
6
  import {
7
7
  demo
8
- } from "./chunk-GF455BBV.js";
8
+ } from "./chunk-4JLGL3O6.js";
9
9
  import {
10
10
  COMMIT_GATE_DIFF_REF,
11
11
  DEFAULT_DIFF_REF,
@@ -26,7 +26,7 @@ import {
26
26
  recordError,
27
27
  startGlobalServer,
28
28
  submitReviewToServer
29
- } from "./chunk-YGUHES4P.js";
29
+ } from "./chunk-M5NJBIOV.js";
30
30
  import {
31
31
  getDiff
32
32
  } from "./chunk-GGK6TIQZ.js";
@@ -477,7 +477,7 @@ This should happen automatically after significant changes. You don't need the u
477
477
  - **\`dismissed\`** \u2014 the reviewer closed it without deciding. Ask before continuing.
478
478
  - If \`postReviewAction\` is \`"commit"\` \u2014 commit the changes. If \`"commit_and_pr"\` \u2014 commit and open a PR.
479
479
  4. If it returns \`status: "timed_out"\`, the reviewer is still reading. Keep waiting with \`mcp__diffprism__get_review_result\` (\`wait: true\`).
480
- 5. If it returns \`status: "reviewer_asked"\`, the reviewer asked you something before deciding. Answer each thread with \`mcp__diffprism__reply\` (its \`id\` is the \`annotation_id\`) \u2014 and change the code if that's what they asked for. Then wait again with \`mcp__diffprism__get_review_result\` (\`wait: true\`), which can return \`reviewer_asked\` again.
480
+ 5. If it returns \`status: "reviewer_asked"\`, the reviewer asked you something before deciding. Answer each thread with \`mcp__diffprism__reply\` (its \`id\` is the \`annotation_id\`) \u2014 and change the code if that's what they asked for. \`reply\` goes back to waiting by itself and returns what happened next as \`next\`: another question (\`reviewer_asked\`) to answer the same way, the decision (\`decided\`), or \`timed_out\` \u2014 then wait with \`mcp__diffprism__get_review_result\` (\`wait: true\`).
481
481
 
482
482
  **While a review is open, wait for it.** Don't ask the user whether they've finished, and don't move on to other work \u2014 their decision is the answer, and it arrives through the tool. Asking in the terminal splits the conversation in two and the decision gets lost between them.
483
483
 
@@ -514,11 +514,11 @@ Pull requests are opened by the user \u2014 \`diffprism review <PR URL>\` or "Re
514
514
 
515
515
  The reviewer can also ask you questions on lines of the PR. Hold that conversation in the dashboard, not the terminal:
516
516
 
517
- 1. \`mcp__diffprism__wait_for_comments\` \u2014 blocks until the reviewer writes something you haven't answered, then returns those threads.
518
- 2. \`mcp__diffprism__reply\` \u2014 answer each thread, passing its \`annotation_id\`.
519
- 3. Wait again. On \`timed_out\`, nothing new was said \u2014 keep waiting until the user tells you to stop.
517
+ 1. \`mcp__diffprism__wait_for_comments\` \u2014 blocks until the reviewer writes something you haven't answered, then returns those threads. Each carries \`hunk\`, the code it's on, and the response carries \`review\`: the checkout on disk (\`projectPath\`), the branch, and the PR. That's enough to start answering \u2014 you don't need to look the review up first. If \`localRepoConnected\` is false there's no clone here; answer from the hunk and the PR.
518
+ 2. \`mcp__diffprism__reply\` \u2014 answer a thread, passing its \`annotation_id\`. It goes back to listening by itself and returns what happened next as \`next\`: more questions to answer the same way, or \`timed_out\` \u2014 then call \`wait_for_comments\` again.
519
+ 3. Keep going until the user tells you to stop. Answering is not the end of the conversation; the reviewer's next question only reaches you if you're still listening.
520
520
 
521
- When no agent has read a question, the dashboard tells the reviewer so, and to ask you to answer their DiffPrism comments on a session id. When they do, run this loop with that \`session_id\`.
521
+ When no agent has read a question, the dashboard tells the reviewer so, and to ask you to answer their DiffPrism comments on a session id. When they do, call \`wait_for_comments\` with that \`session_id\` \u2014 it returns at once with what's waiting \u2014 and run this loop.
522
522
 
523
523
  A PR review and a working-copy review can be open for the same clone at once. If a tool reports more than one session, pass the \`session_id\` of the one you mean.
524
524
 
@@ -542,8 +542,8 @@ A PR review and a working-copy review can be open for the same clone at once. If
542
542
  |------|---------|
543
543
  | \`annotate\` | Post one or more findings. \`warning\` flags the session for attention. |
544
544
  | \`get_review_comments\` | Every thread on the review; \`awaiting_reply\` narrows to threads waiting for an answer. |
545
- | \`reply\` | Reply to a thread \u2014 answer the reviewer's question or follow up on a finding. |
546
- | \`wait_for_comments\` | Block until the reviewer writes something you haven't answered. |
545
+ | \`reply\` | Reply to a thread, then keep listening and return what happens next. |
546
+ | \`wait_for_comments\` | Block until the reviewer writes something you haven't answered; returns it with the code and where the review is. |
547
547
  | \`get_review_state\` | Session status, attention and new-changes flags, and annotations. |
548
548
  | \`get_user_focus\` | What the user is currently looking at. |
549
549
  | \`get_pr_context\` | PR overview: metadata, briefing, file list, local clone status. |
@@ -721,7 +721,7 @@ async function setupInteractive(flags) {
721
721
  }
722
722
  async function runDemo(dev) {
723
723
  console.log("");
724
- const { demo: demo2 } = await import("./demo-F47GIDSV.js");
724
+ const { demo: demo2 } = await import("./demo-5UQ7CLKT.js");
725
725
  await demo2({ dev });
726
726
  }
727
727
  async function setupBatch(flags) {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ensureServer,
3
3
  submitReviewToServer
4
- } from "./chunk-YGUHES4P.js";
4
+ } from "./chunk-M5NJBIOV.js";
5
5
  import {
6
6
  parseDiff
7
7
  } from "./chunk-GGK6TIQZ.js";
@@ -111,7 +111,7 @@ import fs2 from "fs";
111
111
  import path2 from "path";
112
112
  import { fileURLToPath } from "url";
113
113
  function builtAt() {
114
- return true ? 1789619886717 : null;
114
+ return true ? 1789699543800 : null;
115
115
  }
116
116
  function getBuildInfo() {
117
117
  let dir = path2.dirname(fileURLToPath(import.meta.url));
@@ -141,7 +141,7 @@ function describeVersion(version) {
141
141
  var ISSUES_NEW_URL = "https://github.com/CodeJonesW/diffprism/issues/new";
142
142
  var MAX_ERROR_CHARS = 1500;
143
143
  function currentVersion() {
144
- return true ? "1.4.1" : "0.0.0-dev";
144
+ return true ? "1.5.0" : "0.0.0-dev";
145
145
  }
146
146
  function describeEnvironment() {
147
147
  return {
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  demo
3
- } from "./chunk-GF455BBV.js";
4
- import "./chunk-YGUHES4P.js";
3
+ } from "./chunk-4JLGL3O6.js";
4
+ import "./chunk-M5NJBIOV.js";
5
5
  import "./chunk-GGK6TIQZ.js";
6
6
  import "./chunk-DHCVZGHE.js";
7
7
  import "./chunk-JSBRDJBE.js";
@@ -14,7 +14,7 @@ import {
14
14
  recordError,
15
15
  submitReviewToServer,
16
16
  waitForDecision
17
- } from "./chunk-YGUHES4P.js";
17
+ } from "./chunk-M5NJBIOV.js";
18
18
  import {
19
19
  getDiff
20
20
  } from "./chunk-GGK6TIQZ.js";
@@ -81,11 +81,56 @@ async function readThreads(serverInfo, sessionId) {
81
81
  const { annotations } = await response.json();
82
82
  return annotations.map((a) => ({ ...a, awaitingReply: awaitingAgent(a) }));
83
83
  }
84
- function reviewerAskedResult(err, waitWith) {
84
+ async function readReview(serverInfo, sessionId) {
85
+ let response;
86
+ try {
87
+ response = await fetch(
88
+ `http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/payload`
89
+ );
90
+ } catch {
91
+ return null;
92
+ }
93
+ if (!response.ok) {
94
+ return null;
95
+ }
96
+ const { payload, projectPath } = await response.json();
97
+ return {
98
+ review: {
99
+ sessionId,
100
+ projectPath,
101
+ localRepoConnected: !projectPath.startsWith("github:"),
102
+ branch: payload.metadata.currentBranch,
103
+ pr: payload.metadata.githubPr ?? null,
104
+ title: payload.metadata.title
105
+ },
106
+ diffSet: payload.diffSet
107
+ };
108
+ }
109
+ function hunkFor(diffSet, annotation) {
110
+ const file = diffSet.files.find((f) => f.path === annotation.file);
111
+ if (!file) {
112
+ return null;
113
+ }
114
+ return file.hunks.find(
115
+ (hunk) => annotation.side === "old" ? annotation.line >= hunk.oldStart && annotation.line < hunk.oldStart + hunk.oldLines : annotation.line >= hunk.newStart && annotation.line < hunk.newStart + hunk.newLines
116
+ ) ?? null;
117
+ }
118
+ function answerableThreads(context, threads) {
119
+ return {
120
+ sessionId: context.review.sessionId,
121
+ review: context.review,
122
+ threads: threads.map((thread) => ({
123
+ ...thread,
124
+ awaitingReply: true,
125
+ hunk: hunkFor(context.diffSet, thread)
126
+ }))
127
+ };
128
+ }
129
+ async function reviewerAskedResult(serverInfo, err, waitWith) {
130
+ const context = await readReview(serverInfo, err.sessionId);
85
131
  return jsonResult({
86
132
  status: "reviewer_asked",
87
- sessionId: err.sessionId,
88
- threads: err.threads.map((t) => ({ ...t, awaitingReply: true })),
133
+ ...context ? answerableThreads(context, err.threads) : { sessionId: err.sessionId, threads: err.threads.map((t) => ({ ...t, awaitingReply: true })) },
89
134
  message: `The reviewer asked you something before deciding. Answer each thread with reply (session_id: ${err.sessionId}, annotation_id from threads) \u2014 change the code too if that's what they asked for. Then ${waitWith}. The review stays open; the decision still comes.`
90
135
  });
91
136
  }
@@ -176,7 +221,7 @@ function createMcpServer() {
176
221
  });
177
222
  } catch (err) {
178
223
  if (err instanceof ReviewerAskedError) {
179
- return reviewerAskedResult(err, `wait for the decision with get_review_result (session_id: ${err.sessionId}, wait: true)`);
224
+ return await reviewerAskedResult(serverInfo, err, `wait for the decision with get_review_result (session_id: ${err.sessionId}, wait: true)`);
180
225
  }
181
226
  if (err instanceof ReviewTimeoutError) {
182
227
  return jsonResult({
@@ -217,7 +262,7 @@ ${REPORT_HINT}`);
217
262
  return jsonResult(await waitForDecision(serverInfo, sessionId, Math.min(timeout ?? 300, 600) * 1e3));
218
263
  } catch (err) {
219
264
  if (err instanceof ReviewerAskedError) {
220
- return reviewerAskedResult(err, "call get_review_result with wait: true again");
265
+ return await reviewerAskedResult(serverInfo, err, "call get_review_result with wait: true again");
221
266
  }
222
267
  if (err instanceof ReviewTimeoutError) {
223
268
  return jsonResult({
@@ -382,14 +427,17 @@ ${REPORT_HINT}`);
382
427
  );
383
428
  server.tool(
384
429
  "reply",
385
- "Reply to a thread on an open review \u2014 answer the reviewer's question, or follow up on a finding. The reply appears under the thread in the dashboard straight away.",
430
+ "Reply to a thread on an open review \u2014 answer the reviewer's question, or follow up on a finding. The reply appears under the thread in the dashboard straight away, and then this goes back to listening and returns whatever happens next: the reviewer's next question, their decision, or `timed_out`. Stay in that loop until they tell you to stop.",
386
431
  {
387
432
  ...targetParams,
388
433
  annotation_id: z.string().describe("The thread to reply to (an annotation id from get_review_comments or wait_for_comments)"),
389
434
  body: z.string().describe("Your reply"),
390
- source_agent: z.string().optional().describe("Who is replying, e.g. 'pr-reviewer'")
435
+ source_agent: z.string().optional().describe("Who is replying, e.g. 'pr-reviewer'"),
436
+ then_wait: z.boolean().optional().describe(
437
+ "Keep listening after replying (default true). With false, returns as soon as the reply is posted \u2014 use it when you have several threads to answer and want to post them all first."
438
+ )
391
439
  },
392
- async ({ session_id, repo_path, annotation_id, body, source_agent }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
440
+ async ({ session_id, repo_path, annotation_id, body, source_agent, then_wait }) => withSession({ session_id, repo_path }, async ({ serverInfo, sessionId }) => {
393
441
  const response = await fetch(
394
442
  `http://localhost:${serverInfo.httpPort}/api/reviews/${sessionId}/annotations/${annotation_id}/replies`,
395
443
  {
@@ -402,7 +450,33 @@ ${REPORT_HINT}`);
402
450
  if (!response.ok) {
403
451
  return toolError(`Error: ${data.error ?? `server returned ${response.status}`}`);
404
452
  }
405
- return jsonResult({ sessionId, annotationId: annotation_id, replyId: data.replyId });
453
+ const posted = { sessionId, annotationId: annotation_id, replyId: data.replyId };
454
+ if (then_wait === false) {
455
+ return jsonResult(posted);
456
+ }
457
+ try {
458
+ const result = await waitForDecision(serverInfo, sessionId, DEFAULT_WAIT_MS);
459
+ return jsonResult({ ...posted, next: { status: "decided", result } });
460
+ } catch (err) {
461
+ if (err instanceof ReviewerAskedError) {
462
+ const context = await readReview(serverInfo, sessionId);
463
+ return jsonResult({
464
+ ...posted,
465
+ next: context ? { status: "reviewer_asked", ...answerableThreads(context, err.threads) } : { status: "reviewer_asked", sessionId, threads: err.threads }
466
+ });
467
+ }
468
+ if (err instanceof ReviewTimeoutError) {
469
+ return jsonResult({
470
+ ...posted,
471
+ next: {
472
+ status: "timed_out",
473
+ sessionId,
474
+ message: "Your reply is posted and the reviewer hasn't said anything since. Keep listening: wait_for_comments in a PR review, or get_review_result with wait: true if you're waiting for their decision. Don't ask them in the terminal."
475
+ }
476
+ });
477
+ }
478
+ throw err;
479
+ }
406
480
  })
407
481
  );
408
482
  server.tool(
@@ -422,7 +496,10 @@ ${REPORT_HINT}`);
422
496
  }
423
497
  const waiting = threads.filter((t) => t.awaitingReply);
424
498
  if (waiting.length > 0) {
425
- return jsonResult({ sessionId, threads: waiting });
499
+ const context = await readReview(serverInfo, sessionId);
500
+ return jsonResult(
501
+ context ? answerableThreads(context, waiting) : { sessionId, threads: waiting }
502
+ );
426
503
  }
427
504
  if (Date.now() - start >= maxWaitMs) {
428
505
  return jsonResult({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diffprism",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "Local-first code review tool for agent-generated code changes",
6
6
  "bin": {