prreviewbuddy 0.49.0 → 0.50.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.50.1
4
+
5
+ **Copy and Ask Buddy look and behave as a pair.** On every issue and question, both icons are the
6
+ same grey at rest and share one soft highlight when you point at them. Copy no longer turns purple on
7
+ hover. Ask Buddy turns purple only while its menu is open. After you copy, the icon turns into a green
8
+ check for a moment and "Copied" appears just below it. It used to appear beside it, on top of Ask
9
+ Buddy. "Copy failed" shows in the same place.
10
+
11
+ The questions you can ask Buddy AI about a finding or a selection, and the wording that tells Buddy
12
+ which theme a question was asked in, now live in the shared review engine, so the browser extension
13
+ offers the same ones.
14
+
15
+ In a short window, the list of questions you can ask about a selection now scrolls instead of being
16
+ cut off at the bottom. Escape closes a menu again after the page has redrawn around it.
17
+
18
+ ## 0.50.0
19
+
20
+ **Every review says which commits it covers.** Beside the branch at the top of a review is its
21
+ commit count. Open it to see where your branch split off from the base, every commit in the review,
22
+ and a reminder that uncommitted changes are never included. A review is always the combined change
23
+ across all of those commits, not just the latest one.
24
+
25
+ **Help, in the workspace.** A new **Help** place in the sidebar explains what gets reviewed: how
26
+ the base sets the split point, why your local main and origin/main can give different reviews, and
27
+ how to choose between them. The commit count on a review links to it.
28
+
29
+ **Choosing between local main and origin/main tells you what each would review.** When the two are
30
+ different commits and you have not named a base, PR Review Buddy still asks which one you mean,
31
+ and now lists under each choice how many commits it would review and which ones.
32
+
33
+ **`prreviewbuddy review --help` works.** It used to say `review` does not take `--help`. Every
34
+ command now prints the help, which has a new section on what gets reviewed, with examples.
35
+
36
+ ## 0.49.2
37
+
38
+ **Buddy AI remembers which theme you asked in.** Your earlier questions now reach Buddy with the
39
+ theme you had open when you asked them, or a note that they were about the whole change. Asking
40
+ "what is risky here?" in one theme and then "how does that compare?" in another no longer reads to
41
+ Buddy as two questions about the same thing.
42
+
43
+ ## 0.49.1
44
+
45
+ **Buddy AI says what your question is about.** A line under **Ask Buddy AI** now reads **Asking
46
+ about: the whole change**, or the name of the theme you have open in the Review Path. Buddy always
47
+ sees the whole change; an open theme tells it where to focus, so "what is the risk here?" is
48
+ answered about that theme. The line used to be a small grey label beside the title, cut short, with
49
+ its meaning only in a tooltip. Theme names are now shown in full.
50
+
3
51
  ## 0.49.0
4
52
 
5
53
  **Change a review from the review itself.** The **⋯** menu at the top of a review now has
@@ -1876,6 +1876,11 @@ var AmbiguousRefError = class extends Error {
1876
1876
  divergence;
1877
1877
  /** Whether the name was named by the reviewer or discovered as the default. Changes the wording. */
1878
1878
  discovered;
1879
+ /**
1880
+ * What a review against each of the two would contain. Absent until `withReviewRanges` adds it,
1881
+ * which needs the head being reviewed and so cannot happen where the name is resolved.
1882
+ */
1883
+ ranges;
1879
1884
  constructor(detail) {
1880
1885
  super(describeAmbiguity(detail));
1881
1886
  this.name = "AmbiguousRefError";
@@ -1884,9 +1889,49 @@ var AmbiguousRefError = class extends Error {
1884
1889
  this.remote = detail.remote;
1885
1890
  if (detail.divergence) this.divergence = detail.divergence;
1886
1891
  this.discovered = detail.discovered;
1892
+ if (detail.ranges) this.ranges = detail.ranges;
1887
1893
  }
1888
1894
  };
1889
1895
  /**
1896
+ * The same refusal, now saying what choosing each ref would review.
1897
+ *
1898
+ * Counted from the split point, `git merge-base <ref> <head>`, and not as `<ref>..<head>`: the
1899
+ * review diffs from the merge-base, so this has to walk the same boundary or the refusal and the
1900
+ * review it leads to could disagree about which commits are in it. Added after resolution because
1901
+ * the resolver is shared with the review's own target, which has no head to measure against.
1902
+ */
1903
+ async function withReviewRanges(error, repoPath, head) {
1904
+ const [local, remote] = await Promise.all([reviewedCommits(repoPath, error.local.ref, head), reviewedCommits(repoPath, error.remote.ref, head)]);
1905
+ return new AmbiguousRefError({
1906
+ requested: error.requested,
1907
+ local: error.local,
1908
+ remote: error.remote,
1909
+ ...error.divergence ? { divergence: error.divergence } : {},
1910
+ discovered: error.discovered,
1911
+ ranges: {
1912
+ ...local ? { local } : {},
1913
+ ...remote ? { remote } : {}
1914
+ }
1915
+ });
1916
+ }
1917
+ async function reviewedCommits(repoPath, ref, head) {
1918
+ try {
1919
+ const split = (await git(repoPath, [
1920
+ "merge-base",
1921
+ ref,
1922
+ head
1923
+ ])).trim();
1924
+ if (!split) return void 0;
1925
+ return (await git(repoPath, [
1926
+ "rev-list",
1927
+ "--abbrev-commit",
1928
+ `${split}..${head}`
1929
+ ])).split("\n").filter(Boolean);
1930
+ } catch {
1931
+ return;
1932
+ }
1933
+ }
1934
+ /**
1890
1935
  * `refs/heads/feat/foo` and `refs/remotes/origin/feat/foo` are both `feat/foo`.
1891
1936
  *
1892
1937
  * Structural rather than asked of git: the first path segment under `refs/remotes/` is the
@@ -1996,10 +2041,19 @@ function describeAmbiguity(detail) {
1996
2041
  ...refs.map((one) => ` ${one.ref.padEnd(width)} ${one.sha.slice(0, 7)}`),
1997
2042
  ...detail.divergence ? ["", divergenceLine(name, shorthandOf(detail.remote.ref), detail.divergence)] : [],
1998
2043
  "",
2044
+ ...detail.ranges ? [SPLIT_POINT, ""] : [],
1999
2045
  "Name the one you mean:",
2000
- ...refs.map((one) => ` prreviewbuddy review --base ${shorthandOf(one.ref)}`)
2046
+ ...[[detail.local, detail.ranges?.local], [detail.remote, detail.ranges?.remote]].flatMap(([one, commits]) => [` prreviewbuddy review --base ${shorthandOf(one.ref)}`, ...commits ? [` ${rangeLine(commits)}`] : []])
2001
2047
  ].join("\n");
2002
2048
  }
2049
+ var SPLIT_POINT = "The base determines the split point. PR Review Buddy reviews the changes on your branch since that point.";
2050
+ /** "changes across", because a review is one combined diff and not a review of each commit. */
2051
+ function rangeLine(commits) {
2052
+ if (commits.length === 0) return "no commits: nothing on your branch is new against this base";
2053
+ const listed = commits.slice(0, 10).join(" ");
2054
+ const more = commits.length > 10 ? ` and ${commits.length - 10} more` : "";
2055
+ return `changes across ${commits.length} commit${commits.length === 1 ? "" : "s"}: ${listed}${more}`;
2056
+ }
2003
2057
  /**
2004
2058
  * How a person spells this ref: the shortest form that still says which one.
2005
2059
  *
@@ -2176,7 +2230,7 @@ async function buildChangeSet(options) {
2176
2230
  disagreement(files.length, base.display, options.expected);
2177
2231
  const blobShas = await headBlobShas(repoPath);
2178
2232
  for (const file of files) file.sha = blobShas.get(file.name) ?? "";
2179
- const { subjects, authors } = await commitLog(repoPath, base.exact && !await refExists(repoPath, mergeBase) ? "HEAD" : `${mergeBase}..HEAD`);
2233
+ const { subjects, authors, commits } = await commitLog(repoPath, base.exact && !await refExists(repoPath, mergeBase) ? "HEAD" : `${mergeBase}..HEAD`);
2180
2234
  return {
2181
2235
  data: {
2182
2236
  pullRequest: {
@@ -2205,7 +2259,8 @@ async function buildChangeSet(options) {
2205
2259
  includesUncommitted: false,
2206
2260
  isLinkedWorktree,
2207
2261
  checkoutName: basename(repoPath),
2208
- authors
2262
+ authors,
2263
+ commits
2209
2264
  };
2210
2265
  }
2211
2266
  /**
@@ -2316,21 +2371,22 @@ async function commitLog(repoPath, range) {
2316
2371
  const out = await git(repoPath, [
2317
2372
  "log",
2318
2373
  "--reverse",
2319
- "--format=%an%x1f%s",
2374
+ "--format=%H%x1f%an%x1f%s",
2320
2375
  range
2321
2376
  ]);
2322
2377
  const subjects = [];
2323
2378
  const counts = /* @__PURE__ */ new Map();
2379
+ const commits = [];
2324
2380
  for (const line of out.split("\n")) {
2325
- const separator = line.indexOf("");
2326
- if (separator === -1) continue;
2327
- const name = line.slice(0, separator).trim();
2328
- const subject = line.slice(separator + 1).trim();
2329
- if (subject) subjects.push(subject);
2330
- if (name) counts.set(name, (counts.get(name) ?? 0) + 1);
2381
+ const [sha, name, subject] = line.split("");
2382
+ if (!sha || name === void 0) continue;
2383
+ commits.push(sha.trim());
2384
+ if (subject?.trim()) subjects.push(subject.trim());
2385
+ if (name.trim()) counts.set(name.trim(), (counts.get(name.trim()) ?? 0) + 1);
2331
2386
  }
2332
2387
  return {
2333
2388
  subjects,
2389
+ commits: commits.reverse(),
2334
2390
  authors: [...counts].map(([name, commits]) => ({
2335
2391
  name,
2336
2392
  commits
@@ -2746,8 +2802,14 @@ async function peel(repoPath, named) {
2746
2802
  async function resolveTarget(input) {
2747
2803
  const root = await repositoryRoot(input.repoPath);
2748
2804
  if (input.commitRef) return commitTarget(root, input.commitRef);
2749
- if (input.base) await verifyBase(root, input.base);
2750
- else await verifyDefaultBase(root);
2805
+ try {
2806
+ if (input.base) await verifyBase(root, input.base);
2807
+ else await verifyDefaultBase(root);
2808
+ } catch (error) {
2809
+ if (!(error instanceof AmbiguousRefError)) throw error;
2810
+ const head = await headToMeasure(root, input);
2811
+ throw head ? await withReviewRanges(error, root, head) : error;
2812
+ }
2751
2813
  const named = input.base ? { base: input.base } : {};
2752
2814
  if (input.prUrl) {
2753
2815
  if (!input.resolveRequest) throw new TargetError(`${input.prUrl} is a pull request, but nothing here can look one up. Check the request out locally and review the branch instead.`);
@@ -2896,6 +2958,23 @@ async function verifyBase(root, base) {
2896
2958
  throw new TargetError(`There is nothing called \`${base}\` in this repository to compare against. Tried ${candidateRefs(base).map((one) => `\`${one}\``).join(" and ")}. Run \`git fetch\` if it is on the remote and this clone has not seen it yet.`);
2897
2959
  }
2898
2960
  /**
2961
+ * The commit the review would be of, as far as it is known before the target is resolved.
2962
+ *
2963
+ * A pull request's head is not known until the forge is asked, and the refusal exists to come
2964
+ * before that, so it gets none and its refusal lists no ranges. A named branch is resolved rather
2965
+ * than handed to git as typed, because a name that is itself ambiguous would be settled by git's own
2966
+ * preference, which is the guess this module refuses to make.
2967
+ */
2968
+ async function headToMeasure(root, input) {
2969
+ if (input.prUrl) return void 0;
2970
+ if (!input.branchRef) return "HEAD";
2971
+ try {
2972
+ return (await resolveBranchRef(root, input.branchRef))?.sha;
2973
+ } catch {
2974
+ return;
2975
+ }
2976
+ }
2977
+ /**
2899
2978
  * The same check for the base nobody named, which is the one the reported failure came down.
2900
2979
  *
2901
2980
  * A bare `prreviewbuddy review` has no base to verify, so this ran nothing and the refusal landed
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $t as matchingWorkspaceIds, Bn as writeAgentPreference, Bt as progressSteps, Ct as forgeResolver, E as telemetryUploadConsent, Fn as git, Ft as loadJob, Gn as agentById, Gt as followedRefName, Hn as STORE_ROOT, Ht as clearClaim, In as resolveAgent, J as readMarker, Kt as groupByLineage, Ln as resolveModel, Lt as PHASES, Mn as resolveTarget, Nn as displayRef, Ot as checkReview, Pt as isTerminal, Qt as loadWorkspace, S as startJob, T as recordTelemetryUploadConsent, Un as AGENT_IDS, Ut as driverGone, Vn as CONFIG_PATH, Vt as purposeOf, Wn as DEFAULT_AGENT_ID, Wt as isClaimed, X as modelHelpLines, Xt as lineageKeyFor, Y as removeWorktree, Z as relativeTime, _t as MIN_WORKSPACE_PORT, a as wasBlocked, an as reviewedRepositories, b as reanalyseReview, bt as statedWorkspacePort, en as mutateWorkspace, et as queueForReview, ft as BUILD_VERSION, g as runningJobs, gt as MAX_WORKSPACE_PORT, h as liveUpdateFor, ht as DEFAULT_WORKSPACE_PORT, i as liveJobsFor, it as ensureServer, jt as allJobs, l as mergeUpdateInto, lt as stopServer, m as UpdateAlreadyRunningError, n as deleteReview$1, nt as bootstrapUrl, on as summarise, ot as reviewsUrl, pt as PACKAGE_NAME, q as MANAGED_ROOT, qn as detectAgents, rn as recentWorkspaces, tn as positionInLineage, tt as readIndexToken, u as updateReview, ut as workspaceUrl, v as recordKissRun, vt as parseWorkspacePort, w as record$1, x as runJob, xt as writePortPreference, zn as readAgentPreference } from "./delete_review-BBjXPfMV.js";
2
+ import { $t as matchingWorkspaceIds, Bn as writeAgentPreference, Bt as progressSteps, Ct as forgeResolver, E as telemetryUploadConsent, Fn as git, Ft as loadJob, Gn as agentById, Gt as followedRefName, Hn as STORE_ROOT, Ht as clearClaim, In as resolveAgent, J as readMarker, Kt as groupByLineage, Ln as resolveModel, Lt as PHASES, Mn as resolveTarget, Nn as displayRef, Ot as checkReview, Pt as isTerminal, Qt as loadWorkspace, S as startJob, T as recordTelemetryUploadConsent, Un as AGENT_IDS, Ut as driverGone, Vn as CONFIG_PATH, Vt as purposeOf, Wn as DEFAULT_AGENT_ID, Wt as isClaimed, X as modelHelpLines, Xt as lineageKeyFor, Y as removeWorktree, Z as relativeTime, _t as MIN_WORKSPACE_PORT, a as wasBlocked, an as reviewedRepositories, b as reanalyseReview, bt as statedWorkspacePort, en as mutateWorkspace, et as queueForReview, ft as BUILD_VERSION, g as runningJobs, gt as MAX_WORKSPACE_PORT, h as liveUpdateFor, ht as DEFAULT_WORKSPACE_PORT, i as liveJobsFor, it as ensureServer, jt as allJobs, l as mergeUpdateInto, lt as stopServer, m as UpdateAlreadyRunningError, n as deleteReview$1, nt as bootstrapUrl, on as summarise, ot as reviewsUrl, pt as PACKAGE_NAME, q as MANAGED_ROOT, qn as detectAgents, rn as recentWorkspaces, tn as positionInLineage, tt as readIndexToken, u as updateReview, ut as workspaceUrl, v as recordKissRun, vt as parseWorkspacePort, w as record$1, x as runJob, xt as writePortPreference, zn as readAgentPreference } from "./delete_review-BIXqUnpx.js";
3
3
  import { basename, dirname, join, resolve } from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -115,6 +115,7 @@ function parse(argv) {
115
115
  name: "error",
116
116
  message: unknownCommand(head)
117
117
  };
118
+ if (rest.includes("--help") || rest.includes("-h")) return { name: "help" };
118
119
  const values = {};
119
120
  const flags = /* @__PURE__ */ new Set();
120
121
  const positional = [];
@@ -1064,6 +1065,45 @@ running the same command again carries each one on from where it got to.
1064
1065
 
1065
1066
  Reviews can also be deleted from the list in your browser, one at a time or a whole branch at once.
1066
1067
 
1068
+ What gets reviewed
1069
+
1070
+ A review is the combined change between a base and the branch you review, as one diff. It is not
1071
+ just the latest commit. The base determines the split point (git's merge-base of the base and your
1072
+ branch), and PR Review Buddy reviews the changes on your branch since that point.
1073
+
1074
+ main: A---B---C
1075
+ \\
1076
+ feature: D---E
1077
+
1078
+ prreviewbuddy review --base main reviews D and E together
1079
+
1080
+ C is on main but not on your branch, so it is not part of the review, and the review does not show
1081
+ its changes as removed. Commits already in the base are never reviewed again.
1082
+
1083
+ When your local main and origin/main are different commits, which one you name moves the split
1084
+ point:
1085
+
1086
+ origin/main: A---B
1087
+ \\
1088
+ local main: C---D
1089
+ \\
1090
+ feature: E
1091
+
1092
+ --base refs/heads/main reviews E
1093
+ --base origin/main reviews C, D and E together
1094
+
1095
+ Without --base, the repository's default branch is the base. If its local and remote copies are
1096
+ different commits, PR Review Buddy asks which one you mean and says what each would review.
1097
+ Uncommitted changes are never included: a review is a fixed snapshot of your commits.
1098
+
1099
+ Common examples
1100
+
1101
+ prreviewbuddy review --base refs/heads/main
1102
+ everything on this branch since it left your local main
1103
+ prreviewbuddy review --base origin/main
1104
+ everything on this branch not yet on origin/main
1105
+ prreviewbuddy review --commit <commit> one commit, against its parent
1106
+
1067
1107
  There is no account and no API key. PR Review Buddy uses a coding agent installed on your machine
1068
1108
  and that agent's existing authentication, and nothing here ever asks you for a credential.
1069
1109
 
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as compactQueue, A as createPrState, An as processDiscussion, At as reviewedShaOf, B as toneOf, Bn as writeAgentPreference, Bt as progressSteps, C as readEvents, Cn as readQueue, D as announceJob, Dn as issuesOutstanding, Dt as checkCodeFreshness, E as telemetryUploadConsent, En as isQuestionOutstanding, Et as fillFileUrlTemplate, F as markAllRead, Fn as git, Ft as loadJob, G as extractJsonText, Gt as followedRefName, H as withUsageRecorded, Hn as STORE_ROOT, I as markRead, It as saveJob, J as readMarker, Jn as AgentCancelledError, Jt as indexReviewGroups, K as ReviewBeingDeletedError, Kn as agentFor, Kt as groupByLineage, L as pruneNotifications, Ln as resolveModel, M as eventName, Mt as endedAt, N as headline, Nt as fail, O as branchOf, On as questionsOutstanding, Ot as checkReview, P as listNotifications, Pn as isCommitReview, Pt as isTerminal, Q as activateQueued, Qt as loadWorkspace, R as readNotification, Rn as clearAgentPreference, Rt as isLiveJob, Sn as queueStamp, St as claimRequestUrl, T as recordTelemetryUploadConsent, Tn as isIssueOutstanding, Tt as remoteHost, U as EXPLAIN_SIMPLY_PROMPT, Un as AGENT_IDS, V as unreadCount$1, Vt as purposeOf, W as asString, Wt as isClaimed, Y as removeWorktree, Yn as AgentUnavailableError, Yt as latestKissRun, Z as relativeTime$1, Zt as lineagePosition, _ as agentEnvOf, _n as parseLifecycle, _t as MIN_WORKSPACE_PORT, a as wasBlocked, at as readServerRecord, bn as queueAddMany, bt as statedWorkspacePort, c as RefreshUnavailableError, cn as workspaceRevision, ct as spawnServer, d as refreshPrContext, dn as activityLabel, dt as writeServerRecord, en as mutateWorkspace, f as isUnchanged, fn as isLifecycle, ft as BUILD_VERSION, gn as offersLifecycle, gt as MAX_WORKSPACE_PORT, h as liveUpdateFor, hn as lifecyclePill, ht as DEFAULT_WORKSPACE_PORT, i as liveJobsFor, in as reviewedCommit, j as createPrUpdate, jn as describeAuthorship, jt as allJobs, k as subjectOf, kn as reviewerDispositions, kt as followedBranch, l as mergeUpdateInto, ln as workspaceStamps, m as UpdateAlreadyRunningError, mn as lifecycleChip, mt as feedbackUrl, n as deleteReview, nn as previousKissResult, o as discardJob, p as askCheckout, pn as lifecycleAction, q as MANAGED_ROOT, qt as indexKeptIds, r as lineageIds, rn as recentWorkspaces, rt as clearServerRecord, s as FETCH_TIMEOUT_MS, sn as touchWorkspace, st as serverEntryExists, t as deleteLineage, tt as readIndexToken, u as updateReview, un as LIFECYCLES, vn as sectionOf, vt as parseWorkspacePort, w as record, wn as doneVerb, wt as forgeFor, x as runJob, xn as queueRemove, xt as writePortPreference, y as startKissJob, yn as queueAdd, yt as resolveWorkspacePort, z as removeNotification, zn as readAgentPreference, zt as phasesFor } from "./delete_review-BBjXPfMV.js";
1
+ import { $ as compactQueue, A as createPrState, An as processDiscussion, At as reviewedShaOf, B as toneOf, Bn as writeAgentPreference, Bt as progressSteps, C as readEvents, Cn as readQueue, D as announceJob, Dn as issuesOutstanding, Dt as checkCodeFreshness, E as telemetryUploadConsent, En as isQuestionOutstanding, Et as fillFileUrlTemplate, F as markAllRead, Fn as git, Ft as loadJob, G as extractJsonText, Gt as followedRefName, H as withUsageRecorded, Hn as STORE_ROOT, I as markRead, It as saveJob, J as readMarker, Jn as AgentCancelledError, Jt as indexReviewGroups, K as ReviewBeingDeletedError, Kn as agentFor, Kt as groupByLineage, L as pruneNotifications, Ln as resolveModel, M as eventName, Mt as endedAt, N as headline, Nt as fail, O as branchOf, On as questionsOutstanding, Ot as checkReview, P as listNotifications, Pn as isCommitReview, Pt as isTerminal, Q as activateQueued, Qt as loadWorkspace, R as readNotification, Rn as clearAgentPreference, Rt as isLiveJob, Sn as queueStamp, St as claimRequestUrl, T as recordTelemetryUploadConsent, Tn as isIssueOutstanding, Tt as remoteHost, U as EXPLAIN_SIMPLY_PROMPT, Un as AGENT_IDS, V as unreadCount$1, Vt as purposeOf, W as asString, Wt as isClaimed, Y as removeWorktree, Yn as AgentUnavailableError, Yt as latestKissRun, Z as relativeTime$1, Zt as lineagePosition, _ as agentEnvOf, _n as parseLifecycle, _t as MIN_WORKSPACE_PORT, a as wasBlocked, at as readServerRecord, bn as queueAddMany, bt as statedWorkspacePort, c as RefreshUnavailableError, cn as workspaceRevision, ct as spawnServer, d as refreshPrContext, dn as activityLabel, dt as writeServerRecord, en as mutateWorkspace, f as isUnchanged, fn as isLifecycle, ft as BUILD_VERSION, gn as offersLifecycle, gt as MAX_WORKSPACE_PORT, h as liveUpdateFor, hn as lifecyclePill, ht as DEFAULT_WORKSPACE_PORT, i as liveJobsFor, in as reviewedCommit, j as createPrUpdate, jn as describeAuthorship, jt as allJobs, k as subjectOf, kn as reviewerDispositions, kt as followedBranch, l as mergeUpdateInto, ln as workspaceStamps, m as UpdateAlreadyRunningError, mn as lifecycleChip, mt as feedbackUrl, n as deleteReview, nn as previousKissResult, o as discardJob, p as askCheckout, pn as lifecycleAction, q as MANAGED_ROOT, qt as indexKeptIds, r as lineageIds, rn as recentWorkspaces, rt as clearServerRecord, s as FETCH_TIMEOUT_MS, sn as touchWorkspace, st as serverEntryExists, t as deleteLineage, tt as readIndexToken, u as updateReview, un as LIFECYCLES, vn as sectionOf, vt as parseWorkspacePort, w as record, wn as doneVerb, wt as forgeFor, x as runJob, xn as queueRemove, xt as writePortPreference, y as startKissJob, yn as queueAdd, yt as resolveWorkspacePort, z as removeNotification, zn as readAgentPreference, zt as phasesFor } from "./delete_review-BIXqUnpx.js";
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { createHash, timingSafeEqual } from "node:crypto";
@@ -208,6 +208,30 @@ function resolveSectionId(run, sectionId) {
208
208
  return run.result.sections.some((section) => section.id === sectionId) ? namespaced(run.id, sectionId) : sectionId;
209
209
  }
210
210
  //#endregion
211
+ //#region ../../packages/review-harness/src/domain/ai_review/ask_scope.ts
212
+ /**
213
+ * Where an earlier question was asked, which the question alone does not say.
214
+ *
215
+ * "What is risky here?" in one theme followed by "and how does that compare?" in another reads, as
216
+ * bare text, as two questions about the same thing. `undefined` is a question recorded before
217
+ * scopes were, and gets no label: calling it "the whole change" would state something nobody
218
+ * recorded. A theme that has since gone, because the review was updated, is said to have gone.
219
+ */
220
+ function askedWhere(sections, sectionId) {
221
+ if (sectionId === void 0) return null;
222
+ if (sectionId === null) return "(asked about the whole change)";
223
+ const section = sections.find((s) => s.id === sectionId);
224
+ return section ? `(asked while reading the "${section.title}" theme)` : "(asked while reading a theme that is no longer in the review)";
225
+ }
226
+ /**
227
+ * The theme the reviewer has open, named rather than used to filter what the model sees: they
228
+ * routinely ask "does this interact with X", where X is in another theme.
229
+ */
230
+ function readingThemeLine(sections, sectionId) {
231
+ const section = sectionId === null ? void 0 : sections.find((s) => s.id === sectionId);
232
+ return section ? `The reviewer is currently reading the "${section.title}" theme. Answer with that in view, but do not ignore the rest of the change if it bears on the question.` : null;
233
+ }
234
+ //#endregion
211
235
  //#region ../../packages/review-harness/src/domain/utils/html.ts
212
236
  function escapeHtml$1(value) {
213
237
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -371,6 +395,7 @@ var PATHS = {
371
395
  reviews: "<path d=\"M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01\"/>",
372
396
  notifications: "<path d=\"M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9\"/><path d=\"M13.7 21a2 2 0 0 1-3.4 0\"/>",
373
397
  settings: "<circle cx=\"12\" cy=\"12\" r=\"3\"/><path d=\"M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z\"/>",
398
+ help: "<circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3\"/><path d=\"M12 17h.01\"/>",
374
399
  feedback: "<path d=\"m22 2-7 20-4-9-9-4z\"/><path d=\"M22 2 11 13\"/>",
375
400
  stop: "<path d=\"M12 2v10\"/><path d=\"M18.4 6.6a9 9 0 1 1-12.8 0\"/>",
376
401
  restart: "<path d=\"M21 12a9 9 0 1 1-2.64-6.36\"/><path d=\"M21 3v6h-6\"/>"
@@ -444,6 +469,12 @@ var WORKSPACE_PLACES = [
444
469
  href: "/settings",
445
470
  icon: "settings",
446
471
  label: "Settings"
472
+ },
473
+ {
474
+ place: "help",
475
+ href: "/help",
476
+ icon: "help",
477
+ label: "Help"
447
478
  }
448
479
  ];
449
480
  /**
@@ -766,9 +797,10 @@ var FINDING_COPY_CLASS = "pr-helper-finding-copy";
766
797
  * buttons, where an icon alone read as decoration. In the heading it is on its own, and a word
767
798
  * would give a utility the weight of the title beside it. What it does is said by a hover
768
799
  * tooltip (`data-tip`, the same CSS tooltip the pills use, not a native `title`) and by its
769
- * accessible name. "Copied" and "Copy failed" still show as words, because a picture cannot say
770
- * that the click did something. The resting name is kept on the button so the reset returns to
771
- * "Copy finding" rather than to a chat reply's "Copy message".
800
+ * accessible name. After a click it shows a green check and says "Copied" (or "Copy failed") in
801
+ * the bubble below it, the words used everywhere else, but not inline: beside it is Ask Buddy. The
802
+ * resting name and tip are kept on the button so the reset returns to "Copy finding" rather than to
803
+ * a chat reply's "Copy message".
772
804
  */
773
805
  function renderFindingCopySource(source, kind) {
774
806
  const name = kind === "issue" ? "Copy finding" : "Copy question";
@@ -781,11 +813,12 @@ function renderFindingCopySource(source, kind) {
781
813
  /** Where a finding's button keeps its resting word and name. Absent on a chat reply's. */
782
814
  var IDLE_LABEL_ATTR = "data-copy-idle-label";
783
815
  var IDLE_ARIA_ATTR = "data-copy-idle-aria";
816
+ var IDLE_TIP_ATTR = "data-copy-idle-tip";
784
817
  function button(handle, idle) {
785
818
  const { icon } = STATES$1.idle;
786
819
  const label = idle?.label ?? STATES$1.idle.label;
787
820
  const aria = idle?.aria ?? STATES$1.idle.aria;
788
- const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}"` : "";
821
+ const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}" ${IDLE_TIP_ATTR}="${escapeHtml$1(idle.tip)}"` : "";
789
822
  const control = `<button type="button" class="${idle ? `${MESSAGE_COPY_CLASS} ${FINDING_COPY_CLASS}` : MESSAGE_COPY_CLASS}" ${handle}${remembered}${idle ? ` data-tip="${escapeHtml$1(idle.tip)}"` : ""} aria-label="${escapeHtml$1(aria)}"><span class="${MESSAGE_COPY_CLASS}-icon" aria-hidden="true">${svg(icon)}</span><span class="${MESSAGE_COPY_CLASS}-label"${label ? "" : " hidden"}>${escapeHtml$1(label)}</span></button>`;
790
823
  return idle ? control : `<div class="${MESSAGE_ACTIONS_CLASS}">${control}</div>`;
791
824
  }
@@ -806,8 +839,7 @@ function svg(paths) {
806
839
  * closes and positions exactly as that one does. The panel is empty here and filled when it opens,
807
840
  * because it lists the reviewer's saved prompts and those change without the page re-rendering.
808
841
  *
809
- * Workspace only. The extension renders these cards too, and there is no Buddy pane behind a card
810
- * there for this to open, so `ReviewTabOptions.askBuddy` gates it and the extension never sets it.
842
+ * The host decides where the answer appears; `ReviewTabOptions.askBuddy` says the host has wired it.
811
843
  */
812
844
  var ASK_BUDDY_LABEL = "Ask Buddy";
813
845
  /** Four-point sparkle, the conventional mark for "ask the AI". currentColor, like Copy beside it. */
@@ -5011,6 +5043,43 @@ function renderSubject(changeSet) {
5011
5043
  <span>${against}</span>`;
5012
5044
  }
5013
5045
  /**
5046
+ * Which commits this review is of, opened from the bar beside what it compares.
5047
+ *
5048
+ * Beside `.prb-change-ref` rather than inside it, because that element clips to keep the bar one
5049
+ * line and a panel inside it would be cut off. The pill's own label is a fact (how many commits),
5050
+ * so the bar gains information even closed. Facts only, and about this review: the general
5051
+ * explanation is the Help page's, linked when there is a server to answer the link.
5052
+ *
5053
+ * The count comes from the stored commit list, or from the authors' counts for a review made before
5054
+ * the list was recorded, which then says how many without listing them. Neither: no count at all,
5055
+ * never a zero.
5056
+ */
5057
+ function renderRange(changeSet, live) {
5058
+ const commits = changeSet.commits;
5059
+ const count = commits?.length ?? changeSet.authors?.reduce((sum, author) => sum + author.commits, 0);
5060
+ const counted = count !== void 0 && count > 0 ? count : void 0;
5061
+ const plural = (n) => `${n} commit${n === 1 ? "" : "s"}`;
5062
+ const sha = (value) => `<code>${escapeHtml$1(value.slice(0, 7))}</code>`;
5063
+ const lines = [];
5064
+ if (changeSet.targetType === "commit") {
5065
+ const parent = changeSet.parentSha;
5066
+ const against = !parent ? "the empty tree" : `its ${(changeSet.parentCount ?? 1) > 1 ? "first parent" : "parent"} ${sha(parent)}`;
5067
+ lines.push(`The change commit ${sha(changeSet.data.pullRequest.head.sha)} made, compared against ${against}.`);
5068
+ } else {
5069
+ lines.push(changeSet.mergeBase ? `Compared against <code>${escapeHtml$1(changeSet.baseRef)}</code>, from where this branch split off at ${sha(changeSet.mergeBase)}.` : `Compared against <code>${escapeHtml$1(changeSet.baseRef)}</code>.`);
5070
+ if (commits && commits.length > 0) {
5071
+ const more = commits.length > 10 ? ` and ${commits.length - 10} more` : "";
5072
+ lines.push(`Changes across ${plural(commits.length)}: ${commits.slice(0, 10).map(sha).join(" ")}${more}.`);
5073
+ } else if (counted) lines.push(`Changes across ${plural(counted)}.`);
5074
+ }
5075
+ lines.push(changeSet.includesUncommitted ? "This review also read uncommitted changes in the working tree." : "Uncommitted changes are never part of a review.");
5076
+ if (live) lines.push("<a href=\"/help#what-gets-reviewed\">How review ranges work</a>");
5077
+ return `<details class="prb-range" data-range-details>
5078
+ <summary class="prb-range-summary">${changeSet.targetType === "commit" ? "1 commit" : counted ? plural(counted) : "Range"}</summary>
5079
+ <div class="prb-range-panel">${lines.map((line) => `<p>${line}</p>`).join("")}</div>
5080
+ </details>`;
5081
+ }
5082
+ /**
5014
5083
  * The review's own bar, below the global strip.
5015
5084
  *
5016
5085
  * Everything here is about this review and nothing else is: the strip above carries the workspace's
@@ -5031,6 +5100,7 @@ function renderReviewBar(view, freshness) {
5031
5100
  ${changeSet.includesUncommitted ? "<span class=\"prb-file-status\">+ working tree</span>" : ""}
5032
5101
  ${renderAuthor(view)}
5033
5102
  </span>
5103
+ ${renderRange(changeSet, Boolean(view.token))}
5034
5104
  <div class="prb-header-stats">
5035
5105
  <span>${fileCount} file${fileCount === 1 ? "" : "s"}</span>
5036
5106
  <span><span class="prb-add">+${additions}</span> <span class="prb-del">&minus;${deletions}</span></span>
@@ -5635,8 +5705,10 @@ function renderAsk(view, sections, paused = false) {
5635
5705
  tabindex="0"
5636
5706
  ></div>
5637
5707
  <div class="prb-ask-header">
5638
- <h2>Ask Buddy AI</h2>
5639
- <span class="prb-ask-scope" data-ask-scope>whole change</span>
5708
+ <hgroup class="prb-ask-heading">
5709
+ <h2>Ask Buddy AI</h2>
5710
+ <p class="prb-ask-scope">Asking about: <span data-ask-scope>the whole change</span></p>
5711
+ </hgroup>
5640
5712
  ${renderFocusToggle("buddy", "Buddy AI")}
5641
5713
  <button type="button" class="prb-ask-hide" data-ask-toggle aria-controls="prb-ask" aria-label="Hide Buddy AI">${ASK_HIDE_ICON}</button>
5642
5714
  </div>
@@ -7900,6 +7972,87 @@ function defaultFetchDeps() {
7900
7972
  };
7901
7973
  }
7902
7974
  //#endregion
7975
+ //#region ../../packages/review-harness/src/workspace/help_page.ts
7976
+ /** The diagrams are text on purpose: they are the same ones `prreviewbuddy --help` prints. */
7977
+ var TOPICS = [{
7978
+ id: "what-gets-reviewed",
7979
+ title: "What gets reviewed",
7980
+ body: `<p>A review is the combined change between a base and the branch you review, shown as one
7981
+ diff. It is not just the latest commit. The base determines the split point: the commit where
7982
+ your branch left it (git calls this the merge-base). PR Review Buddy reviews the changes on your
7983
+ branch since that point.</p>
7984
+ <pre class="prb-help-diagram">main: A---B---C
7985
+ \\
7986
+ feature: D---E</pre>
7987
+ <p>Reviewing <code>feature</code> against <code>main</code> covers D and E together. C is on
7988
+ main but not on your branch, so it is not part of the review, and the review does not show its
7989
+ changes as removed. Commits already in the base are never reviewed again.</p>
7990
+
7991
+ <h3>Local main and origin/main</h3>
7992
+ <p>Your local <code>main</code> can hold commits that are not on <code>origin/main</code> yet,
7993
+ such as a merge you have not pushed. When the two are different commits, the one you choose
7994
+ moves the split point.</p>
7995
+ <pre class="prb-help-diagram">origin/main: A---B
7996
+ \\
7997
+ local main: C---D
7998
+ \\
7999
+ feature: E</pre>
8000
+ <ul>
8001
+ <li>Against local <code>main</code>, the review covers E.</li>
8002
+ <li>Against <code>origin/main</code>, it covers C, D and E together.</li>
8003
+ </ul>
8004
+ <p>When nobody names a base and the two differ, PR Review Buddy does not guess. It asks which
8005
+ one you mean and says which commits each would review.</p>
8006
+
8007
+ <h3>Uncommitted changes</h3>
8008
+ <p>Never included. A review is a fixed snapshot of your commits, made in a checkout of its own,
8009
+ so work you have not committed is not part of it.</p>
8010
+
8011
+ <h3>Choosing a base</h3>
8012
+ <p>The base is chosen when the review is made, in the terminal:</p>
8013
+ <ul>
8014
+ <li><code>prreviewbuddy review --base refs/heads/main</code> for everything on your branch since it left your local main</li>
8015
+ <li><code>prreviewbuddy review --base origin/main</code> for everything on your branch not yet on origin/main</li>
8016
+ <li><code>prreviewbuddy review --commit &lt;commit&gt;</code> for one commit, against its parent</li>
8017
+ </ul>
8018
+ <p>Every review shows its own range. The commit count beside the branch name at the top of a
8019
+ review opens its split point and the commits it covers.</p>`
8020
+ }];
8021
+ function renderHelpPage(view) {
8022
+ return `<!doctype html>
8023
+ <html lang="en"${themeAttribute(view.theme)}>
8024
+ <head>
8025
+ <meta charset="utf-8">
8026
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8027
+ <title>Help · PR Review Buddy</title>
8028
+ <link rel="icon" type="image/png" href="/assets/icon128.png">
8029
+ <link rel="stylesheet" href="/assets/sidebar.css">
8030
+ <link rel="stylesheet" href="/assets/shell.css">
8031
+ </head>
8032
+ <body class="prb-index-body" data-way-back="${escapeHtml$1(wayBackSentence())}">
8033
+ ${renderGlobalBar({
8034
+ current: "help",
8035
+ unread: view.unreadNotifications ?? 0,
8036
+ driving: view.driving
8037
+ })}
8038
+ ${renderContextBar("Help", "")}
8039
+ <div class="prb-shell">
8040
+ ${renderWorkspaceRail({
8041
+ current: "help",
8042
+ unread: view.unreadNotifications ?? 0
8043
+ })}
8044
+ <main class="prb-index prb-help">
8045
+ ${TOPICS.map((topic) => `<section class="prb-help-topic" id="${topic.id}">
8046
+ <h2>${escapeHtml$1(topic.title)}</h2>
8047
+ ${topic.body}
8048
+ </section>`).join("")}
8049
+ </main>
8050
+ </div>
8051
+ <script src="/assets/help.js" type="module"><\/script>
8052
+ </body>
8053
+ </html>`;
8054
+ }
8055
+ //#endregion
7903
8056
  //#region ../../packages/review-harness/src/workspace/notifications_page.ts
7904
8057
  function renderNotificationsPage(view) {
7905
8058
  const unread = view.notifications.filter((notification) => notification.readAt === void 0).length;
@@ -8234,7 +8387,8 @@ function extractAnswer(raw) {
8234
8387
  * and wrongly.
8235
8388
  */
8236
8389
  function buildPrompt$2(workspace, question, sectionId, cwd) {
8237
- const section = workspace.session.result?.sections.find((s) => s.id === sectionId);
8390
+ const sections = workspace.session.result?.sections ?? [];
8391
+ const reading = readingThemeLine(sections, sectionId);
8238
8392
  const recent = workspace.transcript.slice(-4);
8239
8393
  return [
8240
8394
  SYSTEM_PROMPT$1,
@@ -8246,11 +8400,14 @@ function buildPrompt$2(workspace, question, sectionId, cwd) {
8246
8400
  "---",
8247
8401
  "DIFF:",
8248
8402
  workspace.diff,
8249
- ...section ? ["---", `The reviewer is currently reading the "${section.title}" theme. Answer with that in view, but do not ignore the rest of the change if it bears on the question.`] : [],
8403
+ ...reading ? ["---", reading] : [],
8250
8404
  ...recent.length > 0 ? [
8251
8405
  "---",
8252
8406
  "EARLIER IN THIS CONVERSATION:",
8253
- recent.map((entry) => `Q: ${entry.question}\nA: ${entry.answer}`).join("\n\n")
8407
+ recent.map((entry) => {
8408
+ const where = askedWhere(sections, entry.sectionId);
8409
+ return `Q${where ? ` ${where}` : ""}: ${entry.question}\nA: ${entry.answer}`;
8410
+ }).join("\n\n")
8254
8411
  ] : [],
8255
8412
  "---",
8256
8413
  `QUESTION: ${question}`
@@ -9263,6 +9420,7 @@ async function handle(req, res, options) {
9263
9420
  if (url.pathname === "/open") return handleOpen(req, res, url.searchParams, options.indexToken);
9264
9421
  if (url.pathname === "/settings") return void handleSettings(req, res, url.searchParams, options.indexToken);
9265
9422
  if (url.pathname === "/notifications") return void handleNotifications(req, res, url.searchParams, options.indexToken);
9423
+ if (url.pathname === "/help") return void handleHelp(req, res, url.searchParams, options.indexToken);
9266
9424
  if (url.pathname === "/notifications/open") return handleNotificationOpen(req, res, url.searchParams, options.indexToken);
9267
9425
  if (url.pathname === "/notifications/recent") return handleRecentNotifications(req, res, url.searchParams, options.indexToken);
9268
9426
  if (url.pathname === "/pin") return handlePin(req, res, options.indexToken);
@@ -9472,6 +9630,18 @@ async function handleSettings(req, res, params, indexToken) {
9472
9630
  }), indexCookieHeader(indexToken));
9473
9631
  }
9474
9632
  /**
9633
+ * Help. Static, but still behind the index's credential like every other workspace place: its
9634
+ * rail and strip carry the unread count, which is this machine's state.
9635
+ */
9636
+ function handleHelp(req, res, params, indexToken) {
9637
+ if (!indexAuthorised(req, params, indexToken)) return refuseIndex(req, res);
9638
+ sendHtml(res, 200, renderHelpPage({
9639
+ unreadNotifications: unreadCount$1(),
9640
+ driving: isDriving(),
9641
+ theme: readPreferences().theme
9642
+ }), indexCookieHeader(indexToken));
9643
+ }
9644
+ /**
9475
9645
  * Whether the caller is a browser navigating, rather than something reading the API.
9476
9646
  *
9477
9647
  * `Accept` is the only signal available and browsers are consistent about it: a navigation asks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prreviewbuddy",
3
- "version": "0.49.0",
3
+ "version": "0.50.1",
4
4
  "description": "Review a branch or pull request in an isolated checkout, from your terminal, with the coding agent you already have installed.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://prreviewbuddy.com",
package/static/help.js ADDED
@@ -0,0 +1 @@
1
+ import{c as e,t}from"./chunk-BeL0x-wt.js";t(`/restart`),e(window,()=>window.location.reload());