prreviewbuddy 0.71.2 → 0.73.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 +46 -0
- package/dist/{delete_review-BgIZpcsP.js → delete_review-B0f6rmy_.js} +93 -17
- package/dist/main.js +1 -1
- package/dist/server.js +116 -63
- package/package.json +1 -1
- package/static/shell.css +23 -12
- package/static/workspace.js +43 -43
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.73.1
|
|
4
|
+
|
|
5
|
+
**A review's first update says how long updates take.** While an update runs, the page now
|
|
6
|
+
compares it with this review's earlier updates, and on its first update with other updates in the
|
|
7
|
+
same repository: "Previous update took 51s", "Updates to this review usually take about 1 minute",
|
|
8
|
+
or "Updates in this repository usually take about 1 minute". An update is never compared with a
|
|
9
|
+
fresh review, which does much more work. With no finished update in the repository, nothing is
|
|
10
|
+
said.
|
|
11
|
+
|
|
12
|
+
**The running time is the footer of the progress page.** Under the steps, the running time counts
|
|
13
|
+
to the second ("Running for 1m 08s"), with how long earlier runs took beneath it and Cancel beside
|
|
14
|
+
them as a proper button. The same footer appears while a review runs and while an update runs.
|
|
15
|
+
|
|
16
|
+
## 0.73.0
|
|
17
|
+
|
|
18
|
+
**KISS runs now count as reviews in usage telemetry.** With telemetry on, a KISS run sends the same
|
|
19
|
+
two events a standard review does, `review_started` and `review_finished`, with the same agent,
|
|
20
|
+
outcome and duration. Until now it sent neither, so somebody who only ran KISS looked as if they
|
|
21
|
+
had installed PR Review Buddy and never used it. Both review events now carry one more field,
|
|
22
|
+
`reviewType`, which is one of two words, `standard` or `kiss`: the kind of analysis, and nothing
|
|
23
|
+
about your code, your repository or what the analysis said. It is the only field added. Updating a
|
|
24
|
+
review with new commits still sends nothing. `prreviewbuddy config set telemetry-upload off` and
|
|
25
|
+
`PRB_TELEMETRY=off` stop these exactly as before.
|
|
26
|
+
|
|
27
|
+
## 0.72.1
|
|
28
|
+
|
|
29
|
+
**Local usage logs count themes on the first read.** The `workspace_opened` log entry waited for
|
|
30
|
+
the review to be marked Reviewed rather than for the analysis to finish, so it rarely fired before
|
|
31
|
+
a reading session ended and the session summary recorded zero themes. It now waits only for the
|
|
32
|
+
analysis. Nothing about what is sent changes: neither event leaves the machine.
|
|
33
|
+
|
|
34
|
+
## 0.72.0
|
|
35
|
+
|
|
36
|
+
**Both explanations stay on the card.** Ask Buddy on an issue or question now offers
|
|
37
|
+
**Explain (technical)** and **Explain (simple)**, and either one keeps its answer under the finding,
|
|
38
|
+
labelled **Explanation · Technical** or **Explanation · Simple**. A card holds one explanation: run
|
|
39
|
+
the other kind and it replaces the one there, while both answers stay in Buddy AI. If a new
|
|
40
|
+
explanation fails or is stopped, the card keeps the one it had. The same two names appear wherever
|
|
41
|
+
Ask Buddy offers them, including on a selection.
|
|
42
|
+
|
|
43
|
+
**Ask more follows up on the explanation you were reading.** If another tab replaces a card's
|
|
44
|
+
explanation after you press Ask more, your question still goes with the one you were reading.
|
|
45
|
+
|
|
46
|
+
**Simple explanations kept by 0.71 carry over.** They appear on their cards as
|
|
47
|
+
Explanation · Simple, and Ask more and updates use them as before.
|
|
48
|
+
|
|
3
49
|
## 0.71.2
|
|
4
50
|
|
|
5
51
|
**Buddy AI's opening line reads plainly.** Before your first question, Buddy AI now says "Answers
|
|
@@ -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
|
|
3282
|
-
return session.
|
|
3293
|
+
function explanationOf(session, kind, id) {
|
|
3294
|
+
return session.findingExplanations?.find((entry) => entry.kind === kind && entry.id === id);
|
|
3283
3295
|
}
|
|
3284
|
-
/**
|
|
3285
|
-
|
|
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
|
+
};
|
|
3312
|
+
}
|
|
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
|
-
* `
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
}
|
|
@@ -12831,13 +12869,28 @@ var RUNTIME_WORDS = {
|
|
|
12831
12869
|
last: "Last analysis of this review",
|
|
12832
12870
|
recent: "Recent analyses of this review",
|
|
12833
12871
|
usual: "Usually takes"
|
|
12834
|
-
},
|
|
12835
|
-
update: {
|
|
12836
|
-
last: "Last update of this review",
|
|
12837
|
-
recent: "Recent updates of this review",
|
|
12838
|
-
usual: "Usually takes"
|
|
12839
12872
|
}
|
|
12840
12873
|
};
|
|
12874
|
+
/**
|
|
12875
|
+
* How long earlier updates took, said from the most specific history there is: this review's own
|
|
12876
|
+
* updates, else the repository's, else nothing. Never both, and never a fresh review's runtime,
|
|
12877
|
+
* which is a different operation. The rule above holds at both levels: one run is stated exactly
|
|
12878
|
+
* and only several earn "usually". Several are always one figure, the median, never a range.
|
|
12879
|
+
*/
|
|
12880
|
+
function describeUpdateRuntimes(own, repository) {
|
|
12881
|
+
const [ownRuns, repoRuns] = [own.slice(0, 3), repository.slice(0, 3)];
|
|
12882
|
+
if (ownRuns.length === 1) return `Previous update took ${formatDuration(ownRuns[0])}`;
|
|
12883
|
+
if (ownRuns.length > 1) return `Updates to this review usually take ${aboutMinutes(median(ownRuns))}`;
|
|
12884
|
+
if (repoRuns.length === 1) return `Last update in this repository took ${formatDuration(repoRuns[0])}`;
|
|
12885
|
+
if (repoRuns.length > 1) return `Updates in this repository usually take ${aboutMinutes(median(repoRuns))}`;
|
|
12886
|
+
return null;
|
|
12887
|
+
}
|
|
12888
|
+
/** "less than a minute", "about 1 minute", "about 4 minutes". */
|
|
12889
|
+
function aboutMinutes(ms) {
|
|
12890
|
+
const minutes = toMinutes(ms);
|
|
12891
|
+
if (minutes === 0) return "less than a minute";
|
|
12892
|
+
return `about ${minutes} ${minutes === 1 ? "minute" : "minutes"}`;
|
|
12893
|
+
}
|
|
12841
12894
|
//#endregion
|
|
12842
12895
|
//#region ../../packages/review-harness/src/runtime/store.ts
|
|
12843
12896
|
var VERSION = 1;
|
|
@@ -12944,6 +12997,18 @@ function updateRuntimes(repoPath, reviewId) {
|
|
|
12944
12997
|
reviewType: "standard"
|
|
12945
12998
|
});
|
|
12946
12999
|
}
|
|
13000
|
+
/**
|
|
13001
|
+
* Updates of any review in the repository, for a review's first update. No subject for the reason
|
|
13002
|
+
* `newReviewRuntimes` has none: a review not yet updated has no updates of its own. Still only
|
|
13003
|
+
* updates, because a fresh review of the same change is a different operation and much slower.
|
|
13004
|
+
*/
|
|
13005
|
+
function repoUpdateRuntimes(repoPath) {
|
|
13006
|
+
return durationsOf({
|
|
13007
|
+
operations: ["update"],
|
|
13008
|
+
repoPath,
|
|
13009
|
+
reviewType: "standard"
|
|
13010
|
+
});
|
|
13011
|
+
}
|
|
12947
13012
|
//#endregion
|
|
12948
13013
|
//#region ../../packages/review-harness/src/workspace/runtime_history.ts
|
|
12949
13014
|
/**
|
|
@@ -12977,10 +13042,15 @@ function analysisHistory(workspace, job) {
|
|
|
12977
13042
|
function waitingHistory(workspace, runs) {
|
|
12978
13043
|
return {
|
|
12979
13044
|
job: isLiveJob(runs.job) ? analysisHistory(workspace, runs.job) : null,
|
|
12980
|
-
update: isLiveJob(runs.updateJob) ?
|
|
13045
|
+
update: isLiveJob(runs.updateJob) ? updateHistory(workspace) : null,
|
|
12981
13046
|
kiss: isLiveJob(runs.kissJob) ? analysisHistory(workspace, runs.kissJob) : null
|
|
12982
13047
|
};
|
|
12983
13048
|
}
|
|
13049
|
+
/** This review's own updates first; the repository's only for a review never updated before. */
|
|
13050
|
+
function updateHistory(workspace) {
|
|
13051
|
+
const own = updateRuntimes(workspace.repoPath, workspace.id);
|
|
13052
|
+
return describeUpdateRuntimes(own, own.length > 0 ? [] : repoUpdateRuntimes(workspace.repoPath));
|
|
13053
|
+
}
|
|
12984
13054
|
//#endregion
|
|
12985
13055
|
//#region ../../packages/review-harness/src/domain/feedback/feedback_url.ts
|
|
12986
13056
|
/** How each client names itself in the prefilled message, which is the half a screenshot carries. */
|
|
@@ -13273,6 +13343,7 @@ function project(record) {
|
|
|
13273
13343
|
cliVersion: record.cliVersion,
|
|
13274
13344
|
platform: record.platform,
|
|
13275
13345
|
agent: event.agent,
|
|
13346
|
+
reviewType: event.reviewType,
|
|
13276
13347
|
occurredAt: record.at
|
|
13277
13348
|
};
|
|
13278
13349
|
return event.event === "review_finished" ? {
|
|
@@ -13351,7 +13422,7 @@ var LOG_PATH = join(STORE_ROOT, "events.jsonl");
|
|
|
13351
13422
|
* disable is a different act from sending to a server, and the consent that suits one does not
|
|
13352
13423
|
* carry to the other, so `events.jsonl` keeps recording the full event union either way and the
|
|
13353
13424
|
* question asked on first run is about the wire. Twenty events are written here; three of them,
|
|
13354
|
-
* carrying
|
|
13425
|
+
* carrying nine fields between them, are the most that is ever sent.
|
|
13355
13426
|
*/
|
|
13356
13427
|
function record(event) {
|
|
13357
13428
|
try {
|
|
@@ -13693,10 +13764,11 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
13693
13764
|
contextId: context.contextId
|
|
13694
13765
|
};
|
|
13695
13766
|
saveJob(job);
|
|
13696
|
-
|
|
13767
|
+
record({
|
|
13697
13768
|
event: "review_started",
|
|
13698
13769
|
reviewId: workspaceId,
|
|
13699
13770
|
agent: agentName(job.env.agentId),
|
|
13771
|
+
reviewType: reviewKindOf(job),
|
|
13700
13772
|
changedFiles: totalChangedFiles(context.changeSet.data),
|
|
13701
13773
|
linesChanged: context.changeSet.data.files.reduce((total, file) => total + (file.additions ?? 0) + (file.deletions ?? 0), 0),
|
|
13702
13774
|
analysedFiles: Math.max(0, context.changeSet.data.files.length - context.inventoryOnlyFiles.length)
|
|
@@ -13777,11 +13849,11 @@ async function drive(jobId, workspaceId, runners, driver, signal) {
|
|
|
13777
13849
|
* finished it. That is the number worth having: it is what somebody actually waited.
|
|
13778
13850
|
*/
|
|
13779
13851
|
function finished(job, workspaceId, startedAt, ok, now) {
|
|
13780
|
-
if (job.reviewType === "kiss") return;
|
|
13781
13852
|
record({
|
|
13782
13853
|
event: "review_finished",
|
|
13783
13854
|
reviewId: workspaceId,
|
|
13784
13855
|
agent: agentName(job.env.agentId),
|
|
13856
|
+
reviewType: reviewKindOf(job),
|
|
13785
13857
|
ok,
|
|
13786
13858
|
durationMs: now() - startedAt
|
|
13787
13859
|
});
|
|
@@ -13831,6 +13903,10 @@ function recordAnalysisRuntime(job, workspaceId, from, durationMs, completedAt,
|
|
|
13831
13903
|
* named ran under Claude. Recording those as unknown would invent a fourth agent out of the
|
|
13832
13904
|
* oldest reviews on disk.
|
|
13833
13905
|
*/
|
|
13906
|
+
/** A job with no `reviewType` predates KISS and is a standard review. */
|
|
13907
|
+
function reviewKindOf(job) {
|
|
13908
|
+
return job.reviewType === "kiss" ? "kiss" : "standard";
|
|
13909
|
+
}
|
|
13834
13910
|
function agentName(id) {
|
|
13835
13911
|
const resolved = id ?? DEFAULT_AGENT_ID;
|
|
13836
13912
|
return isAgentId(resolved) ? resolved : "other";
|
|
@@ -16708,4 +16784,4 @@ function lineageIds(id) {
|
|
|
16708
16784
|
return [.../* @__PURE__ */ new Set([id, ...lineage.map((review) => review.id)])];
|
|
16709
16785
|
}
|
|
16710
16786
|
//#endregion
|
|
16711
|
-
export { MAX_WORKSPACE_PORT as $, isClaimed as $n,
|
|
16787
|
+
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,
|
|
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-B0f6rmy_.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
|
|
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-B0f6rmy_.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/
|
|
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
|
-
|
|
1114
|
-
|
|
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">${
|
|
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(
|
|
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
|
|
2646
|
-
*
|
|
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(
|
|
2649
|
-
if (!text
|
|
2650
|
-
return
|
|
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,
|
|
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);
|
|
@@ -5435,20 +5432,6 @@ function conversationNote(report, actions) {
|
|
|
5435
5432
|
* not exist in any form until the agent has finished reasoning, and drawing their outlines would
|
|
5436
5433
|
* promise a shape nobody has produced.
|
|
5437
5434
|
*/
|
|
5438
|
-
/**
|
|
5439
|
-
* How long this has been going, to the second.
|
|
5440
|
-
*
|
|
5441
|
-
* Seconds rather than the strip's rounded minutes, and that is the whole point of a separate
|
|
5442
|
-
* helper: "Running for under a minute" does not change for a full minute, so the one number on
|
|
5443
|
-
* screen that could show the run is alive sits perfectly still during the wait most likely to be
|
|
5444
|
-
* mistaken for a hang. Past a minute the seconds stop earning their precision and it rounds.
|
|
5445
|
-
*/
|
|
5446
|
-
function elapsedWords(startedAt, now = Date.now()) {
|
|
5447
|
-
const seconds = Math.max(0, Math.round((now - startedAt) / 1e3));
|
|
5448
|
-
if (seconds < 60) return `${seconds} second${seconds === 1 ? "" : "s"}`;
|
|
5449
|
-
const minutes = Math.round(seconds / 60);
|
|
5450
|
-
return `${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
5451
|
-
}
|
|
5452
5435
|
/** Whether the change itself is known yet, which is what Files needs and nothing else does. */
|
|
5453
5436
|
function changeIsRead(job) {
|
|
5454
5437
|
return job.completed.includes("context");
|
|
@@ -5510,9 +5493,15 @@ function renderProgressView(job, fileCount, cancellable = false, history) {
|
|
|
5510
5493
|
${renderElapsed(job, cancellable ? "Cancel review" : null, history)}`;
|
|
5511
5494
|
}
|
|
5512
5495
|
/**
|
|
5513
|
-
*
|
|
5496
|
+
* The run's footer: how long it has run and how long earlier ones took, with Cancel beside them
|
|
5497
|
+
* when something can act on one. One block for a review and an update alike; only the button and
|
|
5498
|
+
* the history's words are the operation's own.
|
|
5499
|
+
*
|
|
5500
|
+
* Running time is the primary line, to the second (`48s`, `1m 08s`), so the one number on screen
|
|
5501
|
+
* that shows the run is alive keeps moving through the whole wait. History is secondary, under it,
|
|
5502
|
+
* and outside it, because the client rewrites that line's text every second. Cancel sits just
|
|
5503
|
+
* after the two rather than at the far edge: an action for this run, not a page control.
|
|
5514
5504
|
*
|
|
5515
|
-
* Beside the line rather than inside it, because the client rewrites that line's text every second.
|
|
5516
5505
|
* Offered only while a driver holds the job: with nothing running there is nothing to stop, and a
|
|
5517
5506
|
* button that could not do what it says would be worse than none. Secondary on purpose: it is a way
|
|
5518
5507
|
* out, not the point. A review has nothing to fall back on, so a cancelled one stops where it was
|
|
@@ -5521,16 +5510,34 @@ function renderProgressView(job, fileCount, cancellable = false, history) {
|
|
|
5521
5510
|
function renderElapsed(job, cancel, history) {
|
|
5522
5511
|
const button = !cancel ? "" : cancel === "Cancel review" ? `<button type="button" class="prb-progress-cancel" data-review-cancel="${escapeHtml$1(job.id)}">Cancel review</button>` : `<button type="button" class="prb-progress-cancel" data-update-cancel="${escapeHtml$1(job.id)}">Cancel update</button>`;
|
|
5523
5512
|
return `<div class="prb-progress-foot">
|
|
5524
|
-
<
|
|
5513
|
+
<div class="prb-progress-runtime">
|
|
5514
|
+
<p class="prb-progress-elapsed" data-started-at="${job.startedAt}">${escapeHtml$1(runningFor(job.startedAt))}</p>${renderHistory$1(history)}
|
|
5515
|
+
</div>
|
|
5525
5516
|
${button}
|
|
5526
|
-
</div
|
|
5517
|
+
</div>`;
|
|
5518
|
+
}
|
|
5519
|
+
/** The running time as the footer and the client's clock both say it. */
|
|
5520
|
+
function runningFor(startedAt, now = Date.now()) {
|
|
5521
|
+
return `Running for ${clockWords(now - startedAt)}`;
|
|
5527
5522
|
}
|
|
5528
5523
|
/**
|
|
5529
|
-
*
|
|
5530
|
-
*
|
|
5524
|
+
* To the second at every length: `48s`, `1m 08s`, `1h 00m 01s`. Not `formatDuration`, which drops
|
|
5525
|
+
* the seconds past an hour because a finished run's length does not need them; a live count does,
|
|
5526
|
+
* or it stands still for a minute at a time on exactly the runs most likely to look hung.
|
|
5527
|
+
*/
|
|
5528
|
+
function clockWords(ms) {
|
|
5529
|
+
const seconds = Math.max(0, Math.round(ms / 1e3));
|
|
5530
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
5531
|
+
if (seconds < 60) return `${seconds}s`;
|
|
5532
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${pad(seconds % 60)}s`;
|
|
5533
|
+
return `${Math.floor(seconds / 3600)}h ${pad(Math.floor(seconds / 60) % 60)}m ${pad(seconds % 60)}s`;
|
|
5534
|
+
}
|
|
5535
|
+
/**
|
|
5536
|
+
* How long this took before: a status line, so no full stop. Nothing when no earlier run
|
|
5537
|
+
* succeeded.
|
|
5531
5538
|
*/
|
|
5532
5539
|
function renderHistory$1(history) {
|
|
5533
|
-
return history ? `<p class="prb-progress-history" data-runtime-history>${escapeHtml$1(history)}
|
|
5540
|
+
return history ? `<p class="prb-progress-history" data-runtime-history>${escapeHtml$1(history)}</p>` : "";
|
|
5534
5541
|
}
|
|
5535
5542
|
/**
|
|
5536
5543
|
* An update running, in place of the review it is updating.
|
|
@@ -11373,7 +11380,7 @@ function buildPrompt$2(workspace, question, sectionId, cwd, context = {}) {
|
|
|
11373
11380
|
].join("\n");
|
|
11374
11381
|
}
|
|
11375
11382
|
/**
|
|
11376
|
-
* The
|
|
11383
|
+
* The kept explanation a follow-up was asked from, however long ago it was written.
|
|
11377
11384
|
*
|
|
11378
11385
|
* Given in full rather than left to the recent turns above, which hold only the last four: a
|
|
11379
11386
|
* reviewer who comes back a week later and presses Ask more is continuing from the explanation on
|
|
@@ -11383,22 +11390,24 @@ function continuingFrom(workspace, continuing) {
|
|
|
11383
11390
|
if (!continuing) return [];
|
|
11384
11391
|
return [
|
|
11385
11392
|
"---",
|
|
11386
|
-
`THE REVIEWER IS CONTINUING FROM THIS PLAIN-ENGLISH EXPLANATION YOU GAVE EARLIER OF ${findingName(workspace, continuing)}:`,
|
|
11393
|
+
`THE REVIEWER IS CONTINUING FROM THIS ${explanationActionOf(continuing) === "explain" ? "PLAIN-ENGLISH" : "TECHNICAL"} EXPLANATION YOU GAVE EARLIER OF ${findingName(workspace, continuing)}:`,
|
|
11387
11394
|
continuing.text
|
|
11388
11395
|
];
|
|
11389
11396
|
}
|
|
11390
11397
|
/**
|
|
11391
|
-
* Explain
|
|
11392
|
-
* moved on. So
|
|
11393
|
-
* asserting what the code does now: a later update or investigation says
|
|
11394
|
-
* holds, and an explanation written as a present-tense fact about the code
|
|
11398
|
+
* Explain (technical) or Explain (simple) on a finding, whose answer is kept on the card and read
|
|
11399
|
+
* again after the code has moved on. So either explains the claim, what the finding says and why it
|
|
11400
|
+
* was raised, rather than asserting what the code does now: a later update or investigation says
|
|
11401
|
+
* whether the claim still holds, and an explanation written as a present-tense fact about the code
|
|
11402
|
+
* would contradict it. They differ only in register, as the visible instruction asks.
|
|
11395
11403
|
*/
|
|
11396
11404
|
function explainingFinding(workspace, explaining) {
|
|
11397
11405
|
if (!explaining) return [];
|
|
11406
|
+
const simple = explaining.action === "explain";
|
|
11398
11407
|
return [
|
|
11399
11408
|
"---",
|
|
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
|
-
|
|
11409
|
+
`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.`,
|
|
11410
|
+
`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
11411
|
];
|
|
11403
11412
|
}
|
|
11404
11413
|
/** `THE ISSUE "<title>"` or `THE QUESTION "<question>"`, or just the kind when the finding is gone. */
|
|
@@ -13221,11 +13230,13 @@ var MAX_KEPT_BRANCHES = 50;
|
|
|
13221
13230
|
*
|
|
13222
13231
|
* Only once the analysis is complete. The page is served while a review is still being made, and
|
|
13223
13232
|
* 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.
|
|
13233
|
+
* that has them, which is worse than recording nothing. `result` is that signal: a running or idle
|
|
13234
|
+
* session carries none. `completedAt` is not: it is the Reviewed status, and gating on it kept this
|
|
13235
|
+
* event off every first read.
|
|
13225
13236
|
*/
|
|
13226
13237
|
function workspaceOpenedEvent(workspace) {
|
|
13227
13238
|
const result = workspace.session.result;
|
|
13228
|
-
if (
|
|
13239
|
+
if (!result) return null;
|
|
13229
13240
|
const severities = {
|
|
13230
13241
|
critical: 0,
|
|
13231
13242
|
major: 0,
|
|
@@ -13694,9 +13705,8 @@ async function handleAsk(req, res, workspace) {
|
|
|
13694
13705
|
const question = typeof body?.question === "string" ? body.question.trim() : "";
|
|
13695
13706
|
if (!question) return send(res, 400, { error: "No question given." });
|
|
13696
13707
|
const sectionId = typeof body?.sectionId === "string" ? body.sectionId : null;
|
|
13697
|
-
const explains =
|
|
13698
|
-
const
|
|
13699
|
-
const continuing = continuesFrom ? simpleExplanationOf(workspace.session, continuesFrom.kind, continuesFrom.id) : void 0;
|
|
13708
|
+
const explains = explanationRequested(workspace.session, body?.explains);
|
|
13709
|
+
const continuing = explanationContinued(workspace, body?.continuesExplanation);
|
|
13700
13710
|
if (liveUpdateFor(workspace.id)) return send(res, 409, {
|
|
13701
13711
|
error: "This review is being updated. Ask again when the new review is ready.",
|
|
13702
13712
|
updating: true
|
|
@@ -13749,18 +13759,22 @@ async function handleAsk(req, res, workspace) {
|
|
|
13749
13759
|
});
|
|
13750
13760
|
let explanation;
|
|
13751
13761
|
mutateWorkspace(workspace.id, (fresh) => {
|
|
13762
|
+
const askedAt = Date.now();
|
|
13763
|
+
const kept = explains && findingNamed(fresh.session, explains) && answer.trim() ? explains : null;
|
|
13752
13764
|
fresh.transcript.push({
|
|
13753
13765
|
question,
|
|
13754
13766
|
answer,
|
|
13755
13767
|
sectionId,
|
|
13756
|
-
askedAt
|
|
13768
|
+
askedAt,
|
|
13769
|
+
...kept ? { explains: kept } : {}
|
|
13757
13770
|
});
|
|
13758
|
-
if (
|
|
13771
|
+
if (kept) {
|
|
13759
13772
|
explanation = {
|
|
13760
|
-
...
|
|
13761
|
-
text: answer.trim()
|
|
13773
|
+
...kept,
|
|
13774
|
+
text: answer.trim(),
|
|
13775
|
+
writtenAt: askedAt
|
|
13762
13776
|
};
|
|
13763
|
-
fresh.session.
|
|
13777
|
+
fresh.session.findingExplanations = withExplanation(fresh.session.findingExplanations, explanation);
|
|
13764
13778
|
}
|
|
13765
13779
|
});
|
|
13766
13780
|
sendIfOpen(res, 200, explanation ? {
|
|
@@ -13771,6 +13785,45 @@ async function handleAsk(req, res, workspace) {
|
|
|
13771
13785
|
endRun(run);
|
|
13772
13786
|
}
|
|
13773
13787
|
}
|
|
13788
|
+
/**
|
|
13789
|
+
* The explanation a follow-up was staged from, named by its finding and `writtenAt`.
|
|
13790
|
+
*
|
|
13791
|
+
* The card's current one when it is still that explanation. When another has replaced it since, the
|
|
13792
|
+
* turn that wrote it, which the transcript keeps, so a question written against one explanation is
|
|
13793
|
+
* never sent with another's words. Nothing when neither is there. A follow-up without `writtenAt`
|
|
13794
|
+
* (an explanation kept before 0.72.0, or a page from then) continues from the card's current one.
|
|
13795
|
+
*/
|
|
13796
|
+
function explanationContinued(workspace, value) {
|
|
13797
|
+
const finding = findingNamed(workspace.session, value);
|
|
13798
|
+
if (!finding) return void 0;
|
|
13799
|
+
const current = explanationOf(workspace.session, finding.kind, finding.id);
|
|
13800
|
+
const writtenAt = value.writtenAt;
|
|
13801
|
+
if (writtenAt === void 0 || current?.writtenAt === writtenAt) return current;
|
|
13802
|
+
const turn = workspace.transcript.find((exchange) => exchange.askedAt === writtenAt && exchange.explains?.kind === finding.kind && exchange.explains.id === finding.id);
|
|
13803
|
+
return turn?.explains && turn.answer.trim() ? {
|
|
13804
|
+
...finding,
|
|
13805
|
+
action: turn.explains.action,
|
|
13806
|
+
text: turn.answer.trim(),
|
|
13807
|
+
writtenAt: turn.askedAt
|
|
13808
|
+
} : void 0;
|
|
13809
|
+
}
|
|
13810
|
+
/**
|
|
13811
|
+
* The finding a turn explains and the action explaining it, or null when its answer is not to be kept.
|
|
13812
|
+
*
|
|
13813
|
+
* A missing action is Explain (simple): the only one a page sent before there were two, and a tab
|
|
13814
|
+
* opened then still sends it that way. An action that is named but not an explanation is not
|
|
13815
|
+
* guessed at; the turn is answered like any other and nothing is kept.
|
|
13816
|
+
*/
|
|
13817
|
+
function explanationRequested(session, value) {
|
|
13818
|
+
const finding = findingNamed(session, value);
|
|
13819
|
+
if (!finding) return null;
|
|
13820
|
+
const named = value.action;
|
|
13821
|
+
const action = named === void 0 ? "explain" : named;
|
|
13822
|
+
return isExplanationAction(action) ? {
|
|
13823
|
+
...finding,
|
|
13824
|
+
action
|
|
13825
|
+
} : null;
|
|
13826
|
+
}
|
|
13774
13827
|
/** An issue or question of this review, named by kind and id; null for anything else. */
|
|
13775
13828
|
function findingNamed(session, value) {
|
|
13776
13829
|
const named = value;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.73.1",
|
|
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",
|