prreviewbuddy 0.71.1 → 0.73.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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.73.0
4
+
5
+ **KISS runs now count as reviews in usage telemetry.** With telemetry on, a KISS run sends the same
6
+ two events a standard review does, `review_started` and `review_finished`, with the same agent,
7
+ outcome and duration. Until now it sent neither, so somebody who only ran KISS looked as if they
8
+ had installed PR Review Buddy and never used it. Both review events now carry one more field,
9
+ `reviewType`, which is one of two words, `standard` or `kiss`: the kind of analysis, and nothing
10
+ about your code, your repository or what the analysis said. It is the only field added. Updating a
11
+ review with new commits still sends nothing. `prreviewbuddy config set telemetry-upload off` and
12
+ `PRB_TELEMETRY=off` stop these exactly as before.
13
+
14
+ ## 0.72.1
15
+
16
+ **Local usage logs count themes on the first read.** The `workspace_opened` log entry waited for
17
+ the review to be marked Reviewed rather than for the analysis to finish, so it rarely fired before
18
+ a reading session ended and the session summary recorded zero themes. It now waits only for the
19
+ analysis. Nothing about what is sent changes: neither event leaves the machine.
20
+
21
+ ## 0.72.0
22
+
23
+ **Both explanations stay on the card.** Ask Buddy on an issue or question now offers
24
+ **Explain (technical)** and **Explain (simple)**, and either one keeps its answer under the finding,
25
+ labelled **Explanation · Technical** or **Explanation · Simple**. A card holds one explanation: run
26
+ the other kind and it replaces the one there, while both answers stay in Buddy AI. If a new
27
+ explanation fails or is stopped, the card keeps the one it had. The same two names appear wherever
28
+ Ask Buddy offers them, including on a selection.
29
+
30
+ **Ask more follows up on the explanation you were reading.** If another tab replaces a card's
31
+ explanation after you press Ask more, your question still goes with the one you were reading.
32
+
33
+ **Simple explanations kept by 0.71 carry over.** They appear on their cards as
34
+ Explanation · Simple, and Ask more and updates use them as before.
35
+
36
+ ## 0.71.2
37
+
38
+ **Buddy AI's opening line reads plainly.** Before your first question, Buddy AI now says "Answers
39
+ come only from the review, the diff and the repository on disk."
40
+
3
41
  ## 0.71.1
4
42
 
5
43
  **`prreviewbuddy` on its own is a front door now, not the manual.** It says what PR Review Buddy is,
@@ -3277,12 +3277,44 @@ function describeAuthorship(changeSet, pullRequest) {
3277
3277
  }
3278
3278
  //#endregion
3279
3279
  //#region ../../packages/review-harness/src/domain/ai_review/review_session.ts
3280
+ /**
3281
+ * The Ask Buddy actions whose answer is kept on the finding's card: `explain-detail` is
3282
+ * Explain (technical), `explain` is Explain (simple). The ids are the shared action ids in
3283
+ * selection_prompts.ts, which predate the labels.
3284
+ */
3285
+ var EXPLANATION_ACTIONS = ["explain-detail", "explain"];
3286
+ function isExplanationAction(value) {
3287
+ return EXPLANATION_ACTIONS.includes(value);
3288
+ }
3289
+ function explanationActionOf(explanation) {
3290
+ return explanation.action ?? "explain";
3291
+ }
3280
3292
  /** The explanation of this finding, if the reviewer has asked for one. */
3281
- function simpleExplanationOf(session, kind, id) {
3282
- return session.simpleExplanations?.find((entry) => entry.kind === kind && entry.id === id);
3293
+ function explanationOf(session, kind, id) {
3294
+ return session.findingExplanations?.find((entry) => entry.kind === kind && entry.id === id);
3295
+ }
3296
+ /**
3297
+ * A session as stored by 0.71.0 to 0.71.2, which kept explanations under `simpleExplanations`, read
3298
+ * as this version stores it. Those were all Explain (simple), which an entry without an action
3299
+ * already means, so they move across unchanged. Where both lists name a finding the newer list's
3300
+ * entry wins: a later version wrote it. A session without the old list is returned as it was.
3301
+ */
3302
+ function withLegacyExplanationsMoved(session) {
3303
+ const legacy = session.simpleExplanations;
3304
+ if (legacy === void 0) return session;
3305
+ const { simpleExplanations: _moved, ...rest } = session;
3306
+ const newer = session.findingExplanations ?? [];
3307
+ const merged = [...(Array.isArray(legacy) ? legacy : []).filter((entry) => !explanationOf(session, entry.kind, entry.id)), ...newer];
3308
+ return {
3309
+ ...rest,
3310
+ ...merged.length ? { findingExplanations: merged } : {}
3311
+ };
3283
3312
  }
3284
- /** The list with this finding's explanation replaced, never a second one beside it. */
3285
- function withSimpleExplanation(entries, explanation) {
3313
+ /**
3314
+ * The list with this finding's explanation replaced, never a second one beside it. One slot per
3315
+ * finding whichever action wrote it: the newest explanation is the card's.
3316
+ */
3317
+ function withExplanation(entries, explanation) {
3286
3318
  return [...(entries ?? []).filter((entry) => !(entry.kind === explanation.kind && entry.id === explanation.id)), explanation];
3287
3319
  }
3288
3320
  /**
@@ -4431,12 +4463,12 @@ var WORKSPACES_DIR = join(STORE_ROOT, "workspaces");
4431
4463
  * Named so that a long run that rewrites the session, an update above all, can put these back from
4432
4464
  * the copy on disk when it saves: the reviewer may have answered a question or dismissed a finding
4433
4465
  * in the minutes it took. `handleState` writes the first two and nothing else; `handleAsk` writes
4434
- * `simpleExplanations`, and only when the reviewer asked for one.
4466
+ * `findingExplanations`, and only when the reviewer asked for one.
4435
4467
  */
4436
4468
  var REVIEWER_SESSION_FIELDS = [
4437
4469
  "answeredQuestionIds",
4438
4470
  "dismissedIssues",
4439
- "simpleExplanations"
4471
+ "findingExplanations"
4440
4472
  ];
4441
4473
  /**
4442
4474
  * A session a long run is about to write, with the reviewer's own fields taken from disk instead.
@@ -4668,7 +4700,13 @@ function deleteWorkspace(id) {
4668
4700
  }
4669
4701
  function loadWorkspace(id) {
4670
4702
  try {
4671
- return JSON.parse(readFileSync(workspacePath(id), "utf8"));
4703
+ const stored = JSON.parse(readFileSync(workspacePath(id), "utf8"));
4704
+ if (!stored?.session || typeof stored.session !== "object") return stored;
4705
+ const session = withLegacyExplanationsMoved(stored.session);
4706
+ return session === stored.session ? stored : {
4707
+ ...stored,
4708
+ session
4709
+ };
4672
4710
  } catch {
4673
4711
  return null;
4674
4712
  }
@@ -13273,6 +13311,7 @@ function project(record) {
13273
13311
  cliVersion: record.cliVersion,
13274
13312
  platform: record.platform,
13275
13313
  agent: event.agent,
13314
+ reviewType: event.reviewType,
13276
13315
  occurredAt: record.at
13277
13316
  };
13278
13317
  return event.event === "review_finished" ? {
@@ -13351,7 +13390,7 @@ var LOG_PATH = join(STORE_ROOT, "events.jsonl");
13351
13390
  * disable is a different act from sending to a server, and the consent that suits one does not
13352
13391
  * carry to the other, so `events.jsonl` keeps recording the full event union either way and the
13353
13392
  * question asked on first run is about the wire. Twenty events are written here; three of them,
13354
- * carrying eight fields between them, are the most that is ever sent.
13393
+ * carrying nine fields between them, are the most that is ever sent.
13355
13394
  */
13356
13395
  function record(event) {
13357
13396
  try {
@@ -13693,10 +13732,11 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
13693
13732
  contextId: context.contextId
13694
13733
  };
13695
13734
  saveJob(job);
13696
- if (job.reviewType !== "kiss") record({
13735
+ record({
13697
13736
  event: "review_started",
13698
13737
  reviewId: workspaceId,
13699
13738
  agent: agentName(job.env.agentId),
13739
+ reviewType: reviewKindOf(job),
13700
13740
  changedFiles: totalChangedFiles(context.changeSet.data),
13701
13741
  linesChanged: context.changeSet.data.files.reduce((total, file) => total + (file.additions ?? 0) + (file.deletions ?? 0), 0),
13702
13742
  analysedFiles: Math.max(0, context.changeSet.data.files.length - context.inventoryOnlyFiles.length)
@@ -13777,11 +13817,11 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
13777
13817
  * finished it. That is the number worth having: it is what somebody actually waited.
13778
13818
  */
13779
13819
  function finished(job, workspaceId, startedAt, ok, now) {
13780
- if (job.reviewType === "kiss") return;
13781
13820
  record({
13782
13821
  event: "review_finished",
13783
13822
  reviewId: workspaceId,
13784
13823
  agent: agentName(job.env.agentId),
13824
+ reviewType: reviewKindOf(job),
13785
13825
  ok,
13786
13826
  durationMs: now() - startedAt
13787
13827
  });
@@ -13831,6 +13871,10 @@ function recordAnalysisRuntime(job, workspaceId, from, durationMs, completedAt,
13831
13871
  * named ran under Claude. Recording those as unknown would invent a fourth agent out of the
13832
13872
  * oldest reviews on disk.
13833
13873
  */
13874
+ /** A job with no `reviewType` predates KISS and is a standard review. */
13875
+ function reviewKindOf(job) {
13876
+ return job.reviewType === "kiss" ? "kiss" : "standard";
13877
+ }
13834
13878
  function agentName(id) {
13835
13879
  const resolved = id ?? DEFAULT_AGENT_ID;
13836
13880
  return isAgentId(resolved) ? resolved : "other";
@@ -16708,4 +16752,4 @@ function lineageIds(id) {
16708
16752
  return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
16709
16753
  }
16710
16754
  //#endregion
16711
- export { MAX_WORKSPACE_PORT as $, isClaimed as $n, resolveBase as $r, markReadForReview as $t, relativeTime as A, isWorktreeHeld as An, parseLifecycle as Ar, partFiles as At, readIndexToken as B, reviewedShaOf as Bn, issuesOutstanding as Br, isArchitectureId as Bt, track as C, originRemote as Cn, activityAttributes as Cr, mapRuntimes as Ct, endRun as D, MANAGED_ROOT as Dn, lifecycleItemLabel as Dr, ARCHITECTURE_DIR as Dt, completeRun as E, ReviewBeingDeletedError as En, lifecycleChip as Er, describeRuntimes as Et, runBatch as F, describeSubject as Fn, queueStamp as Fr, trackedFiles as Ft, reviewsUrl as G, loadJob as Gn, simpleExplanationOf as Gr, createArchitectureNotification as Gt, clearServerRecordOwnedBy as H, endedAt as Hn, reviewerDispositions as Hr, announceJob as Ht, activateQueued as I, findExistingReview as In, readQueue as Ir, ownersOf as It, stopChild as J, isLiveJob as Jn, WITHHELD_REASON as Jr, eventName as Jt, serverEntryExists as K, saveJob as Kn, withSimpleExplanation as Kr, createPrState as Kt, compactQueue as L, checkCodeFreshness as Ln, doneVerb as Lr, QUOTE_LIMITS as Lt, extractPullRequestLinks as M, removeWorktree as Mn, queueAdd as Mr, repositoryKey as Mt, parsePullRequestLink as N, requestJobStop as Nn, queueAddMany as Nr, storeArchitecture as Nt, reportRun as O, createWorktree as On, lifecyclePill as Or, checkArchitecture as Ot, unsupportedReason as P, describeMovement as Pn, queueRemove as Pr, storedScopes as Pt, DEFAULT_WORKSPACE_PORT as Q, clearClaim as Qn, isCommitReview as Qr, markRead as Qt, queueForReview as R, checkReview as Rn, isIssueOutstanding as Rr, REPOSITORY_SCOPE as Rt, isDriving as S, remoteHost as Sn, LIFECYCLES as Sr, waitingHistory as St, beginRun as T, modelHelpLines as Tn, isLifecycle as Tr, RUNTIME_WORDS as Tt, ensureServer as U, fail as Un, processDiscussion as Ur, branchOf as Ut, bootstrapUrl as V, allJobs as Vn, questionsOutstanding as Vr, writeJsonAtomically as Vt, readServerRecord as W, isTerminal as Wn, recommendationsOf as Wr, subjectOf as Wt, workspaceUrl as X, progressSteps as Xn, resolveTarget as Xr, listNotifications as Xt, stopServer as Y, phasesFor as Yn, kissOutcome as Yr, headline as Yt, writeServerRecord as Z, purposeOf as Zn, DetachedHeadError as Zr, markAllRead as Zt, liveUpdateFor as _, extractJsonText as _n, reviewedRepositories as _r, telemetryUploadConsent as _t, wasBlocked as a, writeAgentPreference as ai, withUsageRecorded as an, kissDecisionRefusal as ar, failureOf as at, recordKissRun as b, forgeResolver as bn, workspaceRevision as br, PACKAGE_NAME as bt, RefreshUnavailableError as c, AGENT_IDS as ci, authorPreview as cn, lineagePosition as cr, reanalyseReview as ct, mergeUpdateInto as d, detectAgents as di, renderMarkdown as dn, mutateWorkspace as dr, append as dt, git as ei, pruneNotifications as en, decideKissRecommendation as er, MIN_WORKSPACE_PORT as et, updateReview as f, AgentCancelledError as fi, CODE_BLOCK_CLASS as fn, positionInLineage as fr, readEvents as ft, UpdateAlreadyRunningError as g, asString as gn, reviewedCommit as gr, recordTelemetryUploadConsent as gt, askCheckout as h, renderInlineText as hn, recentWorkspaces as hr, deliver as ht, liveJobsFor as i, readAgentPreference as ii, unreadCount as in, indexReviewGroups as ir, writePortPreference as it, batchIdentity as j, readMarker as jn, sectionOf as jr, readArchitecture as jt, stopRun as k, holdWorktree as kn, offersLifecycle as kr, loadArchitecture as kt, cancelUpdate as l, agentById as li, hasAuthorContent as ln, loadWorkspace as lr, runJob as lt, isUnchanged as m, escapeHtml as mn, previousKissResult as mr, stamp as mt, deleteReview as n, resolveModel as ni, removeNotification as nn, groupByLineage as nr, resolveWorkspacePort as nt, discardJob as o, CONFIG_PATH as oi, EXPLAIN_SIMPLY_PROMPT as on, kissDecisionsOf as or, prepareReview as ot, refreshPrContext as p, AgentUnavailableError as pi, CODE_COPY_CLASS as pn, presentedKissRunOf as pr, record as pt, spawnServer as q, PHASES as qn, describeAuthorship as qr, createPrUpdate as qt, lineageIds as r, clearAgentPreference as ri, toneOf as rn, indexKeptIds as rr, statedWorkspacePort as rt, FETCH_TIMEOUT_MS as s, STORE_ROOT as si, UNESTABLISHED_MOTIVATION as sn, latestKissRun as sr, settleReview as st, deleteLineage as t, resolveAgent as ti, readNotification as tn, followedRefName as tr, parseWorkspacePort as tt, cancellableHere as u, agentFor as ui, GITHUB_MARKDOWN as un, matchingWorkspaceIds as ur, startJob as ut, runningJobs as v, objectWithAnyOf as vn, summarise as vr, telemetryUploadEnabled as vt, RUN_KINDS as w, fillFileUrlTemplate as wn, activityLabel as wr, recordRuntimeBestEffort as wt, startKissJob as x, forgeFor as xn, workspaceStamps as xr, feedbackUrl as xt, agentEnvOf as y, claimRequestUrl as yn, touchWorkspace as yr, BUILD_VERSION as yt, settleQueue as z, followedBranch as zn, isQuestionOutstanding as zr, ancestorsOf as zt };
16755
+ export { MAX_WORKSPACE_PORT as $, isClaimed as $n, DetachedHeadError as $r, markReadForReview as $t, relativeTime as A, isWorktreeHeld as An, parseLifecycle as Ar, partFiles as At, readIndexToken as B, reviewedShaOf as Bn, issuesOutstanding as Br, isArchitectureId as Bt, track as C, originRemote as Cn, activityAttributes as Cr, mapRuntimes as Ct, endRun as D, MANAGED_ROOT as Dn, lifecycleItemLabel as Dr, ARCHITECTURE_DIR as Dt, completeRun as E, ReviewBeingDeletedError as En, lifecycleChip as Er, describeRuntimes as Et, runBatch as F, describeSubject as Fn, queueStamp as Fr, trackedFiles as Ft, reviewsUrl as G, loadJob as Gn, explanationActionOf as Gr, createArchitectureNotification as Gt, clearServerRecordOwnedBy as H, endedAt as Hn, reviewerDispositions as Hr, announceJob as Ht, activateQueued as I, findExistingReview as In, readQueue as Ir, ownersOf as It, stopChild as J, isLiveJob as Jn, withExplanation as Jr, eventName as Jt, serverEntryExists as K, saveJob as Kn, explanationOf as Kr, createPrState as Kt, compactQueue as L, checkCodeFreshness as Ln, doneVerb as Lr, QUOTE_LIMITS as Lt, extractPullRequestLinks as M, removeWorktree as Mn, queueAdd as Mr, repositoryKey as Mt, parsePullRequestLink as N, requestJobStop as Nn, queueAddMany as Nr, storeArchitecture as Nt, reportRun as O, createWorktree as On, lifecyclePill as Or, checkArchitecture as Ot, unsupportedReason as P, describeMovement as Pn, queueRemove as Pr, storedScopes as Pt, DEFAULT_WORKSPACE_PORT as Q, clearClaim as Qn, resolveTarget as Qr, markRead as Qt, queueForReview as R, checkReview as Rn, isIssueOutstanding as Rr, REPOSITORY_SCOPE as Rt, isDriving as S, remoteHost as Sn, LIFECYCLES as Sr, waitingHistory as St, beginRun as T, modelHelpLines as Tn, isLifecycle as Tr, RUNTIME_WORDS as Tt, ensureServer as U, fail as Un, processDiscussion as Ur, branchOf as Ut, bootstrapUrl as V, allJobs as Vn, questionsOutstanding as Vr, writeJsonAtomically as Vt, readServerRecord as W, isTerminal as Wn, recommendationsOf as Wr, subjectOf as Wt, workspaceUrl as X, progressSteps as Xn, WITHHELD_REASON as Xr, listNotifications as Xt, stopServer as Y, phasesFor as Yn, describeAuthorship as Yr, headline as Yt, writeServerRecord as Z, purposeOf as Zn, kissOutcome as Zr, markAllRead as Zt, liveUpdateFor as _, extractJsonText as _n, reviewedRepositories as _r, telemetryUploadConsent as _t, wasBlocked as a, clearAgentPreference as ai, withUsageRecorded as an, kissDecisionRefusal as ar, failureOf as at, recordKissRun as b, forgeResolver as bn, workspaceRevision as br, PACKAGE_NAME as bt, RefreshUnavailableError as c, CONFIG_PATH as ci, authorPreview as cn, lineagePosition as cr, reanalyseReview as ct, mergeUpdateInto as d, agentById as di, renderMarkdown as dn, mutateWorkspace as dr, append as dt, isCommitReview as ei, pruneNotifications as en, decideKissRecommendation as er, MIN_WORKSPACE_PORT as et, updateReview as f, agentFor as fi, CODE_BLOCK_CLASS as fn, positionInLineage as fr, readEvents as ft, UpdateAlreadyRunningError as g, asString as gn, reviewedCommit as gr, recordTelemetryUploadConsent as gt, askCheckout as h, AgentUnavailableError as hi, renderInlineText as hn, recentWorkspaces as hr, deliver as ht, liveJobsFor as i, resolveModel as ii, unreadCount as in, indexReviewGroups as ir, writePortPreference as it, batchIdentity as j, readMarker as jn, sectionOf as jr, readArchitecture as jt, stopRun as k, holdWorktree as kn, offersLifecycle as kr, loadArchitecture as kt, cancelUpdate as l, STORE_ROOT as li, hasAuthorContent as ln, loadWorkspace as lr, runJob as lt, isUnchanged as m, AgentCancelledError as mi, escapeHtml as mn, previousKissResult as mr, stamp as mt, deleteReview as n, git as ni, removeNotification as nn, groupByLineage as nr, resolveWorkspacePort as nt, discardJob as o, readAgentPreference as oi, EXPLAIN_SIMPLY_PROMPT as on, kissDecisionsOf as or, prepareReview as ot, refreshPrContext as p, detectAgents as pi, CODE_COPY_CLASS as pn, presentedKissRunOf as pr, record as pt, spawnServer as q, PHASES as qn, isExplanationAction as qr, createPrUpdate as qt, lineageIds as r, resolveAgent as ri, toneOf as rn, indexKeptIds as rr, statedWorkspacePort as rt, FETCH_TIMEOUT_MS as s, writeAgentPreference as si, UNESTABLISHED_MOTIVATION as sn, latestKissRun as sr, settleReview as st, deleteLineage as t, resolveBase as ti, readNotification as tn, followedRefName as tr, parseWorkspacePort as tt, cancellableHere as u, AGENT_IDS as ui, GITHUB_MARKDOWN as un, matchingWorkspaceIds as ur, startJob as ut, runningJobs as v, objectWithAnyOf as vn, summarise as vr, telemetryUploadEnabled as vt, RUN_KINDS as w, fillFileUrlTemplate as wn, activityLabel as wr, recordRuntimeBestEffort as wt, startKissJob as x, forgeFor as xn, workspaceStamps as xr, feedbackUrl as xt, agentEnvOf as y, claimRequestUrl as yn, touchWorkspace as yr, BUILD_VERSION as yt, settleQueue as z, followedBranch as zn, isQuestionOutstanding as zr, ancestorsOf as zt };
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as MAX_WORKSPACE_PORT, A as relativeTime, B as readIndexToken, Dn as MANAGED_ROOT, F as runBatch, Fn as describeSubject, G as reviewsUrl, Gn as loadJob, In as findExistingReview, Jr as WITHHELD_REASON, Mn as removeWorktree, Pn as describeMovement, Q as DEFAULT_WORKSPACE_PORT, Qn as clearClaim, R as queueForReview, Tn as modelHelpLines, U as ensureServer, V as bootstrapUrl, Vn as allJobs, X as workspaceUrl, Xn as progressSteps, Xr as resolveTarget, Y as stopServer, Yr as kissOutcome, Zn as purposeOf, _ as liveUpdateFor, _r as reviewedRepositories, _t as telemetryUploadConsent, a as wasBlocked, ai as writeAgentPreference, b as recordKissRun, bn as forgeResolver, bt as PACKAGE_NAME, ci as AGENT_IDS, ct as reanalyseReview, d as mergeUpdateInto, di as detectAgents, dr as mutateWorkspace, dt as append, ei as git, et as MIN_WORKSPACE_PORT, f as updateReview, fr as positionInLineage, g as UpdateAlreadyRunningError, gt as recordTelemetryUploadConsent, hr as recentWorkspaces, ht as deliver, i as liveJobsFor, ii as readAgentPreference, it as writePortPreference, j as batchIdentity, jn as readMarker, li as agentById, lr as loadWorkspace, mt as stamp, n as deleteReview$1, ni as resolveModel, oi as CONFIG_PATH, ot as prepareReview, pt as record$1, qn as PHASES, rt as statedWorkspacePort, si as STORE_ROOT, st as settleReview, ti as resolveAgent, tr as followedRefName, tt as parseWorkspacePort, ur as matchingWorkspaceIds, ut as startJob, v as runningJobs, vr as summarise, vt as telemetryUploadEnabled, yt as BUILD_VERSION } from "./delete_review-BgIZpcsP.js";
2
+ import { $ as MAX_WORKSPACE_PORT, A as relativeTime, B as readIndexToken, Dn as MANAGED_ROOT, F as runBatch, Fn as describeSubject, G as reviewsUrl, Gn as loadJob, In as findExistingReview, Mn as removeWorktree, Pn as describeMovement, Q as DEFAULT_WORKSPACE_PORT, Qn as clearClaim, Qr as resolveTarget, R as queueForReview, Tn as modelHelpLines, U as ensureServer, V as bootstrapUrl, Vn as allJobs, X as workspaceUrl, Xn as progressSteps, Xr as WITHHELD_REASON, Y as stopServer, Zn as purposeOf, Zr as kissOutcome, _ as liveUpdateFor, _r as reviewedRepositories, _t as telemetryUploadConsent, a as wasBlocked, b as recordKissRun, bn as forgeResolver, bt as PACKAGE_NAME, ci as CONFIG_PATH, ct as reanalyseReview, d as mergeUpdateInto, di as agentById, dr as mutateWorkspace, dt as append, et as MIN_WORKSPACE_PORT, f as updateReview, fr as positionInLineage, g as UpdateAlreadyRunningError, gt as recordTelemetryUploadConsent, hr as recentWorkspaces, ht as deliver, i as liveJobsFor, ii as resolveModel, it as writePortPreference, j as batchIdentity, jn as readMarker, li as STORE_ROOT, lr as loadWorkspace, mt as stamp, n as deleteReview$1, ni as git, oi as readAgentPreference, ot as prepareReview, pi as detectAgents, pt as record$1, qn as PHASES, ri as resolveAgent, rt as statedWorkspacePort, si as writeAgentPreference, st as settleReview, tr as followedRefName, tt as parseWorkspacePort, ui as AGENT_IDS, ur as matchingWorkspaceIds, ut as startJob, v as runningJobs, vr as summarise, vt as telemetryUploadEnabled, yt as BUILD_VERSION } from "./delete_review-CqnTq6yF.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";
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as MAX_WORKSPACE_PORT, $n as isClaimed, $r as resolveBase, $t as markReadForReview, A as relativeTime$1, An as isWorktreeHeld, Ar as parseLifecycle, At as partFiles, B as readIndexToken, Bn as reviewedShaOf, Br as issuesOutstanding, Bt as isArchitectureId, C as track, Cn as originRemote, Cr as activityAttributes, Ct as mapRuntimes, D as endRun, Dn as MANAGED_ROOT, Dr as lifecycleItemLabel, Dt as ARCHITECTURE_DIR, E as completeRun, En as ReviewBeingDeletedError, Er as lifecycleChip, Et as describeRuntimes, F as runBatch, Fr as queueStamp, Ft as trackedFiles, Gn as loadJob, Gr as simpleExplanationOf, Gt as createArchitectureNotification, H as clearServerRecordOwnedBy, Hn as endedAt, Hr as reviewerDispositions, Ht as announceJob, I as activateQueued, In as findExistingReview, Ir as readQueue, It as ownersOf, J as stopChild, Jn as isLiveJob, Jr as WITHHELD_REASON, Jt as eventName, K as serverEntryExists, Kn as saveJob, Kr as withSimpleExplanation, Kt as createPrState, L as compactQueue, Ln as checkCodeFreshness, Lr as doneVerb, Lt as QUOTE_LIMITS, M as extractPullRequestLinks, Mn as removeWorktree, Mr as queueAdd, Mt as repositoryKey, N as parsePullRequestLink, Nn as requestJobStop, Nr as queueAddMany, Nt as storeArchitecture, O as reportRun, On as createWorktree, Or as lifecyclePill, Ot as checkArchitecture, P as unsupportedReason, Pr as queueRemove, Pt as storedScopes, Q as DEFAULT_WORKSPACE_PORT, Qr as isCommitReview, Qt as markRead, Rn as checkReview, Rr as isIssueOutstanding, Rt as REPOSITORY_SCOPE, S as isDriving, Sn as remoteHost, Sr as LIFECYCLES, St as waitingHistory, T as beginRun, Tr as isLifecycle, Tt as RUNTIME_WORDS, Un as fail, Ur as processDiscussion, Ut as branchOf, Vn as allJobs, Vr as questionsOutstanding, Vt as writeJsonAtomically, W as readServerRecord, Wn as isTerminal, Wr as recommendationsOf, Wt as subjectOf, Xn as progressSteps, Xr as resolveTarget, Xt as listNotifications, Yn as phasesFor, Yr as kissOutcome, Yt as headline, Z as writeServerRecord, Zn as purposeOf, Zr as DetachedHeadError, Zt as markAllRead, _ as liveUpdateFor, _n as extractJsonText, _t as telemetryUploadConsent, a as wasBlocked, ai as writeAgentPreference, an as withUsageRecorded, ar as kissDecisionRefusal, at as failureOf$1, bn as forgeResolver, br as workspaceRevision, c as RefreshUnavailableError, ci as AGENT_IDS, cn as authorPreview, cr as lineagePosition, ct as reanalyseReview, d as mergeUpdateInto, di as detectAgents, dn as renderMarkdown, dr as mutateWorkspace, ei as git, en as pruneNotifications, er as decideKissRecommendation, et as MIN_WORKSPACE_PORT, f as updateReview, fi as AgentCancelledError, fn as CODE_BLOCK_CLASS, ft as readEvents, g as UpdateAlreadyRunningError, gn as asString, gr as reviewedCommit, gt as recordTelemetryUploadConsent, h as askCheckout, hn as renderInlineText, hr as recentWorkspaces, i as liveJobsFor, ii as readAgentPreference, in as unreadCount$1, ir as indexReviewGroups, it as writePortPreference, j as batchIdentity, jn as readMarker, jr as sectionOf, jt as readArchitecture, k as stopRun, kn as holdWorktree, kr as offersLifecycle, kt as loadArchitecture, l as cancelUpdate, li as agentById, ln as hasAuthorContent, lr as loadWorkspace, lt as runJob, m as isUnchanged, mn as escapeHtml$1, mr as previousKissResult, n as deleteReview, ni as resolveModel, nn as removeNotification, nr as groupByLineage, nt as resolveWorkspacePort, o as discardJob, on as EXPLAIN_SIMPLY_PROMPT, or as kissDecisionsOf, ot as prepareReview, p as refreshPrContext, pi as AgentUnavailableError, pn as CODE_COPY_CLASS, pr as presentedKissRunOf, pt as record, q as spawnServer, qr as describeAuthorship, qt as createPrUpdate, r as lineageIds, ri as clearAgentPreference, rn as toneOf, rr as indexKeptIds, rt as statedWorkspacePort, s as FETCH_TIMEOUT_MS, si as STORE_ROOT, sn as UNESTABLISHED_MOTIVATION, sr as latestKissRun, st as settleReview, t as deleteLineage, ti as resolveAgent, tn as readNotification, tr as followedRefName, tt as parseWorkspacePort, u as cancellableHere, ui as agentFor$1, un as GITHUB_MARKDOWN, vn as objectWithAnyOf, w as RUN_KINDS, wn as fillFileUrlTemplate, wr as activityLabel, wt as recordRuntimeBestEffort, x as startKissJob, xn as forgeFor, xr as workspaceStamps, xt as feedbackUrl, y as agentEnvOf, yn as claimRequestUrl, yr as touchWorkspace, yt as BUILD_VERSION, z as settleQueue, zn as followedBranch, zr as isQuestionOutstanding, zt as ancestorsOf } from "./delete_review-BgIZpcsP.js";
1
+ import { $ as MAX_WORKSPACE_PORT, $n as isClaimed, $r as DetachedHeadError, $t as markReadForReview, A as relativeTime$1, An as isWorktreeHeld, Ar as parseLifecycle, At as partFiles, B as readIndexToken, Bn as reviewedShaOf, Br as issuesOutstanding, Bt as isArchitectureId, C as track, Cn as originRemote, Cr as activityAttributes, Ct as mapRuntimes, D as endRun, Dn as MANAGED_ROOT, Dr as lifecycleItemLabel, Dt as ARCHITECTURE_DIR, E as completeRun, En as ReviewBeingDeletedError, Er as lifecycleChip, Et as describeRuntimes, F as runBatch, Fr as queueStamp, Ft as trackedFiles, Gn as loadJob, Gr as explanationActionOf, Gt as createArchitectureNotification, H as clearServerRecordOwnedBy, Hn as endedAt, Hr as reviewerDispositions, Ht as announceJob, I as activateQueued, In as findExistingReview, Ir as readQueue, It as ownersOf, J as stopChild, Jn as isLiveJob, Jr as withExplanation, Jt as eventName, K as serverEntryExists, Kn as saveJob, Kr as explanationOf, Kt as createPrState, L as compactQueue, Ln as checkCodeFreshness, Lr as doneVerb, Lt as QUOTE_LIMITS, M as extractPullRequestLinks, Mn as removeWorktree, Mr as queueAdd, Mt as repositoryKey, N as parsePullRequestLink, Nn as requestJobStop, Nr as queueAddMany, Nt as storeArchitecture, O as reportRun, On as createWorktree, Or as lifecyclePill, Ot as checkArchitecture, P as unsupportedReason, Pr as queueRemove, Pt as storedScopes, Q as DEFAULT_WORKSPACE_PORT, Qr as resolveTarget, Qt as markRead, Rn as checkReview, Rr as isIssueOutstanding, Rt as REPOSITORY_SCOPE, S as isDriving, Sn as remoteHost, Sr as LIFECYCLES, St as waitingHistory, T as beginRun, Tr as isLifecycle, Tt as RUNTIME_WORDS, Un as fail, Ur as processDiscussion, Ut as branchOf, Vn as allJobs, Vr as questionsOutstanding, Vt as writeJsonAtomically, W as readServerRecord, Wn as isTerminal, Wr as recommendationsOf, Wt as subjectOf, Xn as progressSteps, Xr as WITHHELD_REASON, Xt as listNotifications, Yn as phasesFor, Yr as describeAuthorship, Yt as headline, Z as writeServerRecord, Zn as purposeOf, Zr as kissOutcome, Zt as markAllRead, _ as liveUpdateFor, _n as extractJsonText, _t as telemetryUploadConsent, a as wasBlocked, ai as clearAgentPreference, an as withUsageRecorded, ar as kissDecisionRefusal, at as failureOf$1, bn as forgeResolver, br as workspaceRevision, c as RefreshUnavailableError, cn as authorPreview, cr as lineagePosition, ct as reanalyseReview, d as mergeUpdateInto, di as agentById, dn as renderMarkdown, dr as mutateWorkspace, ei as isCommitReview, en as pruneNotifications, er as decideKissRecommendation, et as MIN_WORKSPACE_PORT, f as updateReview, fi as agentFor$1, fn as CODE_BLOCK_CLASS, ft as readEvents, g as UpdateAlreadyRunningError, gn as asString, gr as reviewedCommit, gt as recordTelemetryUploadConsent, h as askCheckout, hi as AgentUnavailableError, hn as renderInlineText, hr as recentWorkspaces, i as liveJobsFor, ii as resolveModel, in as unreadCount$1, ir as indexReviewGroups, it as writePortPreference, j as batchIdentity, jn as readMarker, jr as sectionOf, jt as readArchitecture, k as stopRun, kn as holdWorktree, kr as offersLifecycle, kt as loadArchitecture, l as cancelUpdate, li as STORE_ROOT, ln as hasAuthorContent, lr as loadWorkspace, lt as runJob, m as isUnchanged, mi as AgentCancelledError, mn as escapeHtml$1, mr as previousKissResult, n as deleteReview, ni as git, nn as removeNotification, nr as groupByLineage, nt as resolveWorkspacePort, o as discardJob, oi as readAgentPreference, on as EXPLAIN_SIMPLY_PROMPT, or as kissDecisionsOf, ot as prepareReview, p as refreshPrContext, pi as detectAgents, pn as CODE_COPY_CLASS, pr as presentedKissRunOf, pt as record, q as spawnServer, qr as isExplanationAction, qt as createPrUpdate, r as lineageIds, ri as resolveAgent, rn as toneOf, rr as indexKeptIds, rt as statedWorkspacePort, s as FETCH_TIMEOUT_MS, si as writeAgentPreference, sn as UNESTABLISHED_MOTIVATION, sr as latestKissRun, st as settleReview, t as deleteLineage, ti as resolveBase, tn as readNotification, tr as followedRefName, tt as parseWorkspacePort, u as cancellableHere, ui as AGENT_IDS, un as GITHUB_MARKDOWN, vn as objectWithAnyOf, w as RUN_KINDS, wn as fillFileUrlTemplate, wr as activityLabel, wt as recordRuntimeBestEffort, x as startKissJob, xn as forgeFor, xr as workspaceStamps, xt as feedbackUrl, y as agentEnvOf, yn as claimRequestUrl, yr as touchWorkspace, yt as BUILD_VERSION, z as settleQueue, zn as followedBranch, zr as isQuestionOutstanding, zt as ancestorsOf } from "./delete_review-CqnTq6yF.js";
2
2
  import { basename, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -1097,25 +1097,22 @@ function renderAskBuddyMenu(kind) {
1097
1097
  </div>`;
1098
1098
  }
1099
1099
  //#endregion
1100
- //#region ../../packages/review-harness/src/domain/ui/components/simple_explanation.ts
1101
- /**
1102
- * A finding's simple explanation: Buddy AI's plain-English account of it, kept on the card because
1103
- * the reviewer asked for one.
1104
- *
1105
- * For reading the finding, not part of it. It carries no copy source, so Copy and Prepare feedback,
1106
- * which take the card's first one, can only ever find the finding's own.
1107
- *
1108
- * A native disclosure, closed when the page loads and open when it has just been written. Whether a
1109
- * reader left it open is not kept: the explanation is the durable part.
1110
- */
1111
- var SIMPLE_EXPLANATION_LABEL = "Simple explanation";
1100
+ //#region ../../packages/review-harness/src/domain/ui/components/finding_explanation.ts
1112
1101
  var ASK_MORE_LABEL = "Ask more →";
1113
- function renderSimpleExplanation(text, options = {}) {
1114
- const body = renderMarkdown(text);
1102
+ var EXPLANATION_LABELS = {
1103
+ "explain-detail": "Explanation · Technical",
1104
+ explain: "Explanation · Simple"
1105
+ };
1106
+ /** The disclosure's label: which kind of explanation the card holds, so the other is one choice away. */
1107
+ function explanationLabel(explanation) {
1108
+ return EXPLANATION_LABELS[explanationActionOf(explanation)];
1109
+ }
1110
+ function renderFindingExplanation(explanation, options = {}) {
1111
+ const body = renderMarkdown(explanation.text);
1115
1112
  const askMore = options.askMore ? `<button type="button" class="pr-helper-simple-explanation-more" data-simple-explanation-more>${ASK_MORE_LABEL}</button>` : "";
1116
1113
  const content = askMore && body.endsWith("</p>") ? `${body.slice(0, -4)} ${askMore}</p>` : body + askMore;
1117
- return `<details class="pr-helper-simple-explanation" data-simple-explanation${options.open ? " open" : ""}>
1118
- <summary class="pr-helper-simple-explanation-summary">${SIMPLE_EXPLANATION_LABEL}</summary>
1114
+ return `<details class="pr-helper-simple-explanation" data-simple-explanation${!Number.isFinite(explanation.writtenAt) ? "" : ` data-explanation-written-at="${explanation.writtenAt}"`}${options.open ? " open" : ""}>
1115
+ <summary class="pr-helper-simple-explanation-summary">${explanationLabel(explanation)}</summary>
1119
1116
  <div class="pr-helper-simple-explanation-body pr-helper-markdown">${content}</div>
1120
1117
  </details>`;
1121
1118
  }
@@ -2608,7 +2605,7 @@ function renderIssuesTab(session, investigatingId, options) {
2608
2605
  </div>
2609
2606
  ${renderStaleDismissal(stale)}
2610
2607
  ${renderSeverityRevision(issue.priorSeverity, issue.severity)}
2611
- <p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>${renderCardExplanation(simpleExplanationOf(session, "issue", issue.id)?.text, options)}
2608
+ <p class="pr-helper-issue-desc">${renderInlineText(issue.description)}</p>${renderCardExplanation(explanationOf(session, "issue", issue.id), options)}
2612
2609
  ${renderReconciliationNote(issue.reconciliationNote)}
2613
2610
  ${renderInvestigationNote(issue.investigationNote)}
2614
2611
  ${renderExistingDiscussion(issue.existingDiscussion)}
@@ -2642,12 +2639,12 @@ function closedFindingsSummary(counts) {
2642
2639
  return `Dealt with (${counts.done})`;
2643
2640
  }
2644
2641
  /**
2645
- * The finding's simple explanation, under what the finding says and above where it stands. Nothing
2646
- * at all when the reviewer has not asked for one, so such a card is the card it always was.
2642
+ * The finding's explanation, under what the finding says and above where it stands. Nothing at all
2643
+ * when the reviewer has not asked for one, so such a card is the card it always was.
2647
2644
  */
2648
- function renderCardExplanation(text, options) {
2649
- if (!text?.trim()) return "";
2650
- return renderSimpleExplanation(text, { askMore: options?.askBuddy });
2645
+ function renderCardExplanation(explanation, options) {
2646
+ if (!explanation?.text.trim()) return "";
2647
+ return renderFindingExplanation(explanation, { askMore: options?.askBuddy });
2651
2648
  }
2652
2649
  /** The reviewer's own decision, said on the card so the closed pile is never one undifferentiated heap. */
2653
2650
  function renderDismissedBadge(isDismissed) {
@@ -2787,7 +2784,7 @@ function renderQuestionsTab(session, investigatingId, options) {
2787
2784
  const progress = renderQuestionsProgress(result.questions, answered);
2788
2785
  const records = session.investigations;
2789
2786
  const now = Date.now();
2790
- const card = (q) => renderQuestionCard(q, sections, answered.has(q.id), records, now, investigatingId, options, simpleExplanationOf(session, "question", q.id)?.text);
2787
+ const card = (q) => renderQuestionCard(q, sections, answered.has(q.id), records, now, investigatingId, options, explanationOf(session, "question", q.id));
2791
2788
  const open = result.questions.filter((q) => isQuestionOutstanding(q, answered));
2792
2789
  const settled = result.questions.filter((q) => !isQuestionOutstanding(q, answered));
2793
2790
  const closed = settled.length === 0 ? "" : renderClosedFindings(`Answered (${settled.length})`, settled.map(card).join(""), "questions", open.length === 0);
@@ -6771,7 +6768,7 @@ function renderAsk(view, sections, paused = false) {
6771
6768
  <div class="prb-ask-log" data-ask-log>
6772
6769
  ${transcript.length > 0 || pending ? transcript.map((exchange) => `<div class="prb-msg prb-msg-user">${escapeHtml$1(exchange.question)}</div>
6773
6770
  <div class="prb-msg prb-msg-assistant prb-md">${renderMarkdown(exchange.answer)}${renderMessageCopySource(exchange.answer)}</div>`).join("") + pendingTurn : `<div class="prb-ask-empty"${paused ? " inert" : ""}>
6774
- <p>Ask about this change. Answers come from the review, the diff and the repository on disk — not from anywhere else.</p>
6771
+ <p>Ask about this change. Answers come only from the review, the diff and the repository on disk.</p>
6775
6772
  ${suggestions.map((text) => `<button type="button" class="prb-suggestion" data-suggestion>${escapeHtml$1(text)}</button>`).join("")}
6776
6773
  </div>`}
6777
6774
  </div>
@@ -11373,7 +11370,7 @@ function buildPrompt$2(workspace, question, sectionId, cwd, context = {}) {
11373
11370
  ].join("\n");
11374
11371
  }
11375
11372
  /**
11376
- * The simple explanation a follow-up was asked from, however long ago it was written.
11373
+ * The kept explanation a follow-up was asked from, however long ago it was written.
11377
11374
  *
11378
11375
  * Given in full rather than left to the recent turns above, which hold only the last four: a
11379
11376
  * reviewer who comes back a week later and presses Ask more is continuing from the explanation on
@@ -11383,22 +11380,24 @@ function continuingFrom(workspace, continuing) {
11383
11380
  if (!continuing) return [];
11384
11381
  return [
11385
11382
  "---",
11386
- `THE REVIEWER IS CONTINUING FROM THIS PLAIN-ENGLISH EXPLANATION YOU GAVE EARLIER OF ${findingName(workspace, continuing)}:`,
11383
+ `THE REVIEWER IS CONTINUING FROM THIS ${explanationActionOf(continuing) === "explain" ? "PLAIN-ENGLISH" : "TECHNICAL"} EXPLANATION YOU GAVE EARLIER OF ${findingName(workspace, continuing)}:`,
11387
11384
  continuing.text
11388
11385
  ];
11389
11386
  }
11390
11387
  /**
11391
- * Explain simply on a finding, whose answer is kept on the card and read again after the code has
11392
- * moved on. So it explains the claim, what the finding says and why it was raised, rather than
11393
- * asserting what the code does now: a later update or investigation says whether the claim still
11394
- * holds, and an explanation written as a present-tense fact about the code would contradict it.
11388
+ * Explain (technical) or Explain (simple) on a finding, whose answer is kept on the card and read
11389
+ * again after the code has moved on. So either explains the claim, what the finding says and why it
11390
+ * was raised, rather than asserting what the code does now: a later update or investigation says
11391
+ * whether the claim still holds, and an explanation written as a present-tense fact about the code
11392
+ * would contradict it. They differ only in register, as the visible instruction asks.
11395
11393
  */
11396
11394
  function explainingFinding(workspace, explaining) {
11397
11395
  if (!explaining) return [];
11396
+ const simple = explaining.action === "explain";
11398
11397
  return [
11399
11398
  "---",
11400
- `THE REVIEWER ASKED FOR A SIMPLE EXPLANATION OF ${findingName(workspace, explaining)}. It is kept beside that finding and read again later, possibly after the code has changed.`,
11401
- "Explain in plain English what the finding is saying and why it was raised: the concern as the review stated it, in terms of what was reviewed (for example \"this was raised because...\"), not as a statement about how the code behaves now. Do not judge whether it still holds; the review tracks that separately."
11399
+ `THE REVIEWER ASKED FOR ${simple ? "A SIMPLE" : "A TECHNICAL"} EXPLANATION OF ${findingName(workspace, explaining)}. It is kept beside that finding and read again later, possibly after the code has changed.`,
11400
+ `Explain ${simple ? "in plain English" : "with the technical detail kept"} what the finding is saying and why it was raised: the concern as the review stated it, in terms of what was reviewed (for example "this was raised because..."), not as a statement about how the code behaves now. Do not judge whether it still holds; the review tracks that separately.`
11402
11401
  ];
11403
11402
  }
11404
11403
  /** `THE ISSUE "<title>"` or `THE QUESTION "<question>"`, or just the kind when the finding is gone. */
@@ -13221,11 +13220,13 @@ var MAX_KEPT_BRANCHES = 50;
13221
13220
  *
13222
13221
  * Only once the analysis is complete. The page is served while a review is still being made, and
13223
13222
  * recording that would file a row of zeroes — no risk, no themes, no findings — against a review
13224
- * that has them, which is worse than recording nothing.
13223
+ * that has them, which is worse than recording nothing. `result` is that signal: a running or idle
13224
+ * session carries none. `completedAt` is not: it is the Reviewed status, and gating on it kept this
13225
+ * event off every first read.
13225
13226
  */
13226
13227
  function workspaceOpenedEvent(workspace) {
13227
13228
  const result = workspace.session.result;
13228
- if (workspace.completedAt === void 0 || !result) return null;
13229
+ if (!result) return null;
13229
13230
  const severities = {
13230
13231
  critical: 0,
13231
13232
  major: 0,
@@ -13694,9 +13695,8 @@ async function handleAsk(req, res, workspace) {
13694
13695
  const question = typeof body?.question === "string" ? body.question.trim() : "";
13695
13696
  if (!question) return send(res, 400, { error: "No question given." });
13696
13697
  const sectionId = typeof body?.sectionId === "string" ? body.sectionId : null;
13697
- const explains = findingNamed(workspace.session, body?.explains);
13698
- const continuesFrom = findingNamed(workspace.session, body?.continuesExplanation);
13699
- const continuing = continuesFrom ? simpleExplanationOf(workspace.session, continuesFrom.kind, continuesFrom.id) : void 0;
13698
+ const explains = explanationRequested(workspace.session, body?.explains);
13699
+ const continuing = explanationContinued(workspace, body?.continuesExplanation);
13700
13700
  if (liveUpdateFor(workspace.id)) return send(res, 409, {
13701
13701
  error: "This review is being updated. Ask again when the new review is ready.",
13702
13702
  updating: true
@@ -13749,18 +13749,22 @@ async function handleAsk(req, res, workspace) {
13749
13749
  });
13750
13750
  let explanation;
13751
13751
  mutateWorkspace(workspace.id, (fresh) => {
13752
+ const askedAt = Date.now();
13753
+ const kept = explains && findingNamed(fresh.session, explains) && answer.trim() ? explains : null;
13752
13754
  fresh.transcript.push({
13753
13755
  question,
13754
13756
  answer,
13755
13757
  sectionId,
13756
- askedAt: Date.now()
13758
+ askedAt,
13759
+ ...kept ? { explains: kept } : {}
13757
13760
  });
13758
- if (explains && findingNamed(fresh.session, explains) && answer.trim()) {
13761
+ if (kept) {
13759
13762
  explanation = {
13760
- ...explains,
13761
- text: answer.trim()
13763
+ ...kept,
13764
+ text: answer.trim(),
13765
+ writtenAt: askedAt
13762
13766
  };
13763
- fresh.session.simpleExplanations = withSimpleExplanation(fresh.session.simpleExplanations, explanation);
13767
+ fresh.session.findingExplanations = withExplanation(fresh.session.findingExplanations, explanation);
13764
13768
  }
13765
13769
  });
13766
13770
  sendIfOpen(res, 200, explanation ? {
@@ -13771,6 +13775,45 @@ async function handleAsk(req, res, workspace) {
13771
13775
  endRun(run);
13772
13776
  }
13773
13777
  }
13778
+ /**
13779
+ * The explanation a follow-up was staged from, named by its finding and `writtenAt`.
13780
+ *
13781
+ * The card's current one when it is still that explanation. When another has replaced it since, the
13782
+ * turn that wrote it, which the transcript keeps, so a question written against one explanation is
13783
+ * never sent with another's words. Nothing when neither is there. A follow-up without `writtenAt`
13784
+ * (an explanation kept before 0.72.0, or a page from then) continues from the card's current one.
13785
+ */
13786
+ function explanationContinued(workspace, value) {
13787
+ const finding = findingNamed(workspace.session, value);
13788
+ if (!finding) return void 0;
13789
+ const current = explanationOf(workspace.session, finding.kind, finding.id);
13790
+ const writtenAt = value.writtenAt;
13791
+ if (writtenAt === void 0 || current?.writtenAt === writtenAt) return current;
13792
+ const turn = workspace.transcript.find((exchange) => exchange.askedAt === writtenAt && exchange.explains?.kind === finding.kind && exchange.explains.id === finding.id);
13793
+ return turn?.explains && turn.answer.trim() ? {
13794
+ ...finding,
13795
+ action: turn.explains.action,
13796
+ text: turn.answer.trim(),
13797
+ writtenAt: turn.askedAt
13798
+ } : void 0;
13799
+ }
13800
+ /**
13801
+ * The finding a turn explains and the action explaining it, or null when its answer is not to be kept.
13802
+ *
13803
+ * A missing action is Explain (simple): the only one a page sent before there were two, and a tab
13804
+ * opened then still sends it that way. An action that is named but not an explanation is not
13805
+ * guessed at; the turn is answered like any other and nothing is kept.
13806
+ */
13807
+ function explanationRequested(session, value) {
13808
+ const finding = findingNamed(session, value);
13809
+ if (!finding) return null;
13810
+ const named = value.action;
13811
+ const action = named === void 0 ? "explain" : named;
13812
+ return isExplanationAction(action) ? {
13813
+ ...finding,
13814
+ action
13815
+ } : null;
13816
+ }
13774
13817
  /** An issue or question of this review, named by kind and id; null for anything else. */
13775
13818
  function findingNamed(session, value) {
13776
13819
  const named = value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prreviewbuddy",
3
- "version": "0.71.1",
3
+ "version": "0.73.0",
4
4
  "description": "Understand code changes before you approve them. Local branch and pull request reviews with Claude Code, Codex or Gemini CLI.",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://prreviewbuddy.com",