prreviewbuddy 0.52.2 → 0.53.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 +38 -0
- package/dist/{delete_review-BiV1aBpx.js → delete_review-DMJXD3bf.js} +175 -82
- package/dist/main.js +1 -1
- package/dist/server.js +357 -57
- package/package.json +1 -1
- package/static/shell.css +145 -22
- package/static/workspace.js +35 -27
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as compactQueue, $t as
|
|
1
|
+
import { $ as compactQueue, $t as latestKissRun, A as createPrState, An as isIssueOutstanding, At as followedBranch, B as toneOf, Bt as phasesFor, C as readEvents, Cn as sectionOf, Ct as claimRequestUrl, D as announceJob, Dn as queueStamp, Dt as fillFileUrlTemplate, E as telemetryUploadConsent, En as queueRemove, Et as remoteHost, F as markAllRead, Fn as processDiscussion, Ft as isTerminal, G as extractJsonText, Gn as resolveModel, Gt as isClaimed, H as withUsageRecorded, Hn as isCommitReview, Ht as purposeOf, I as markRead, In as recommendationsOf, It as loadJob, J as readMarker, Jn as writeAgentPreference, Jt as groupByLineage, K as ReviewBeingDeletedError, Kn as clearAgentPreference, Kt as decideKissRecommendation, L as pruneNotifications, Ln as describeAuthorship, Lt as saveJob, M as eventName, Mn as issuesOutstanding, Mt as allJobs, N as headline, Nn as questionsOutstanding, Nt as endedAt, O as branchOf, On as readQueue, Ot as checkCodeFreshness, P as listNotifications, Pn as reviewerDispositions, Pt as fail, Q as activateQueued, Qt as kissDecisionsOf, R as readNotification, Rn as WITHHELD_REASON, Sn as parseLifecycle, St as writePortPreference, T as recordTelemetryUploadConsent, Tn as queueAddMany, Tt as forgeFor, U as EXPLAIN_SIMPLY_PROMPT, Un as git, V as unreadCount$1, Vt as progressSteps, W as asString, Xn as STORE_ROOT, Xt as indexReviewGroups, Y as removeWorktree, Yt as indexKeptIds, Z as relativeTime$1, Zn as AGENT_IDS, Zt as kissDecisionRefusal, _ as agentEnvOf, _n as isLifecycle, _t as MAX_WORKSPACE_PORT, a as wasBlocked, bn as lifecyclePill, bt as resolveWorkspacePort, c as RefreshUnavailableError, cn as recentWorkspaces, ct as serverEntryExists, d as refreshPrContext, er as agentFor, f as isUnchanged, fn as touchWorkspace, ft as writeServerRecord, gn as activityLabel, gt as DEFAULT_WORKSPACE_PORT, h as liveUpdateFor, hn as LIFECYCLES, ht as feedbackUrl, i as liveJobsFor, in as mutateWorkspace, it as clearServerRecord, j as createPrUpdate, jn as isQuestionOutstanding, jt as reviewedShaOf, k as subjectOf, kn as doneVerb, kt as checkReview, l as mergeUpdateInto, ln as reviewedCommit, lt as spawnServer, m as UpdateAlreadyRunningError, mn as workspaceStamps, n as deleteReview, nn as loadWorkspace, nr as AgentCancelledError, nt as readIndexToken, o as discardJob, on as presentedKissRunOf, ot as readServerRecord, p as askCheckout, pn as workspaceRevision, pt as BUILD_VERSION, q as MANAGED_ROOT, qn as readAgentPreference, qt as followedRefName, r as lineageIds, rr as AgentUnavailableError, s as FETCH_TIMEOUT_MS, sn as previousKissResult, t as deleteLineage, tn as lineagePosition, tt as settleQueue, u as updateReview, vn as lifecycleAction, vt as MIN_WORKSPACE_PORT, w as record, wn as queueAdd, x as runJob, xn as offersLifecycle, xt as statedWorkspacePort, y as startKissJob, yn as lifecycleChip, yt as parseWorkspacePort, z as removeNotification, zn as kissOutcome, zt as isLiveJob } from "./delete_review-DMJXD3bf.js";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
@@ -839,13 +839,28 @@ var FINDING_COPY_CLASS = "pr-helper-finding-copy";
|
|
|
839
839
|
* a chat reply's "Copy message".
|
|
840
840
|
*/
|
|
841
841
|
function renderFindingCopySource(source, kind) {
|
|
842
|
-
const name = kind === "issue" ? "Copy finding" : "Copy question";
|
|
842
|
+
const name = kind === "issue" ? "Copy finding" : kind === "question" ? "Copy question" : "Copy recommendation";
|
|
843
843
|
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`, {
|
|
844
844
|
label: "",
|
|
845
845
|
aria: name,
|
|
846
846
|
tip: name
|
|
847
847
|
});
|
|
848
848
|
}
|
|
849
|
+
/**
|
|
850
|
+
* The same control with its word showing, for a footer of worded actions.
|
|
851
|
+
*
|
|
852
|
+
* A KISS card's Copy draft comment sits among Plan to simplify and Leave it, where an icon alone
|
|
853
|
+
* would read as decoration. `extra` is for attributes the caller's own client needs, such as the
|
|
854
|
+
* handle Copy recommendations is found by; the source may be updated after render, since the
|
|
855
|
+
* listener reads it at the moment of the click.
|
|
856
|
+
*/
|
|
857
|
+
function renderLabelledCopySource(source, label, extra = "") {
|
|
858
|
+
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"${extra ? ` ${extra}` : ""}`, {
|
|
859
|
+
label,
|
|
860
|
+
aria: label,
|
|
861
|
+
tip: label
|
|
862
|
+
});
|
|
863
|
+
}
|
|
849
864
|
/** Where a finding's button keeps its resting word and name. Absent on a chat reply's. */
|
|
850
865
|
var IDLE_LABEL_ATTR = "data-copy-idle-label";
|
|
851
866
|
var IDLE_ARIA_ATTR = "data-copy-idle-aria";
|
|
@@ -3565,6 +3580,58 @@ function verdictLabel(verdict) {
|
|
|
3565
3580
|
return VERDICT_LABELS[verdict] ?? verdict;
|
|
3566
3581
|
}
|
|
3567
3582
|
//#endregion
|
|
3583
|
+
//#region ../../packages/review-harness/src/domain/kiss/handoff.ts
|
|
3584
|
+
function handoffSource(recommendation) {
|
|
3585
|
+
const { id, title, gist, complexity, whyNotJustified, simplerDirection, draftComment, locations } = recommendation;
|
|
3586
|
+
return {
|
|
3587
|
+
id,
|
|
3588
|
+
title,
|
|
3589
|
+
...gist ? { gist } : {},
|
|
3590
|
+
...complexity ? { complexity } : {},
|
|
3591
|
+
whyNotJustified,
|
|
3592
|
+
simplerDirection,
|
|
3593
|
+
...draftComment ? { draftComment } : {},
|
|
3594
|
+
locations
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
/**
|
|
3598
|
+
* One recommendation: the title, why it matters, the simpler approach, where to start, and the
|
|
3599
|
+
* draft comment when the run wrote one.
|
|
3600
|
+
*
|
|
3601
|
+
* "Why it matters" is the gist, which carries the case in a sentence or two. A schema 1 record has
|
|
3602
|
+
* none, and its `whyNotJustified` is the field of that shape that says why the complexity is not
|
|
3603
|
+
* needed, so it stands in rather than leaving the line out.
|
|
3604
|
+
*/
|
|
3605
|
+
function recommendationMarkdown(recommendation) {
|
|
3606
|
+
const why = recommendation.gist || recommendation.whyNotJustified;
|
|
3607
|
+
const primary = recommendation.locations.find((location) => location.primary) ?? recommendation.locations[0];
|
|
3608
|
+
return [
|
|
3609
|
+
`### ${recommendation.title.trim()}`,
|
|
3610
|
+
why ? `**Why it matters:** ${why.trim()}` : "",
|
|
3611
|
+
`**Simpler approach:** ${recommendation.simplerDirection.trim()}`,
|
|
3612
|
+
primary ? `**Start here:** \`${primary.line ? `${primary.path}:${primary.line}` : primary.path}\`` : "",
|
|
3613
|
+
recommendation.draftComment?.trim() ? `**Draft comment:**\n\n${quote(recommendation.draftComment.trim())}` : ""
|
|
3614
|
+
].filter((part) => part !== "").join("\n\n");
|
|
3615
|
+
}
|
|
3616
|
+
/** The reviewer's handoff: the recommendations they chose to raise, in the order given. */
|
|
3617
|
+
function handoffMarkdown(recommendations) {
|
|
3618
|
+
if (recommendations.length === 0) return "";
|
|
3619
|
+
return [`## Simplification suggestions (${recommendations.length})`, ...recommendations.map(recommendationMarkdown)].join("\n\n");
|
|
3620
|
+
}
|
|
3621
|
+
function quote(text) {
|
|
3622
|
+
return text.split("\n").map((line) => line === "" ? ">" : `> ${line}`).join("\n");
|
|
3623
|
+
}
|
|
3624
|
+
/**
|
|
3625
|
+
* The sources as JSON that is safe inside a `<script type="application/json">` element.
|
|
3626
|
+
*
|
|
3627
|
+
* JSON unicode escapes rather than HTML entities: `JSON.parse` restores the original characters,
|
|
3628
|
+
* while `<\/script>` in a title or a draft can no longer end the element early. `&` is escaped too
|
|
3629
|
+
* so no sequence in the text can be read as markup by anything that looks at the raw element.
|
|
3630
|
+
*/
|
|
3631
|
+
function embedHandoffSources(sources) {
|
|
3632
|
+
return JSON.stringify(sources).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
3633
|
+
}
|
|
3634
|
+
//#endregion
|
|
3568
3635
|
//#region ../../packages/review-harness/src/workspace/kiss_view.ts
|
|
3569
3636
|
/**
|
|
3570
3637
|
* The KISS review, on the page.
|
|
@@ -3628,11 +3695,13 @@ var INTENTS = [{
|
|
|
3628
3695
|
audience: "author",
|
|
3629
3696
|
label: "Review my code",
|
|
3630
3697
|
describes: "Reviewing my code",
|
|
3698
|
+
perspective: "My code",
|
|
3631
3699
|
consequence: "Get direct suggestions to simplify your implementation."
|
|
3632
3700
|
}, {
|
|
3633
3701
|
audience: "reviewer",
|
|
3634
3702
|
label: "Review someone else's code",
|
|
3635
3703
|
describes: "Reviewing someone else's code",
|
|
3704
|
+
perspective: "Someone else's code",
|
|
3636
3705
|
consequence: "Decide which simplifications are worth raising with the author."
|
|
3637
3706
|
}];
|
|
3638
3707
|
/**
|
|
@@ -3648,6 +3717,61 @@ function audienceSentence(audience) {
|
|
|
3648
3717
|
return INTENTS.find((intent) => intent.audience === audience)?.describes ?? "";
|
|
3649
3718
|
}
|
|
3650
3719
|
/**
|
|
3720
|
+
* The two decisions, in each audience's words.
|
|
3721
|
+
*
|
|
3722
|
+
* One stored value, two vocabularies: an author accepts by planning the change and a reviewer by
|
|
3723
|
+
* raising it, and neither declines by dismissing, because declining is not a verdict that the
|
|
3724
|
+
* recommendation was wrong. Total over both unions, so a new audience or decision will not build
|
|
3725
|
+
* without its words. Exported for the client, which relabels a card after a click.
|
|
3726
|
+
*/
|
|
3727
|
+
var DECISION_LABELS = {
|
|
3728
|
+
author: {
|
|
3729
|
+
accepted: "Plan to simplify",
|
|
3730
|
+
declined: "Keep as implemented"
|
|
3731
|
+
},
|
|
3732
|
+
reviewer: {
|
|
3733
|
+
accepted: "Raise with author",
|
|
3734
|
+
declined: "Leave it"
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
3737
|
+
/** What a card's status pill says. "Open" until decided, then the decision in the audience's words. */
|
|
3738
|
+
function decisionStatusLabel(audience, decision) {
|
|
3739
|
+
return decision === "undecided" ? "Open" : DECISION_LABELS[audience][decision];
|
|
3740
|
+
}
|
|
3741
|
+
/** The line over the list. Shared with the client so a click and a reload say the same thing. */
|
|
3742
|
+
function kissProgressBody(total, open) {
|
|
3743
|
+
return open === 0 ? `<span class="pr-helper-questions-progress-check" aria-hidden="true">\u2713</span><span class="pr-helper-questions-progress-count">All ${total} decided</span>` : `<span class="pr-helper-questions-progress-count">${open} of ${total} undecided</span>`;
|
|
3744
|
+
}
|
|
3745
|
+
/** The drawer's heading. Shared with the client for the same reason. */
|
|
3746
|
+
function decidedDrawerTitle(count) {
|
|
3747
|
+
return `Decided (${count})`;
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* Whether the reviewer's handoff can be copied, and the sentence that says why or why not.
|
|
3751
|
+
*
|
|
3752
|
+
* Stale comes first: handing off advice about commits the branch has moved past presents old advice
|
|
3753
|
+
* as current, however carefully it was chosen. The sentence is visible text beside the button
|
|
3754
|
+
* rather than a tooltip, so a disabled control always says what would enable it.
|
|
3755
|
+
*/
|
|
3756
|
+
function handoffState(accepted, stale, saving = false) {
|
|
3757
|
+
if (saving) return {
|
|
3758
|
+
enabled: false,
|
|
3759
|
+
note: "Saving your decision. You can copy once it is saved."
|
|
3760
|
+
};
|
|
3761
|
+
if (stale) return {
|
|
3762
|
+
enabled: false,
|
|
3763
|
+
note: "Run KISS again before handing these recommendations off."
|
|
3764
|
+
};
|
|
3765
|
+
if (accepted === 0) return {
|
|
3766
|
+
enabled: false,
|
|
3767
|
+
note: "Choose Raise with author on the recommendations you want to hand off."
|
|
3768
|
+
};
|
|
3769
|
+
return {
|
|
3770
|
+
enabled: true,
|
|
3771
|
+
note: `${accepted === 1 ? "1 recommendation" : `${accepted} recommendations`} to raise with the author.`
|
|
3772
|
+
};
|
|
3773
|
+
}
|
|
3774
|
+
/**
|
|
3651
3775
|
* The KISS summary: the assessment, what drives it, and the complexity worth keeping.
|
|
3652
3776
|
*
|
|
3653
3777
|
* This is the pane the rail's KISS entry opens, so it carries every state the section can be in.
|
|
@@ -3659,13 +3783,53 @@ function renderKissSummary(view) {
|
|
|
3659
3783
|
if (!run) return renderKissEmpty(view.canRun);
|
|
3660
3784
|
if (run.status === "generating") return renderRunning(run, view.job ?? null);
|
|
3661
3785
|
if (run.status === "failed" || !run.result) return renderFailed(run, view);
|
|
3662
|
-
return `${renderDocHead("KISS review", `${renderAnalysisDetails(provenanceFacts(run))}${KISS_TOGGLE}`)}
|
|
3663
|
-
|
|
3786
|
+
return `${renderDocHead("KISS review", `${renderPerspective(run.audience)}${renderAnalysisDetails(provenanceFacts(run))}${renderRunAgain(view.canRun)}${KISS_TOGGLE}`)}
|
|
3787
|
+
${renderFreshRegion(run, view)}
|
|
3788
|
+
${renderResultBody(run, "header")}
|
|
3789
|
+
${renderRunDialog(view.canRun, run.audience)}`;
|
|
3790
|
+
}
|
|
3791
|
+
/**
|
|
3792
|
+
* Whether the answer on the page still describes the branch, for the run the page presents.
|
|
3793
|
+
*
|
|
3794
|
+
* Drawn wherever a result is shown, including the earlier answer under a failed rerun: that answer
|
|
3795
|
+
* is the one being decided and handed off, and without this region the client has nowhere to repaint
|
|
3796
|
+
* a push it notices later.
|
|
3797
|
+
*/
|
|
3798
|
+
function renderFreshRegion(run, view) {
|
|
3799
|
+
return `<div class="prb-kiss-fresh-region" data-kiss-fresh-region data-reviewed-sha="${escapeHtml$1(run.reviewedSha)}"${view.freshness ? ` data-freshness="${escapeHtml$1(JSON.stringify(view.freshness))}"` : ""}${view.dismissedFreshnessSha ? ` data-dismissed-sha="${escapeHtml$1(view.dismissedFreshnessSha)}"` : ""}>${renderKissFreshness(run.reviewedSha, view.freshness ?? null, {
|
|
3664
3800
|
dismissedSha: view.dismissedFreshnessSha,
|
|
3665
3801
|
actions: view.canRun
|
|
3666
|
-
})}</div
|
|
3667
|
-
|
|
3668
|
-
|
|
3802
|
+
})}</div>`;
|
|
3803
|
+
}
|
|
3804
|
+
/**
|
|
3805
|
+
* Whose code the run was about, said as configuration rather than as part of the report.
|
|
3806
|
+
*
|
|
3807
|
+
* A compact line in the header rather than the question left standing under the result: the
|
|
3808
|
+
* choice was made, and what a finished report owes the reader is which choice it was.
|
|
3809
|
+
*/
|
|
3810
|
+
function renderPerspective(audience) {
|
|
3811
|
+
const intent = INTENTS.find((candidate) => candidate.audience === audience);
|
|
3812
|
+
if (!intent) return "";
|
|
3813
|
+
return `<span class="prb-kiss-perspective" data-kiss-perspective="${audience}">Perspective: ${escapeHtml$1(intent.perspective)}</span>`;
|
|
3814
|
+
}
|
|
3815
|
+
/** Opens the run dialog. Beside Analysis details, because running again is a fact about the run too. */
|
|
3816
|
+
function renderRunAgain(canRun) {
|
|
3817
|
+
return canRun ? "<button type=\"button\" class=\"prb-kiss-run-again\" data-kiss-run-open>Run again</button>" : "";
|
|
3818
|
+
}
|
|
3819
|
+
/**
|
|
3820
|
+
* The run control once a result exists: the same question, asked in a dialog.
|
|
3821
|
+
*
|
|
3822
|
+
* The previous run's audience is preselected, and the choice is still shown and still has to be
|
|
3823
|
+
* confirmed by pressing Run. Preselecting carries yesterday's framing forward only as far as the
|
|
3824
|
+
* reader can see it, which is the difference from the remembered default the empty state refuses.
|
|
3825
|
+
*/
|
|
3826
|
+
function renderRunDialog(canRun, audience) {
|
|
3827
|
+
if (!canRun) return "";
|
|
3828
|
+
return `<dialog class="prb-kiss-run-dialog" data-kiss-run-dialog aria-labelledby="prb-kiss-run-dialog-title">
|
|
3829
|
+
<h4 class="prb-kiss-run-dialog-title" id="prb-kiss-run-dialog-title">Run KISS review again</h4>
|
|
3830
|
+
${renderRunControl(true, "Run KISS review", audience)}
|
|
3831
|
+
<button type="button" class="prb-kiss-run-cancel" data-kiss-run-cancel>Cancel</button>
|
|
3832
|
+
</dialog>`;
|
|
3669
3833
|
}
|
|
3670
3834
|
/**
|
|
3671
3835
|
* A completed run's answer, from the outcome down to how it was made.
|
|
@@ -3692,12 +3856,15 @@ ${provenance === "header" ? "" : renderProvenance(run)}`;
|
|
|
3692
3856
|
}
|
|
3693
3857
|
function renderKissFreshness(reviewedSha, freshness, options = {}) {
|
|
3694
3858
|
if (!freshness || freshness.state !== "changed") return "";
|
|
3695
|
-
|
|
3859
|
+
const runAgain = options.actions ? "<button type=\"button\" class=\"prb-kiss-fresh-run\" data-kiss-run-open>Run again</button>" : "";
|
|
3860
|
+
if (options.dismissedSha && options.dismissedSha === freshness.latestSha) return `<div class="prb-kiss-fresh" data-kiss-freshness="changed" data-collapsed role="status">
|
|
3861
|
+
<p><strong>This KISS review describes commit ${escapeHtml$1(reviewedSha.slice(0, 7))}.</strong> The branch has moved on since. ${runAgain}</p>
|
|
3862
|
+
</div>`;
|
|
3696
3863
|
const moved = freshness.rewritten ? "The branch has been rewritten since, so this report describes commits that are no longer on it." : freshness.newCommits !== null ? `There ${freshness.newCommits === 1 ? "has been 1 commit" : `have been ${freshness.newCommits} commits`} on the branch since.` : "The branch has moved on since.";
|
|
3697
3864
|
return `<div class="prb-kiss-fresh" data-kiss-freshness="changed" role="status">
|
|
3698
3865
|
<p><strong>This KISS review describes commit ${escapeHtml$1(reviewedSha.slice(0, 7))}.</strong> ${escapeHtml$1(moved)}</p>
|
|
3699
|
-
<p class="prb-kiss-fresh-note">Running KISS again analyses the branch as it is now. The report below
|
|
3700
|
-
${options.actions && freshness.latestSha ? `<button type="button" class="prb-kiss-fresh-dismiss" data-kiss-freshness-dismiss="${escapeHtml$1(freshness.latestSha)}">Not now</button>` : ""}
|
|
3866
|
+
<p class="prb-kiss-fresh-note">Running KISS again analyses the branch as it is now. The report below and your decisions about it are kept either way.</p>
|
|
3867
|
+
<p class="prb-kiss-fresh-actions">${runAgain}${options.actions && freshness.latestSha ? `<button type="button" class="prb-kiss-fresh-dismiss" data-kiss-freshness-dismiss="${escapeHtml$1(freshness.latestSha)}">Not now</button>` : ""}</p>
|
|
3701
3868
|
</div>`;
|
|
3702
3869
|
}
|
|
3703
3870
|
/**
|
|
@@ -3718,8 +3885,73 @@ function renderKissRecommendations(view) {
|
|
|
3718
3885
|
<h4 class="prb-kiss-outcome-title">Nothing to simplify</h4>
|
|
3719
3886
|
<p class="prb-kiss-outcome-body">The analysis completed and did not identify a materially simpler way to meet these requirements.</p>
|
|
3720
3887
|
</div>`;
|
|
3888
|
+
const decisions = decisionsFor(view, presented);
|
|
3889
|
+
const stale = view.freshness?.state === "changed";
|
|
3890
|
+
const card = (recommendation, rank) => renderRecommendation(recommendation, {
|
|
3891
|
+
runId: presented.id,
|
|
3892
|
+
audience: presented.audience,
|
|
3893
|
+
decision: decisions.get(recommendation.id) ?? "undecided",
|
|
3894
|
+
rank,
|
|
3895
|
+
stale,
|
|
3896
|
+
actions: view.canRun
|
|
3897
|
+
});
|
|
3898
|
+
const recommendations = recommendationsOf(result);
|
|
3899
|
+
const ranked = recommendations.map((recommendation, rank) => ({
|
|
3900
|
+
recommendation,
|
|
3901
|
+
rank
|
|
3902
|
+
}));
|
|
3903
|
+
const open = ranked.filter(({ recommendation }) => !decisions.has(recommendation.id));
|
|
3904
|
+
const decided = ranked.filter(({ recommendation }) => decisions.has(recommendation.id));
|
|
3905
|
+
const total = recommendations.length;
|
|
3721
3906
|
return `${RECS_HEAD}${earlier}${renderWithheldNote(presented)}
|
|
3722
|
-
${
|
|
3907
|
+
${presented.audience === "reviewer" ? renderHandoff(recommendations, decisions, stale) : ""}
|
|
3908
|
+
<div class="pr-helper-questions-progress prb-kiss-progress" data-kiss-progress role="status">${kissProgressBody(total, open.length)}</div>
|
|
3909
|
+
<div class="prb-kiss-open-list" data-kiss-open-list>${open.map(({ recommendation, rank }) => card(recommendation, rank)).join("")}</div>
|
|
3910
|
+
<details class="pr-helper-closed-findings prb-kiss-decided" data-kiss-decided${decided.length === 0 ? " hidden" : ""}${open.length === 0 ? " data-nothing-open" : ""}>
|
|
3911
|
+
<summary class="pr-helper-closed-findings-summary" data-kiss-decided-summary>${escapeHtml$1(decidedDrawerTitle(decided.length))}</summary>
|
|
3912
|
+
<div class="pr-helper-closed-findings-list" data-kiss-decided-list>${decided.map(({ recommendation, rank }) => card(recommendation, rank)).join("")}</div>
|
|
3913
|
+
</details>`;
|
|
3914
|
+
}
|
|
3915
|
+
/**
|
|
3916
|
+
* What was decided about the run this pane presents, by recommendation id.
|
|
3917
|
+
*
|
|
3918
|
+
* Only decisions about recommendations the run still lists: a stored decision is never shown on a
|
|
3919
|
+
* card it was not made about, and a recommendation withheld as incomplete has no card to decide on.
|
|
3920
|
+
*/
|
|
3921
|
+
function decisionsFor(view, run) {
|
|
3922
|
+
const listed = new Set(recommendationsOf(run.result).map((recommendation) => recommendation.id));
|
|
3923
|
+
return new Map((view.decisions ?? []).filter((entry) => entry.runId === run.id && listed.has(entry.recommendationId)).map((entry) => [entry.recommendationId, entry.decision]));
|
|
3924
|
+
}
|
|
3925
|
+
/** The decisions made about the run the panes present, for the rail. */
|
|
3926
|
+
function kissDecisionCounts(view) {
|
|
3927
|
+
const run = presentedKissRun(view);
|
|
3928
|
+
if (!run?.result) return null;
|
|
3929
|
+
const total = recommendationsOf(run.result).length;
|
|
3930
|
+
return {
|
|
3931
|
+
total,
|
|
3932
|
+
open: total - decisionsFor(view, run).size
|
|
3933
|
+
};
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* The reviewer's handoff: what they chose to raise, as Markdown to paste.
|
|
3937
|
+
*
|
|
3938
|
+
* An export of the decisions on this page, not a report of its own. The Markdown comes from one
|
|
3939
|
+
* helper, over the model's own fields embedded beside the button, so what is copied is never read
|
|
3940
|
+
* back out of the rendered cards. The button's source is filled here for the decisions the page
|
|
3941
|
+
* loaded with, and refilled by the client after every decision.
|
|
3942
|
+
*
|
|
3943
|
+
* Reviewer runs only: an author was never going to hand these to anybody.
|
|
3944
|
+
*/
|
|
3945
|
+
function renderHandoff(recommendations, decisions, stale) {
|
|
3946
|
+
const accepted = recommendations.filter((recommendation) => decisions.get(recommendation.id) === "accepted");
|
|
3947
|
+
const state = handoffState(accepted.length, stale);
|
|
3948
|
+
const sources = recommendations.map(handoffSource);
|
|
3949
|
+
const copy = renderLabelledCopySource(state.enabled ? handoffMarkdown(accepted.map(handoffSource)) : "", "Copy recommendations", `data-kiss-handoff-copy aria-describedby="prb-kiss-handoff-note"${state.enabled ? "" : " disabled"}`);
|
|
3950
|
+
return `<div class="prb-kiss-handoff" data-kiss-handoff${stale ? " data-stale" : ""}>
|
|
3951
|
+
${copy}
|
|
3952
|
+
<p class="prb-kiss-handoff-note" id="prb-kiss-handoff-note" data-kiss-handoff-note>${escapeHtml$1(state.note)}</p>
|
|
3953
|
+
<script type="application/json" data-kiss-handoff-source>${embedHandoffSources(sources)}<\/script>
|
|
3954
|
+
</div>`;
|
|
3723
3955
|
}
|
|
3724
3956
|
/**
|
|
3725
3957
|
* A run whose every recommendation was withheld: incomplete, and said to be.
|
|
@@ -3780,22 +4012,23 @@ ${renderRunControl(canRun, "Run KISS review")}`;
|
|
|
3780
4012
|
* the reader that the page argues with them; one that is plainly waiting on the choice above it
|
|
3781
4013
|
* teaches them what the choice is for.
|
|
3782
4014
|
*
|
|
3783
|
-
* Nothing is preselected
|
|
3784
|
-
*
|
|
3785
|
-
*
|
|
4015
|
+
* Nothing is preselected on a first run: there is no earlier answer, and a default would be the
|
|
4016
|
+
* page guessing whose code this is. A rerun preselects the audience the run it replaces was asked
|
|
4017
|
+
* for, inside a dialog that shows both choices and waits for Run, so the framing carried forward is
|
|
4018
|
+
* one the reader can see and change rather than one remembered for them.
|
|
3786
4019
|
*/
|
|
3787
|
-
function renderRunControl(canRun, label) {
|
|
4020
|
+
function renderRunControl(canRun, label, preselected) {
|
|
3788
4021
|
if (!canRun) return "";
|
|
3789
4022
|
return `<div class="prb-kiss-run" data-kiss-run-control>
|
|
3790
4023
|
<fieldset class="prb-kiss-intents">
|
|
3791
4024
|
<legend>Whose code is this?</legend>
|
|
3792
4025
|
${INTENTS.map((intent) => `<label class="prb-kiss-intent">
|
|
3793
|
-
<input type="radio" name="kiss-audience" value="${intent.audience}" data-kiss-audience>
|
|
4026
|
+
<input type="radio" name="kiss-audience${preselected ? "-again" : ""}" value="${intent.audience}" data-kiss-audience${intent.audience === preselected ? " checked" : ""}>
|
|
3794
4027
|
<span class="prb-kiss-intent-label">${escapeHtml$1(intent.label)}</span>
|
|
3795
4028
|
<span class="prb-kiss-intent-note">${escapeHtml$1(intent.consequence)}</span>
|
|
3796
4029
|
</label>`).join("")}
|
|
3797
4030
|
</fieldset>
|
|
3798
|
-
<button type="button" class="prb-kiss-start" data-kiss-run disabled>${escapeHtml$1(label)}</button>
|
|
4031
|
+
<button type="button" class="prb-kiss-start" data-kiss-run${preselected ? "" : " disabled"}>${escapeHtml$1(label)}</button>
|
|
3799
4032
|
</div>`;
|
|
3800
4033
|
}
|
|
3801
4034
|
/**
|
|
@@ -3833,15 +4066,15 @@ function renderRunning(run, job) {
|
|
|
3833
4066
|
*/
|
|
3834
4067
|
function renderFailed(run, view) {
|
|
3835
4068
|
const standing = view.previous ?? null;
|
|
3836
|
-
return `${renderDocHead("KISS review", KISS_TOGGLE)}
|
|
4069
|
+
return `${renderDocHead("KISS review", `${standing ? renderRunAgain(view.canRun) : ""}${KISS_TOGGLE}`)}
|
|
3837
4070
|
<div class="prb-kiss-outcome" data-review-outcome="failed">
|
|
3838
4071
|
<h4 class="prb-kiss-outcome-title">KISS review could not be completed</h4>
|
|
3839
4072
|
<p class="prb-kiss-outcome-body">${escapeHtml$1(run.error ?? "The analysis did not finish.")}</p>
|
|
3840
4073
|
${standing ? "" : "<p class=\"prb-kiss-outcome-note\">Nothing was recorded from this run.</p>"}
|
|
3841
4074
|
${renderRejectedAnswer(run)}
|
|
3842
4075
|
</div>
|
|
3843
|
-
${standing ? renderEarlier(standing) : ""}
|
|
3844
|
-
${renderRunControl(view.canRun, "Run KISS review again")}`;
|
|
4076
|
+
${standing ? `${renderFreshRegion(standing, view)}${renderEarlier(standing)}` : ""}
|
|
4077
|
+
${standing ? renderRunDialog(view.canRun, run.audience) : renderRunControl(view.canRun, "Run KISS review again", run.audience)}`;
|
|
3845
4078
|
}
|
|
3846
4079
|
/**
|
|
3847
4080
|
* What the agent actually replied, folded away.
|
|
@@ -3935,14 +4168,14 @@ function renderProvenance(run) {
|
|
|
3935
4168
|
*/
|
|
3936
4169
|
function renderJustified(items) {
|
|
3937
4170
|
if (items.length === 0) return "";
|
|
3938
|
-
return `<
|
|
3939
|
-
<
|
|
3940
|
-
${items.map((item) => `<
|
|
3941
|
-
<
|
|
4171
|
+
return `<details class="prb-kiss-justified" data-kiss-justified>
|
|
4172
|
+
<summary class="prb-kiss-justified-summary">Complexity worth keeping (${items.length})</summary>
|
|
4173
|
+
${items.map((item) => `<details class="prb-kiss-kept" data-kiss-kept>
|
|
4174
|
+
<summary class="prb-kiss-kept-summary"><span class="prb-kiss-kept-title">${escapeHtml$1(item.title)}</span>${item.reason ? `<span class="prb-kiss-kept-reason" data-kiss-kept-reason>${escapeHtml$1(item.reason)}</span>` : ""}</summary>
|
|
3942
4175
|
<div class="pr-helper-markdown">${renderMarkdown(item.rationale)}</div>
|
|
3943
4176
|
${renderLocations(item.locations)}
|
|
3944
|
-
</
|
|
3945
|
-
</
|
|
4177
|
+
</details>`).join("")}
|
|
4178
|
+
</details>`;
|
|
3946
4179
|
}
|
|
3947
4180
|
/**
|
|
3948
4181
|
* What validation had to withdraw, disclosed rather than swallowed.
|
|
@@ -3964,26 +4197,20 @@ function renderAudit(run) {
|
|
|
3964
4197
|
${notes.map((note) => `<p>${escapeHtml$1(note)}</p>`).join("")}
|
|
3965
4198
|
</div>`;
|
|
3966
4199
|
}
|
|
3967
|
-
|
|
3968
|
-
* One recommendation, as a decision aid rather than an analysis record.
|
|
3969
|
-
*
|
|
3970
|
-
* The card carries what a reviewer needs to understand and weigh the recommendation: the title,
|
|
3971
|
-
* the case in a sentence or two, the simpler approach, what it gains, the trade-off that could
|
|
3972
|
-
* change the decision, and where to start. Everything that exists to audit or challenge it sits in
|
|
3973
|
-
* one disclosure beneath, closed until asked for.
|
|
3974
|
-
*
|
|
3975
|
-
* Reads both schemas. A record written under schema 1 has no gist, and its `complexity` stands in
|
|
3976
|
-
* as the lede, because it is the only field of that shape that names what the card is about; it is
|
|
3977
|
-
* then not repeated in the disclosure. Stored runs are history and are not rewritten to fit.
|
|
3978
|
-
*/
|
|
3979
|
-
function renderRecommendation(recommendation) {
|
|
4200
|
+
function renderRecommendation(recommendation, context) {
|
|
3980
4201
|
const worthiness = recommendation.commentWorthiness;
|
|
3981
4202
|
const lede = recommendation.gist || recommendation.complexity || "";
|
|
3982
4203
|
const [start, ...rest] = orderedLocations(recommendation.locations);
|
|
3983
|
-
return `<article class="prb-kiss-rec" data-payoff="${recommendation.payoff}"${worthiness ? ` data-worthiness="${worthiness}"` : ""}>
|
|
4204
|
+
return `<article class="prb-kiss-rec" data-finding-card="${escapeHtml$1(recommendation.id)}" data-kiss-rec="${escapeHtml$1(recommendation.id)}" data-kiss-run-id="${escapeHtml$1(context.runId)}" data-audience="${context.audience}" data-rank="${context.rank}" data-decision="${context.decision}" data-payoff="${recommendation.payoff}"${worthiness ? ` data-worthiness="${worthiness}"` : ""}>
|
|
3984
4205
|
<header class="prb-kiss-rec-head">
|
|
3985
|
-
<
|
|
4206
|
+
<div class="prb-kiss-rec-title-row">
|
|
4207
|
+
<h4 class="prb-kiss-rec-title" tabindex="-1">${escapeHtml$1(recommendation.title)}</h4>
|
|
4208
|
+
${renderFindingCopySource(recommendationMarkdown(handoffSource(recommendation)), "recommendation")}
|
|
4209
|
+
${context.actions ? renderAskBuddyMenu("recommendation") : ""}
|
|
4210
|
+
</div>
|
|
3986
4211
|
<div class="prb-kiss-rec-pills">
|
|
4212
|
+
<span class="pr-helper-finding-status prb-kiss-decision-status" data-kiss-status="${context.decision}">${escapeHtml$1(decisionStatusLabel(context.audience, context.decision))}</span>
|
|
4213
|
+
<span class="prb-kiss-stale" data-kiss-stale${context.stale ? "" : " hidden"}>Stale</span>
|
|
3987
4214
|
<span class="prb-kiss-payoff" data-payoff="${recommendation.payoff}">${escapeHtml$1(PAYOFF[recommendation.payoff])}</span>
|
|
3988
4215
|
${worthiness ? `<span class="prb-kiss-worthiness" data-worthiness="${worthiness}">${escapeHtml$1(WORTHINESS[worthiness])}</span>` : ""}
|
|
3989
4216
|
</div>
|
|
@@ -4000,11 +4227,33 @@ function renderRecommendation(recommendation) {
|
|
|
4000
4227
|
${recommendation.draftComment ? `<details class="prb-kiss-draft">
|
|
4001
4228
|
<summary class="prb-kiss-draft-title">Draft comment</summary>
|
|
4002
4229
|
<blockquote class="pr-helper-markdown">${renderMarkdown(recommendation.draftComment)}</blockquote>
|
|
4003
|
-
${renderMessageCopySource(recommendation.draftComment)}
|
|
4004
4230
|
</details>` : ""}
|
|
4231
|
+
${renderCardActions(recommendation, context, Boolean(start))}
|
|
4005
4232
|
</article>`;
|
|
4006
4233
|
}
|
|
4007
4234
|
/**
|
|
4235
|
+
* The card's actions: open the file, copy the draft, and the decision.
|
|
4236
|
+
*
|
|
4237
|
+
* The decision is two toggles rather than a menu, because both answers are equally ordinary and
|
|
4238
|
+
* the reader should see both before choosing. Pressing the pressed one again takes the decision
|
|
4239
|
+
* back, which is how a decision is reversed; nothing is deleted either way.
|
|
4240
|
+
*
|
|
4241
|
+
* Open file presses the card's own Start here row, so there is exactly one way a KISS location
|
|
4242
|
+
* opens. The draft comment copies the Markdown the model wrote, which is what a forge needs.
|
|
4243
|
+
*/
|
|
4244
|
+
function renderCardActions(recommendation, context, hasStart) {
|
|
4245
|
+
const labels = DECISION_LABELS[context.audience];
|
|
4246
|
+
const toggle = (decision) => `<button type="button" class="prb-kiss-decide" data-kiss-decide="${decision}" aria-pressed="${context.decision === decision}">${escapeHtml$1(labels[decision])}</button>`;
|
|
4247
|
+
const openFile = hasStart ? "<button type=\"button\" class=\"prb-kiss-open-file\" data-kiss-open-primary>Open file</button>" : "";
|
|
4248
|
+
const draft = recommendation.draftComment ? renderLabelledCopySource(recommendation.draftComment, "Copy draft comment") : "";
|
|
4249
|
+
const decide = context.actions ? `<div class="prb-kiss-decide-group" role="group" aria-label="${escapeHtml$1(`Decision on ${recommendation.title}`)}">${toggle("accepted")}${toggle("declined")}</div>` : "";
|
|
4250
|
+
if (!openFile && !draft && !decide) return "";
|
|
4251
|
+
return `<div class="prb-kiss-rec-actions" data-kiss-actions>
|
|
4252
|
+
<div class="prb-kiss-rec-actions-aside">${openFile}${draft}</div>
|
|
4253
|
+
${decide}
|
|
4254
|
+
</div>`;
|
|
4255
|
+
}
|
|
4256
|
+
/**
|
|
4008
4257
|
* What acting on it gains, on one line: the estimate first, then the gains.
|
|
4009
4258
|
*
|
|
4010
4259
|
* The estimate says it is one in words beside it, so a phrase like "~35–50 lines" cannot be taken
|
|
@@ -4927,7 +5176,8 @@ function kissViewOf(view) {
|
|
|
4927
5176
|
job: view.kissJob ?? null,
|
|
4928
5177
|
freshness: view.kissFreshness ?? null,
|
|
4929
5178
|
dismissedFreshnessSha: view.dismissedKissFreshnessSha,
|
|
4930
|
-
previous: view.previousKissRun ?? null
|
|
5179
|
+
previous: view.previousKissRun ?? null,
|
|
5180
|
+
decisions: view.kissDecisions ?? []
|
|
4931
5181
|
};
|
|
4932
5182
|
}
|
|
4933
5183
|
function renderKissViews(view) {
|
|
@@ -5434,16 +5684,16 @@ function renderRunningRail(view) {
|
|
|
5434
5684
|
* item that opens a page saying "nothing has run yet" is a promise the rail cannot keep.
|
|
5435
5685
|
*/
|
|
5436
5686
|
function renderKissRail(view) {
|
|
5437
|
-
const
|
|
5687
|
+
const counts = kissDecisionCounts(kissViewOf(view));
|
|
5438
5688
|
return railGroup({
|
|
5439
5689
|
id: "prb-rail-kiss",
|
|
5440
5690
|
title: "KISS",
|
|
5441
5691
|
icon: "kiss",
|
|
5442
5692
|
view: "kiss",
|
|
5443
5693
|
current: view.session.result === null && !showsProgress(view),
|
|
5444
|
-
badge:
|
|
5694
|
+
badge: counts ? recommendationsBadge(counts) : null,
|
|
5445
5695
|
collapses: "the KISS review",
|
|
5446
|
-
body:
|
|
5696
|
+
body: counts ? railLink("kissrecs", "recommendations", "Recommendations", recommendationsBadge(counts)) : ""
|
|
5447
5697
|
});
|
|
5448
5698
|
}
|
|
5449
5699
|
/** The one chevron both twisties draw. Rotated by CSS off `aria-expanded`, never by markup. */
|
|
@@ -5473,17 +5723,20 @@ function railGroup(group) {
|
|
|
5473
5723
|
</div>`;
|
|
5474
5724
|
}
|
|
5475
5725
|
/**
|
|
5476
|
-
* How many recommendations
|
|
5726
|
+
* How many recommendations are still waiting on a decision, in KISS's own noun.
|
|
5477
5727
|
*
|
|
5478
|
-
*
|
|
5479
|
-
*
|
|
5480
|
-
*
|
|
5728
|
+
* The same shape as the Issues and Questions badges: the open count, a tick once every one has been
|
|
5729
|
+
* decided, and the history in the label. "Decided" rather than "resolved", because accepting or
|
|
5730
|
+
* declining a suggestion fixes nothing in the code. A zero total keeps its `0`: a run that
|
|
5731
|
+
* recommended nothing has had nothing decided.
|
|
5481
5732
|
*/
|
|
5482
|
-
function recommendationsBadge(
|
|
5483
|
-
return {
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5733
|
+
function recommendationsBadge(counts) {
|
|
5734
|
+
return remainingBadge({
|
|
5735
|
+
total: counts.total,
|
|
5736
|
+
open: counts.open,
|
|
5737
|
+
done: counts.total - counts.open,
|
|
5738
|
+
reviewerClosed: counts.total - counts.open
|
|
5739
|
+
}, "recommendation", "decided");
|
|
5487
5740
|
}
|
|
5488
5741
|
/** Drawings are facts too, and the label is a sentence, so it has to be a sentence about one. */
|
|
5489
5742
|
/** The map and the implementation diagrams together: everything behind the one rail item. */
|
|
@@ -10434,7 +10687,8 @@ async function sendWorkspacePage(res, workspace) {
|
|
|
10434
10687
|
kissJob: kissRun ? loadJob(kissRun.jobId) : void 0,
|
|
10435
10688
|
kissFreshness,
|
|
10436
10689
|
dismissedKissFreshnessSha: workspace.dismissedKissFreshnessSha,
|
|
10437
|
-
previousKissRun: previousKissResult(workspace)
|
|
10690
|
+
previousKissRun: previousKissResult(workspace),
|
|
10691
|
+
kissDecisions: workspace.kissDecisions ?? []
|
|
10438
10692
|
});
|
|
10439
10693
|
recordWorkspaceOpened(workspace);
|
|
10440
10694
|
if (freshness) record({
|
|
@@ -10578,7 +10832,7 @@ async function handleFreshness(res, workspace, quiet) {
|
|
|
10578
10832
|
* an answer would be wrong.
|
|
10579
10833
|
*/
|
|
10580
10834
|
async function kissFreshnessOrNull(workspace) {
|
|
10581
|
-
const run =
|
|
10835
|
+
const run = presentedKissRunOf(workspace);
|
|
10582
10836
|
if (!run) return null;
|
|
10583
10837
|
try {
|
|
10584
10838
|
return await checkCodeFreshness(workspace, run.reviewedSha);
|
|
@@ -11019,6 +11273,7 @@ async function handlePrefs(req, res) {
|
|
|
11019
11273
|
}
|
|
11020
11274
|
async function handleState(req, res, workspace) {
|
|
11021
11275
|
const body = await readJson(req);
|
|
11276
|
+
if (body !== null && "kissDecision" in body) return handleKissDecision(res, workspace, body.kissDecision);
|
|
11022
11277
|
const saved = STATE_FIELDS.some((field) => body !== null && field in body) ? mutateWorkspace(workspace.id, (current) => applyState(current, body)) : workspace;
|
|
11023
11278
|
if (!saved) return send(res, 404, { error: "No such review" });
|
|
11024
11279
|
send(res, 200, {
|
|
@@ -11028,6 +11283,51 @@ async function handleState(req, res, workspace) {
|
|
|
11028
11283
|
queued: readQueue().includes(saved.id)
|
|
11029
11284
|
});
|
|
11030
11285
|
}
|
|
11286
|
+
var KISS_DECISION_VALUES = [
|
|
11287
|
+
"accepted",
|
|
11288
|
+
"declined",
|
|
11289
|
+
"undecided"
|
|
11290
|
+
];
|
|
11291
|
+
function kissDecisionInput(value) {
|
|
11292
|
+
const input = value;
|
|
11293
|
+
if (typeof input?.runId !== "string" || input.runId === "") return null;
|
|
11294
|
+
if (typeof input.recommendationId !== "string" || input.recommendationId === "") return null;
|
|
11295
|
+
if (!KISS_DECISION_VALUES.includes(input.decision)) return null;
|
|
11296
|
+
return {
|
|
11297
|
+
runId: input.runId,
|
|
11298
|
+
recommendationId: input.recommendationId,
|
|
11299
|
+
decision: input.decision
|
|
11300
|
+
};
|
|
11301
|
+
}
|
|
11302
|
+
var KISS_DECISION_REFUSED = {
|
|
11303
|
+
"unknown-run": "This review has no completed KISS run with that id.",
|
|
11304
|
+
"unknown-recommendation": "That KISS run has no recommendation with that id."
|
|
11305
|
+
};
|
|
11306
|
+
/**
|
|
11307
|
+
* One decision, written onto the review as it is now and answered with what was stored.
|
|
11308
|
+
*
|
|
11309
|
+
* Checked against a fresh read before writing, so a refused decision leaves the file alone, and
|
|
11310
|
+
* checked again inside the write, where the answer is final. The reply carries the stored decision
|
|
11311
|
+
* so the page confirms from the server rather than from what it asked for.
|
|
11312
|
+
*/
|
|
11313
|
+
function handleKissDecision(res, workspace, value) {
|
|
11314
|
+
const input = kissDecisionInput(value);
|
|
11315
|
+
if (!input) return send(res, 400, { error: "A KISS decision names a run, a recommendation, and accepted, declined or undecided." });
|
|
11316
|
+
const before = loadWorkspace(workspace.id);
|
|
11317
|
+
if (!before) return send(res, 404, { error: "No such review" });
|
|
11318
|
+
const early = kissDecisionRefusal(before, input);
|
|
11319
|
+
if (early) return send(res, 400, { error: KISS_DECISION_REFUSED[early] });
|
|
11320
|
+
let refused = null;
|
|
11321
|
+
const saved = mutateWorkspace(workspace.id, (current) => {
|
|
11322
|
+
refused = decideKissRecommendation(current, input);
|
|
11323
|
+
});
|
|
11324
|
+
if (!saved) return send(res, 404, { error: "No such review" });
|
|
11325
|
+
if (refused) return send(res, 400, { error: KISS_DECISION_REFUSED[refused] });
|
|
11326
|
+
send(res, 200, {
|
|
11327
|
+
ok: true,
|
|
11328
|
+
decision: kissDecisionsOf(saved, input.runId).find((entry) => entry.recommendationId === input.recommendationId)?.decision ?? "undecided"
|
|
11329
|
+
});
|
|
11330
|
+
}
|
|
11031
11331
|
/** What `/state` can change. A body naming none of these writes nothing. */
|
|
11032
11332
|
var STATE_FIELDS = [
|
|
11033
11333
|
"answeredQuestionIds",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.1",
|
|
4
4
|
"description": "Review a branch or pull request in an isolated checkout, from your terminal, with the coding agent you already have installed.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://prreviewbuddy.com",
|