prreviewbuddy 0.43.0 → 0.45.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 +70 -0
- package/dist/{delete_review-DPRL5IPt.js → delete_review-BWMmO81Q.js} +257 -145
- package/dist/main.js +95 -14
- package/dist/server.js +221 -82
- package/package.json +1 -1
- package/static/shell.css +210 -1
- package/static/sidebar.css +58 -14
- package/static/workspace.js +35 -35
package/dist/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as createPrState, An as
|
|
1
|
+
import { $t as recentWorkspaces, A as createPrState, An as STORE_ROOT, At as fail, B as toneOf, C as readEvents, Cn as git, Ct as fillFileUrlTemplate, D as announceJob, Dn as readAgentPreference, Dt as reviewedShaOf, E as telemetryUploadConsent, En as clearAgentPreference, Et as followedBranch, F as markAllRead, Ft as isLiveJob, G as MANAGED_ROOT, Gt as latestKissRun, H as withUsageRecorded, Ht as followedRefName, I as markRead, In as AgentCancelledError, It as phasesFor, Jt as loadWorkspace, K as readMarker, L as pruneNotifications, Ln as AgentUnavailableError, Lt as progressSteps, M as eventName, Mt as loadJob, N as headline, Nt as saveJob, O as branchOf, On as writeAgentPreference, Ot as allJobs, P as listNotifications, Pn as agentFor, Q as readIndexToken, Qt as previousKissResult, R as readNotification, Rt as purposeOf, Sn as isCommitReview, St as remoteHost, T as recordTelemetryUploadConsent, Tn as resolveModel, Tt as checkReview, U as EXPLAIN_SIMPLY_PROMPT, V as unreadCount$1, Vt as isClaimed, W as ReviewBeingDeletedError, Wt as indexReviewGroups, X as compactQueue, Xt as mutateWorkspace, Y as relativeTime$1, _ as agentEnvOf, _n as reviewerDispositions, _t as statedWorkspacePort, a as wasBlocked, an as workspaceStamps, at as spawnServer, c as RefreshUnavailableError, cn as queueAddMany, ct as writeServerRecord, d as refreshPrContext, dn as readQueue, dt as feedbackUrl, en as reviewedCommit, et as clearServerRecord, f as isUnchanged, fn as doneVerb, ft as DEFAULT_WORKSPACE_PORT, gn as questionsOutstanding, gt as resolveWorkspacePort, h as liveUpdateFor, hn as issuesOutstanding, ht as parseWorkspacePort, i as liveJobsFor, in as workspaceRevision, it as serverEntryExists, j as createPrUpdate, jn as AGENT_IDS, jt as isTerminal, k as subjectOf, kt as endedAt, l as mergeUpdateInto, ln as queueRemove, lt as BUILD_VERSION, m as UpdateAlreadyRunningError, mn as isQuestionOutstanding, mt as MIN_WORKSPACE_PORT, n as deleteReview, nt as readServerRecord, o as discardJob, on as dequeueOnComplete, p as askCheckout, pn as isIssueOutstanding, pt as MAX_WORKSPACE_PORT, q as removeWorktree, qt as lineagePosition, r as lineageIds, rn as touchWorkspace, s as FETCH_TIMEOUT_MS, sn as queueAdd, t as deleteLineage, u as updateReview, un as queueStamp, vn as processDiscussion, vt as writePortPreference, w as record, wt as checkCodeFreshness, x as runJob, xt as forgeFor, y as startKissJob, yn as describeAuthorship, yt as claimRequestUrl, z as removeNotification } from "./delete_review-BWMmO81Q.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";
|
|
@@ -744,19 +744,30 @@ var STATES$1 = {
|
|
|
744
744
|
function renderMessageCopySource(source) {
|
|
745
745
|
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`);
|
|
746
746
|
}
|
|
747
|
+
/** The class that places a finding's copy control in its card's heading, and gives it its tooltip. */
|
|
748
|
+
var FINDING_COPY_CLASS = "pr-helper-finding-copy";
|
|
747
749
|
/**
|
|
748
750
|
* The same control on an issue or question card, carrying that finding as Markdown.
|
|
749
751
|
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
752
|
+
* It sits in the card's heading, not its footer. It copies the whole finding, so the heading is
|
|
753
|
+
* the thing it acts on. And it is a different kind of act from Investigate, Dismiss and Mark
|
|
754
|
+
* answered, which change the review: those keep the footer to themselves, so the footer holds
|
|
755
|
+
* only actions that change something.
|
|
756
|
+
*
|
|
757
|
+
* Icon only at rest, like a chat reply's. It used to show a word because it sat in a row of worded
|
|
758
|
+
* buttons, where an icon alone read as decoration. In the heading it is on its own, and a word
|
|
759
|
+
* would give a utility the weight of the title beside it. What it does is said by a hover
|
|
760
|
+
* tooltip (`data-tip`, the same CSS tooltip the pills use, not a native `title`) and by its
|
|
761
|
+
* accessible name. "Copied" and "Copy failed" still show as words, because a picture cannot say
|
|
762
|
+
* that the click did something. The resting name is kept on the button so the reset returns to
|
|
763
|
+
* "Copy finding" rather than to a chat reply's "Copy message".
|
|
755
764
|
*/
|
|
756
765
|
function renderFindingCopySource(source, kind) {
|
|
766
|
+
const name = kind === "issue" ? "Copy finding" : "Copy question";
|
|
757
767
|
return button(`${MESSAGE_SOURCE_ATTR}="${escapeHtml$1(source)}"`, {
|
|
758
|
-
label: "
|
|
759
|
-
aria:
|
|
768
|
+
label: "",
|
|
769
|
+
aria: name,
|
|
770
|
+
tip: name
|
|
760
771
|
});
|
|
761
772
|
}
|
|
762
773
|
/** Where a finding's button keeps its resting word and name. Absent on a chat reply's. */
|
|
@@ -766,13 +777,42 @@ function button(handle, idle) {
|
|
|
766
777
|
const { icon } = STATES$1.idle;
|
|
767
778
|
const label = idle?.label ?? STATES$1.idle.label;
|
|
768
779
|
const aria = idle?.aria ?? STATES$1.idle.aria;
|
|
769
|
-
const
|
|
780
|
+
const remembered = idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}"` : "";
|
|
781
|
+
const control = `<button type="button" class="${idle ? `${MESSAGE_COPY_CLASS} ${FINDING_COPY_CLASS}` : MESSAGE_COPY_CLASS}" ${handle}${remembered}${idle ? ` data-tip="${escapeHtml$1(idle.tip)}"` : ""} aria-label="${escapeHtml$1(aria)}"><span class="${MESSAGE_COPY_CLASS}-icon" aria-hidden="true">${svg(icon)}</span><span class="${MESSAGE_COPY_CLASS}-label"${label ? "" : " hidden"}>${escapeHtml$1(label)}</span></button>`;
|
|
770
782
|
return idle ? control : `<div class="${MESSAGE_ACTIONS_CLASS}">${control}</div>`;
|
|
771
783
|
}
|
|
772
784
|
function svg(paths) {
|
|
773
785
|
return `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor">${paths}</svg>`;
|
|
774
786
|
}
|
|
775
787
|
//#endregion
|
|
788
|
+
//#region ../../packages/review-harness/src/domain/ui/components/ask_buddy_menu.ts
|
|
789
|
+
/**
|
|
790
|
+
* Ask Buddy on a finding card: a sparkle beside Copy that opens the assistant's actions for the
|
|
791
|
+
* whole finding.
|
|
792
|
+
*
|
|
793
|
+
* The selection menu has offered these actions for a long time, but only to someone who thought to
|
|
794
|
+
* highlight text first, and few did. This puts the same actions on the card itself. The selection
|
|
795
|
+
* menu stays the precision tool for asking about an excerpt.
|
|
796
|
+
*
|
|
797
|
+
* The markup is the page's existing `[data-menu]` menu (the header's `...` uses it), so it opens,
|
|
798
|
+
* closes and positions exactly as that one does. The panel is empty here and filled when it opens,
|
|
799
|
+
* because it lists the reviewer's saved prompts and those change without the page re-rendering.
|
|
800
|
+
*
|
|
801
|
+
* Workspace only. The extension renders these cards too, and there is no Buddy pane behind a card
|
|
802
|
+
* there for this to open, so `ReviewTabOptions.askBuddy` gates it and the extension never sets it.
|
|
803
|
+
*/
|
|
804
|
+
var ASK_BUDDY_LABEL = "Ask Buddy";
|
|
805
|
+
/** Four-point sparkle, the conventional mark for "ask the AI". currentColor, like Copy beside it. */
|
|
806
|
+
var SPARKLE = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="14" height="14" fill="currentColor" aria-hidden="true"><path d="M7.53 1.28a.5.5 0 0 1 .94 0l.9 2.6a3 3 0 0 0 1.85 1.85l2.6.9a.5.5 0 0 1 0 .94l-2.6.9a3 3 0 0 0-1.85 1.85l-.9 2.6a.5.5 0 0 1-.94 0l-.9-2.6a3 3 0 0 0-1.85-1.85l-2.6-.9a.5.5 0 0 1 0-.94l2.6-.9a3 3 0 0 0 1.85-1.85z"/><path d="M13 11.2a.3.3 0 0 1 .56 0l.3.86a1 1 0 0 0 .62.62l.86.3a.3.3 0 0 1 0 .56l-.86.3a1 1 0 0 0-.62.62l-.3.86a.3.3 0 0 1-.56 0l-.3-.86a1 1 0 0 0-.62-.62l-.86-.3a.3.3 0 0 1 0-.56l.86-.3a1 1 0 0 0 .62-.62z"/></svg>`;
|
|
807
|
+
/** `kind` goes into the accessible name, so a screen reader hears which finding it is about. */
|
|
808
|
+
function renderAskBuddyMenu(kind) {
|
|
809
|
+
const name = `${ASK_BUDDY_LABEL} about this ${kind}`;
|
|
810
|
+
return `<div class="prb-menu prb-ask-buddy" data-menu data-ask-buddy-menu>
|
|
811
|
+
<button type="button" class="prb-menu-trigger prb-ask-buddy-trigger" data-menu-trigger aria-haspopup="menu" aria-expanded="false" aria-label="${escapeHtml$1(name)}" data-tip="${escapeHtml$1(ASK_BUDDY_LABEL)}">${SPARKLE}</button>
|
|
812
|
+
<div class="prb-menu-panel prb-ask-buddy-panel" data-menu-panel role="menu" aria-label="${escapeHtml$1(name)}" hidden></div>
|
|
813
|
+
</div>`;
|
|
814
|
+
}
|
|
815
|
+
//#endregion
|
|
776
816
|
//#region ../../packages/review-harness/src/domain/ui/components/finding_copy.ts
|
|
777
817
|
/** An issue: its title in bold, the description, then the location when it has one. */
|
|
778
818
|
function issueMarkdown(issue) {
|
|
@@ -2027,7 +2067,7 @@ function renderReconciliationNote(note) {
|
|
|
2027
2067
|
* stuck. Outlined rather than filled — see sidebar.css.
|
|
2028
2068
|
*
|
|
2029
2069
|
* Text only, no leading icon. Every other action in the sidebar (Jump to file, Mark answered,
|
|
2030
|
-
* Explain, Analyse
|
|
2070
|
+
* Explain, Analyse) is a bare label, and an emoji here read as decoration on a control
|
|
2031
2071
|
* whose neighbours have none rather than as emphasis.
|
|
2032
2072
|
*/
|
|
2033
2073
|
function renderInvestigateButton(attribute, id, label) {
|
|
@@ -2127,7 +2167,7 @@ function renderClosedFindings(summary, cards, kind, nothingOpen) {
|
|
|
2127
2167
|
<div class="pr-helper-closed-findings-list">${cards}</div>
|
|
2128
2168
|
</details>`;
|
|
2129
2169
|
}
|
|
2130
|
-
function renderIssuesTab(session, investigatingId) {
|
|
2170
|
+
function renderIssuesTab(session, investigatingId, options) {
|
|
2131
2171
|
if (!session) return emptyState("Generate an analysis to see issues.");
|
|
2132
2172
|
const outcome = outcomeOf(session);
|
|
2133
2173
|
if (outcome.kind === "none") return emptyState("Generate an analysis to see issues.");
|
|
@@ -2147,7 +2187,11 @@ function renderIssuesTab(session, investigatingId) {
|
|
|
2147
2187
|
return `
|
|
2148
2188
|
<div class="pr-helper-issue-card pr-helper-severity-${issue.severity}${issue.reconciliationStatus ? ` pr-helper-issue-${issue.reconciliationStatus}` : ""}${issue.investigationImpact ? ` pr-helper-issue-investigation-${issue.investigationImpact}` : ""}${isDismissed && !stale ? " pr-helper-issue-card-dismissed" : ""}" data-finding-card="${escapeHtml$1(issue.id)}">
|
|
2149
2189
|
<div class="pr-helper-issue-header">
|
|
2150
|
-
<
|
|
2190
|
+
<div class="pr-helper-issue-title-row">
|
|
2191
|
+
<p class="pr-helper-issue-title">${renderInlineText(issue.title)}</p>
|
|
2192
|
+
${renderFindingCopySource(issueMarkdown(issue), "issue")}
|
|
2193
|
+
${options?.askBuddy ? renderAskBuddyMenu("issue") : ""}
|
|
2194
|
+
</div>
|
|
2151
2195
|
<div class="pr-helper-issue-badges">
|
|
2152
2196
|
${renderFindingStatus(issue.reconciliationStatus)}
|
|
2153
2197
|
${renderDismissedBadge(isDismissed)}
|
|
@@ -2167,7 +2211,6 @@ function renderIssuesTab(session, investigatingId) {
|
|
|
2167
2211
|
${renderRelatedFiles(issue.relatedFiles)}
|
|
2168
2212
|
${renderInvestigationProvenance("issue", issue.id, records, now)}
|
|
2169
2213
|
<div class="pr-helper-issue-actions">
|
|
2170
|
-
${renderFindingCopySource(issueMarkdown(issue), "issue")}
|
|
2171
2214
|
${renderInvestigationActions("issue", issue.id, records, investigatingId)}
|
|
2172
2215
|
${renderDismissToggle(issue, isDismissed, stale, dismissals.get(issue.id))}
|
|
2173
2216
|
</div>
|
|
@@ -2236,7 +2279,11 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
2236
2279
|
return `
|
|
2237
2280
|
<div class="pr-helper-question-card${answered ? " pr-helper-question-card-answered" : ""}${q.investigationImpact ? ` pr-helper-issue-investigation-${q.investigationImpact}` : ""}" data-finding-card="${escapeHtml$1(q.id)}"${q.sectionId ? ` data-section-id="${escapeHtml$1(q.sectionId)}"` : ""}>
|
|
2238
2281
|
<div class="pr-helper-issue-header">
|
|
2239
|
-
<
|
|
2282
|
+
<div class="pr-helper-issue-title-row">
|
|
2283
|
+
<p class="pr-helper-issue-title">${renderInlineText(q.question)}</p>
|
|
2284
|
+
${renderFindingCopySource(questionMarkdown(q), "question")}
|
|
2285
|
+
${options?.askBuddy ? renderAskBuddyMenu("question") : ""}
|
|
2286
|
+
</div>
|
|
2240
2287
|
<div class="pr-helper-issue-badges">
|
|
2241
2288
|
${renderQuestionStatus(q.reconciliationStatus)}
|
|
2242
2289
|
${renderInvestigationImpact(q.investigationImpact)}
|
|
@@ -2259,7 +2306,6 @@ function renderQuestionCard(q, sections, answered, records, now = Date.now(), in
|
|
|
2259
2306
|
<div class="pr-helper-question-actions">
|
|
2260
2307
|
${jumpLink}
|
|
2261
2308
|
<div class="pr-helper-question-actions-group">
|
|
2262
|
-
${renderFindingCopySource(questionMarkdown(q), "question")}
|
|
2263
2309
|
${renderInvestigationActions("question", q.id, records, investigatingId)}
|
|
2264
2310
|
${answerToggle}
|
|
2265
2311
|
</div>
|
|
@@ -2334,6 +2380,70 @@ function renderQuestionsTab(session, investigatingId, options) {
|
|
|
2334
2380
|
`).join("")}${ungrouped.map(card).join("")}${closed}`;
|
|
2335
2381
|
}
|
|
2336
2382
|
//#endregion
|
|
2383
|
+
//#region ../../packages/review-harness/src/workspace/doc_head.ts
|
|
2384
|
+
/**
|
|
2385
|
+
* The title row a document pane opens with, and the provenance disclosure at the right of it.
|
|
2386
|
+
*
|
|
2387
|
+
* Two things sit in this row and they are not the same kind of thing. The title names the pane;
|
|
2388
|
+
* Analysis details answers "how was this made, and does it describe the code I am looking at" --
|
|
2389
|
+
* when it completed, which commit, which audience, which model, what it spent, which rubric. That
|
|
2390
|
+
* is a question about the whole analysis, so it belongs level with the pane's name rather than
|
|
2391
|
+
* below the output: a reader who meets it after the recommendations has already decided how much
|
|
2392
|
+
* to trust them.
|
|
2393
|
+
*
|
|
2394
|
+
* A control, not a strip. Full width would give provenance the weight of content, which it is not,
|
|
2395
|
+
* and it is the reason the disclosure opens into a panel anchored under the button rather than
|
|
2396
|
+
* pushing the report down the page. Opening it must not move the thing the reader was reading.
|
|
2397
|
+
*
|
|
2398
|
+
* This row lives inside the light document pane, never in the dark page header above it. The
|
|
2399
|
+
* header names the change; these details describe one run against it, and the two would be read as
|
|
2400
|
+
* one fact if they shared a surface. The `prb-doc-` prefix is load-bearing for that: it is what
|
|
2401
|
+
* `pane_palette.test.ts` looks for when it checks that nothing in the pane borrows a shell colour.
|
|
2402
|
+
*/
|
|
2403
|
+
/**
|
|
2404
|
+
* The panel's contents are plain sentences, escaped here, because every caller is passing facts
|
|
2405
|
+
* rather than markup and one of them is a model's own words about a file it cited.
|
|
2406
|
+
*/
|
|
2407
|
+
function renderAnalysisDetails(items) {
|
|
2408
|
+
if (items.length === 0) return "";
|
|
2409
|
+
return `<details class="prb-doc-provenance" data-analysis-details>
|
|
2410
|
+
<summary class="prb-doc-provenance-summary">Analysis details</summary>
|
|
2411
|
+
<div class="prb-doc-provenance-panel">
|
|
2412
|
+
<ul>${items.map((item) => `<li>${escapeHtml$1(item)}</li>`).join("")}</ul>
|
|
2413
|
+
</div>
|
|
2414
|
+
</details>`;
|
|
2415
|
+
}
|
|
2416
|
+
/**
|
|
2417
|
+
* `aside` is markup rather than a string of text: it is a rendered control, and the only one so
|
|
2418
|
+
* far is the disclosure above. Empty when a pane has no provenance to show, which is every state
|
|
2419
|
+
* before a run has produced something, and the row then draws as the bare title it used to be.
|
|
2420
|
+
*/
|
|
2421
|
+
function renderDocHead(title, aside = "") {
|
|
2422
|
+
return `<div class="prb-doc-head">
|
|
2423
|
+
<h3 class="prb-doc-title">${escapeHtml$1(title)}</h3>
|
|
2424
|
+
${aside}
|
|
2425
|
+
</div>`;
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Expand a surface to fill the workspace, or bring it back.
|
|
2429
|
+
*
|
|
2430
|
+
* Icon-only, like Copy on a card, because it sits in a title row and a word there would compete
|
|
2431
|
+
* with the title. Both icons are in the markup and the stylesheet shows the one `aria-pressed`
|
|
2432
|
+
* calls for, so the client only has to flip the attribute (see `paintFocus` in focus_mode.ts),
|
|
2433
|
+
* which also rewrites the name to say what pressing it will do.
|
|
2434
|
+
*
|
|
2435
|
+
* `name` is the surface as a person would say it, and goes into the accessible name.
|
|
2436
|
+
*/
|
|
2437
|
+
var EXPAND_ICON = "<path d=\"M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7\"/>";
|
|
2438
|
+
var COLLAPSE_ICON = "<path d=\"M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7\"/>";
|
|
2439
|
+
function focusIcon(paths, className) {
|
|
2440
|
+
return `<svg class="${className}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
2441
|
+
}
|
|
2442
|
+
function renderFocusToggle(surface, name) {
|
|
2443
|
+
const label = `Expand ${name}`;
|
|
2444
|
+
return `<button type="button" class="prb-focus-toggle" data-focus-toggle="${surface}" data-focus-name="${escapeHtml$1(name)}" aria-pressed="false" aria-label="${escapeHtml$1(label)}" title="${escapeHtml$1(label)}">${focusIcon(EXPAND_ICON, "prb-focus-expand")}${focusIcon(COLLAPSE_ICON, "prb-focus-collapse")}</button>`;
|
|
2445
|
+
}
|
|
2446
|
+
//#endregion
|
|
2337
2447
|
//#region ../../packages/review-harness/src/workspace/guide_view.ts
|
|
2338
2448
|
/**
|
|
2339
2449
|
* The workspace's Guide: one theme at a time.
|
|
@@ -2384,7 +2494,7 @@ function renderWorkspaceGuide(session, followUpInFlight = null) {
|
|
|
2384
2494
|
return `<div class="prb-guide-detail" data-guide-detail="${escapeHtml$1(section.id)}"${hidden}>${card}</div>`;
|
|
2385
2495
|
}).join("");
|
|
2386
2496
|
return `
|
|
2387
|
-
<div class="pr-helper-guide prb-guide">
|
|
2497
|
+
<div class="pr-helper-guide prb-guide" data-guide-start="${escapeHtml$1(selected.id)}">
|
|
2388
2498
|
${renderGuideProgress(sections, completed)}
|
|
2389
2499
|
${renderThemePicker(sections, selected.id)}
|
|
2390
2500
|
<div class="prb-guide-details">${details}</div>
|
|
@@ -2404,7 +2514,7 @@ function renderGuideProgress(sections, completed) {
|
|
|
2404
2514
|
const done = sections.filter((section) => completed.has(section.id)).length;
|
|
2405
2515
|
const left = remainingMinutes(sections, completed);
|
|
2406
2516
|
const time = left > 0 ? ` · ~${left} min` : "";
|
|
2407
|
-
return `<p class="prb-guide-progress">${done > 0 ? `${done} of ${total} theme${total === 1 ? "" : "s"} completed` : `${total} theme${total === 1 ? "" : "s"}`}${time}</p>`;
|
|
2517
|
+
return `<div class="prb-guide-head"><p class="prb-guide-progress">${done > 0 ? `${done} of ${total} theme${total === 1 ? "" : "s"} completed` : `${total} theme${total === 1 ? "" : "s"}`}${time}</p>${renderFocusToggle("guide", "Review path")}</div>`;
|
|
2408
2518
|
}
|
|
2409
2519
|
/**
|
|
2410
2520
|
* The same theme list the rail draws, for the width where the rail is not drawn.
|
|
@@ -2418,51 +2528,6 @@ function renderThemePicker(sections, selectedId) {
|
|
|
2418
2528
|
return `<div class="prb-guide-picker">${sections.map((section, index) => `<button type="button" class="prb-guide-pick" data-goto-section="${escapeHtml$1(section.id)}" data-theme-index="${index}" data-recommended-start="${section.recommendedStartingPoint}" aria-current="${section.id === selectedId}"><span class="prb-guide-pick-order">${index + 1}</span>${escapeHtml$1(section.title)}</button>`).join("")}</div>`;
|
|
2419
2529
|
}
|
|
2420
2530
|
//#endregion
|
|
2421
|
-
//#region ../../packages/review-harness/src/workspace/doc_head.ts
|
|
2422
|
-
/**
|
|
2423
|
-
* The title row a document pane opens with, and the provenance disclosure at the right of it.
|
|
2424
|
-
*
|
|
2425
|
-
* Two things sit in this row and they are not the same kind of thing. The title names the pane;
|
|
2426
|
-
* Analysis details answers "how was this made, and does it describe the code I am looking at" --
|
|
2427
|
-
* when it completed, which commit, which audience, which model, what it spent, which rubric. That
|
|
2428
|
-
* is a question about the whole analysis, so it belongs level with the pane's name rather than
|
|
2429
|
-
* below the output: a reader who meets it after the recommendations has already decided how much
|
|
2430
|
-
* to trust them.
|
|
2431
|
-
*
|
|
2432
|
-
* A control, not a strip. Full width would give provenance the weight of content, which it is not,
|
|
2433
|
-
* and it is the reason the disclosure opens into a panel anchored under the button rather than
|
|
2434
|
-
* pushing the report down the page. Opening it must not move the thing the reader was reading.
|
|
2435
|
-
*
|
|
2436
|
-
* This row lives inside the light document pane, never in the dark page header above it. The
|
|
2437
|
-
* header names the change; these details describe one run against it, and the two would be read as
|
|
2438
|
-
* one fact if they shared a surface. The `prb-doc-` prefix is load-bearing for that: it is what
|
|
2439
|
-
* `pane_palette.test.ts` looks for when it checks that nothing in the pane borrows a shell colour.
|
|
2440
|
-
*/
|
|
2441
|
-
/**
|
|
2442
|
-
* The panel's contents are plain sentences, escaped here, because every caller is passing facts
|
|
2443
|
-
* rather than markup and one of them is a model's own words about a file it cited.
|
|
2444
|
-
*/
|
|
2445
|
-
function renderAnalysisDetails(items) {
|
|
2446
|
-
if (items.length === 0) return "";
|
|
2447
|
-
return `<details class="prb-doc-provenance" data-analysis-details>
|
|
2448
|
-
<summary class="prb-doc-provenance-summary">Analysis details</summary>
|
|
2449
|
-
<div class="prb-doc-provenance-panel">
|
|
2450
|
-
<ul>${items.map((item) => `<li>${escapeHtml$1(item)}</li>`).join("")}</ul>
|
|
2451
|
-
</div>
|
|
2452
|
-
</details>`;
|
|
2453
|
-
}
|
|
2454
|
-
/**
|
|
2455
|
-
* `aside` is markup rather than a string of text: it is a rendered control, and the only one so
|
|
2456
|
-
* far is the disclosure above. Empty when a pane has no provenance to show, which is every state
|
|
2457
|
-
* before a run has produced something, and the row then draws as the bare title it used to be.
|
|
2458
|
-
*/
|
|
2459
|
-
function renderDocHead(title, aside = "") {
|
|
2460
|
-
return `<div class="prb-doc-head">
|
|
2461
|
-
<h3 class="prb-doc-title">${escapeHtml$1(title)}</h3>
|
|
2462
|
-
${aside}
|
|
2463
|
-
</div>`;
|
|
2464
|
-
}
|
|
2465
|
-
//#endregion
|
|
2466
2531
|
//#region ../../packages/review-harness/src/workspace/diagram_view.ts
|
|
2467
2532
|
/**
|
|
2468
2533
|
* The diagrams a review holds, drawn as cards in one scrolling pane.
|
|
@@ -4216,7 +4281,7 @@ function renderKissViews(view) {
|
|
|
4216
4281
|
function renderFilesView(view, sections) {
|
|
4217
4282
|
if (!(view.session.result !== null || (view.job ? changeIsRead(view.job) : false))) return "";
|
|
4218
4283
|
return `<section class="prb-view" data-view="files" hidden>
|
|
4219
|
-
|
|
4284
|
+
${renderDocHead("Files", renderFocusToggle("files", "Files"))}
|
|
4220
4285
|
${renderFiles(view, sections)}
|
|
4221
4286
|
</section>`;
|
|
4222
4287
|
}
|
|
@@ -4234,13 +4299,16 @@ function renderStandardViews(view, sections) {
|
|
|
4234
4299
|
${renderDiagrams(view.diagrams ?? [], sections, view.reviewedSha ?? null)}
|
|
4235
4300
|
</section>
|
|
4236
4301
|
<section class="prb-view" data-view="issues" hidden>
|
|
4237
|
-
|
|
4302
|
+
${renderDocHead("Issues", renderFocusToggle("issues", "Issues"))}
|
|
4238
4303
|
${renderAttributionAge(view)}
|
|
4239
|
-
${renderIssuesTab(session, null)}
|
|
4304
|
+
${renderIssuesTab(session, null, { askBuddy: true })}
|
|
4240
4305
|
</section>
|
|
4241
4306
|
<section class="prb-view" data-view="questions" hidden>
|
|
4242
|
-
|
|
4243
|
-
${renderQuestionsTab(session, null, {
|
|
4307
|
+
${renderDocHead("Questions", renderFocusToggle("questions", "Questions"))}
|
|
4308
|
+
${renderQuestionsTab(session, null, {
|
|
4309
|
+
suppressFileJump: true,
|
|
4310
|
+
askBuddy: true
|
|
4311
|
+
})}
|
|
4244
4312
|
</section>
|
|
4245
4313
|
<section class="prb-view" data-view="prcontext" hidden>
|
|
4246
4314
|
${renderPrContextView(view.prContext ?? null, {
|
|
@@ -4975,8 +5043,9 @@ function renderAsk(view, sections, paused = false) {
|
|
|
4975
5043
|
<div class="prb-ask-header">
|
|
4976
5044
|
<h2>Ask Buddy AI</h2>
|
|
4977
5045
|
<span class="prb-ask-scope" data-ask-scope>whole change</span>
|
|
5046
|
+
${renderFocusToggle("buddy", "Buddy AI")}
|
|
4978
5047
|
</div>
|
|
4979
|
-
${paused ? "" : renderPromptBank(view.prompts ?? [])}
|
|
5048
|
+
${paused ? "" : renderPromptBank(view.prompts ?? [], view.promptsCollapsed === true)}
|
|
4980
5049
|
<div class="prb-ask-log" data-ask-log>
|
|
4981
5050
|
${transcript.length > 0 || pending ? transcript.map((exchange) => `<div class="prb-msg prb-msg-user">${escapeHtml$1(exchange.question)}</div>
|
|
4982
5051
|
<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" : ""}>
|
|
@@ -5001,13 +5070,16 @@ function renderAsk(view, sections, paused = false) {
|
|
|
5001
5070
|
* The list is always present, even when empty: an empty bank with an Add button explains what
|
|
5002
5071
|
* the feature is, where a hidden one leaves it undiscoverable until someone reads a changelog.
|
|
5003
5072
|
*/
|
|
5004
|
-
function renderPromptBank(prompts) {
|
|
5005
|
-
return `<div class="prb-bank" data-prompt-bank data-prompts="${escapeHtml$1(JSON.stringify(prompts))}">
|
|
5073
|
+
function renderPromptBank(prompts, collapsed = false) {
|
|
5074
|
+
return `<div class="prb-bank" data-prompt-bank data-prompts="${escapeHtml$1(JSON.stringify(prompts))}"${collapsed ? " data-collapsed" : ""}>
|
|
5006
5075
|
<div class="prb-bank-head">
|
|
5007
|
-
<
|
|
5076
|
+
<button type="button" class="prb-bank-toggle" data-prompt-toggle aria-expanded="${!collapsed}" aria-controls="prb-bank-list">
|
|
5077
|
+
<svg class="prb-bank-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="10" height="10" fill="currentColor" aria-hidden="true"><path d="M6 4l4 4-4 4z"/></svg>
|
|
5078
|
+
<span>Your prompts</span><span class="prb-bank-count" data-prompt-count>${prompts.length}</span>
|
|
5079
|
+
</button>
|
|
5008
5080
|
<button type="button" class="prb-bank-add" data-prompt-add>Add</button>
|
|
5009
5081
|
</div>
|
|
5010
|
-
<div class="prb-bank-list" data-prompt-list>${prompts.map(renderPromptChip).join("")}</div>
|
|
5082
|
+
<div class="prb-bank-list" id="prb-bank-list" data-prompt-list${collapsed ? " hidden" : ""}>${prompts.map(renderPromptChip).join("")}</div>
|
|
5011
5083
|
</div>`;
|
|
5012
5084
|
}
|
|
5013
5085
|
function renderPromptChip(prompt) {
|
|
@@ -6239,7 +6311,7 @@ function renderCard(group, now, analysis, queued, moved, prState) {
|
|
|
6239
6311
|
${renderStatus(status)}
|
|
6240
6312
|
${renderPrState(prState)}
|
|
6241
6313
|
</div>
|
|
6242
|
-
${review.summary ? `<p class="prb-card-summary">${
|
|
6314
|
+
${review.summary ? `<p class="prb-card-summary">${renderInlineText(review.summary)}</p>` : ""}
|
|
6243
6315
|
<p class="prb-card-where">
|
|
6244
6316
|
<span class="prb-card-repo">${escapeHtml$1(lastSegment(review.repoPath))}</span>
|
|
6245
6317
|
${review.prNumber === void 0 ? "" : `<span class="prb-card-pr">#${review.prNumber}</span>`}
|
|
@@ -7918,10 +7990,11 @@ function readPreferences() {
|
|
|
7918
7990
|
try {
|
|
7919
7991
|
const parsed = JSON.parse(readFileSync(PREFERENCES_PATH, "utf8"));
|
|
7920
7992
|
if (!parsed || typeof parsed !== "object") return {};
|
|
7921
|
-
const { askWidth, dismissedNotices } = parsed;
|
|
7993
|
+
const { askWidth, dismissedNotices, promptsCollapsed } = parsed;
|
|
7922
7994
|
return {
|
|
7923
7995
|
...typeof askWidth === "number" && Number.isFinite(askWidth) ? { askWidth: clampAskWidth(askWidth) } : {},
|
|
7924
|
-
...Array.isArray(dismissedNotices) ? { dismissedNotices: dismissedNotices.filter((key) => typeof key === "string" && NOTICE_KEY.test(key)) } : {}
|
|
7996
|
+
...Array.isArray(dismissedNotices) ? { dismissedNotices: dismissedNotices.filter((key) => typeof key === "string" && NOTICE_KEY.test(key)) } : {},
|
|
7997
|
+
...typeof promptsCollapsed === "boolean" ? { promptsCollapsed } : {}
|
|
7925
7998
|
};
|
|
7926
7999
|
} catch {
|
|
7927
8000
|
return {};
|
|
@@ -7938,6 +8011,16 @@ function writeAskWidth(width) {
|
|
|
7938
8011
|
writeFileSync(PREFERENCES_PATH, `${JSON.stringify(next, null, 2)}\n`);
|
|
7939
8012
|
} catch {}
|
|
7940
8013
|
}
|
|
8014
|
+
/** Remember whether "Your prompts" is folded. Read-modify-write, like every other field here. */
|
|
8015
|
+
function writePromptsCollapsed(collapsed) {
|
|
8016
|
+
try {
|
|
8017
|
+
mkdirSync(STORE_ROOT, { recursive: true });
|
|
8018
|
+
writeFileSync(PREFERENCES_PATH, `${JSON.stringify({
|
|
8019
|
+
...readPreferences(),
|
|
8020
|
+
promptsCollapsed: collapsed
|
|
8021
|
+
}, null, 2)}\n`);
|
|
8022
|
+
} catch {}
|
|
8023
|
+
}
|
|
7941
8024
|
/** Remember that the reviewer has read a notice. Refuses anything not shaped like a key. */
|
|
7942
8025
|
function dismissNotice(key) {
|
|
7943
8026
|
if (!NOTICE_KEY.test(key)) return false;
|
|
@@ -7973,6 +8056,44 @@ function clampAskWidth(width) {
|
|
|
7973
8056
|
/** Long enough for a paragraph of standing instructions, short enough not to crowd the question. */
|
|
7974
8057
|
var MAX_PROMPT_CHARS = 2e3;
|
|
7975
8058
|
/**
|
|
8059
|
+
* What a reviewer starts with, before they have saved anything.
|
|
8060
|
+
*
|
|
8061
|
+
* Drawn from what reviewers were typing by hand most often. Ordinary saved prompts in every
|
|
8062
|
+
* respect: no flag marks them as defaults, so they edit, delete and reorder exactly as a prompt the
|
|
8063
|
+
* reviewer wrote does. Whoever reads a bank decides when these apply. In the workspace that is
|
|
8064
|
+
* only when no bank has ever been written, so a deleted default stays deleted.
|
|
8065
|
+
*
|
|
8066
|
+
* The ids are fixed rather than generated, so reading an unwritten bank twice gives the same bank.
|
|
8067
|
+
* Nothing depends on their spelling beyond that.
|
|
8068
|
+
*/
|
|
8069
|
+
var DEFAULT_PROMPTS = Object.freeze([
|
|
8070
|
+
{
|
|
8071
|
+
id: "default-assumptions",
|
|
8072
|
+
name: "What does it assume?",
|
|
8073
|
+
prompt: "What assumptions does this change make? Say which are stated, which are implied, and what happens if one turns out to be wrong."
|
|
8074
|
+
},
|
|
8075
|
+
{
|
|
8076
|
+
id: "default-explain-educational",
|
|
8077
|
+
name: "Teach me this",
|
|
8078
|
+
prompt: "Explain this in an educational way: the concepts it relies on, why it is built this way, and what I should take away from it."
|
|
8079
|
+
},
|
|
8080
|
+
{
|
|
8081
|
+
id: "default-what-could-go-wrong",
|
|
8082
|
+
name: "What could go wrong?",
|
|
8083
|
+
prompt: "What could go wrong here? Think about edge cases, failure modes and the existing callers this could break."
|
|
8084
|
+
},
|
|
8085
|
+
{
|
|
8086
|
+
id: "default-walkthrough",
|
|
8087
|
+
name: "Walk me through it",
|
|
8088
|
+
prompt: "Walk me through the implementation step by step, in the order the code runs."
|
|
8089
|
+
},
|
|
8090
|
+
{
|
|
8091
|
+
id: "default-verify-manually",
|
|
8092
|
+
name: "What should I verify?",
|
|
8093
|
+
prompt: "What should I verify manually before approving this, and how would I check each one?"
|
|
8094
|
+
}
|
|
8095
|
+
].map((prompt) => Object.freeze(prompt)));
|
|
8096
|
+
/**
|
|
7976
8097
|
* Everything reaching this function came from a client, so nothing here is trusted.
|
|
7977
8098
|
*
|
|
7978
8099
|
* Entries are repaired where the repair is unambiguous — trimming, truncating — and dropped where
|
|
@@ -8014,9 +8135,29 @@ function sanitisePromptBank(value) {
|
|
|
8014
8135
|
* a workspace that will not open.
|
|
8015
8136
|
*/
|
|
8016
8137
|
var PROMPTS_PATH = join(STORE_ROOT, "prompts.json");
|
|
8138
|
+
/**
|
|
8139
|
+
* The bank as stored or, when nothing has ever been stored, the defaults.
|
|
8140
|
+
*
|
|
8141
|
+
* Only a file that does not exist counts as a new bank. Once the file exists it is the reviewer's,
|
|
8142
|
+
* even when it is empty: an empty list is a reviewer who deleted every prompt, and handing the
|
|
8143
|
+
* defaults back to them would be undoing what they did. A corrupt file is not "new" either. It
|
|
8144
|
+
* reads as empty, as it always has, rather than being covered over with prompts the reviewer
|
|
8145
|
+
* never chose.
|
|
8146
|
+
*
|
|
8147
|
+
* The defaults are returned, not written. The first add, edit or delete posts the whole bank,
|
|
8148
|
+
* defaults included, and that write is what turns them into the reviewer's own. A reader that
|
|
8149
|
+
* wrote here would create the file on a page load, which the offline preview must not do.
|
|
8150
|
+
*/
|
|
8017
8151
|
function readPromptBank() {
|
|
8152
|
+
let raw;
|
|
8153
|
+
try {
|
|
8154
|
+
raw = readFileSync(PROMPTS_PATH, "utf8");
|
|
8155
|
+
} catch (error) {
|
|
8156
|
+
if (error.code === "ENOENT") return DEFAULT_PROMPTS.map((prompt) => ({ ...prompt }));
|
|
8157
|
+
return [];
|
|
8158
|
+
}
|
|
8018
8159
|
try {
|
|
8019
|
-
return sanitisePromptBank(JSON.parse(
|
|
8160
|
+
return sanitisePromptBank(JSON.parse(raw));
|
|
8020
8161
|
} catch {
|
|
8021
8162
|
return [];
|
|
8022
8163
|
}
|
|
@@ -8656,11 +8797,6 @@ function addMany(res, ids) {
|
|
|
8656
8797
|
refused
|
|
8657
8798
|
});
|
|
8658
8799
|
}
|
|
8659
|
-
/** Compacted here rather than in `queue.ts`, which must not learn to read the review store. */
|
|
8660
|
-
function compactQueue(stored) {
|
|
8661
|
-
const live = stored.filter((queued) => loadWorkspace(queued) !== null);
|
|
8662
|
-
return live.length === stored.length ? stored : writeQueue(live) ?? stored;
|
|
8663
|
-
}
|
|
8664
8800
|
/**
|
|
8665
8801
|
* Delete one review, or everything on one card.
|
|
8666
8802
|
*
|
|
@@ -8886,6 +9022,7 @@ async function sendWorkspacePage(res, workspace) {
|
|
|
8886
9022
|
repoPath: workspace.repoPath,
|
|
8887
9023
|
askWidth: readPreferences().askWidth,
|
|
8888
9024
|
dismissedNotices: readPreferences().dismissedNotices,
|
|
9025
|
+
promptsCollapsed: readPreferences().promptsCollapsed === true,
|
|
8889
9026
|
transcript: workspace.transcript,
|
|
8890
9027
|
pendingAsk: reportRun(workspace.id, "ask"),
|
|
8891
9028
|
pendingDiagram: reportRun(workspace.id, "diagram"),
|
|
@@ -9470,6 +9607,7 @@ async function handlePrefs(req, res) {
|
|
|
9470
9607
|
const askWidth = body?.askWidth;
|
|
9471
9608
|
if (typeof askWidth === "number") writeAskWidth(askWidth);
|
|
9472
9609
|
else if (askWidth === null) clearAskWidth();
|
|
9610
|
+
if (typeof body?.promptsCollapsed === "boolean") writePromptsCollapsed(body.promptsCollapsed);
|
|
9473
9611
|
if (typeof body?.dismissNotice === "string" && !dismissNotice(body.dismissNotice)) return send(res, 400, { error: "Not a notice this page can dismiss." });
|
|
9474
9612
|
send(res, 200, { ok: true });
|
|
9475
9613
|
}
|
|
@@ -9531,7 +9669,8 @@ var ASK_SOURCES = [
|
|
|
9531
9669
|
"suggestion",
|
|
9532
9670
|
"free_text",
|
|
9533
9671
|
"explain_simply",
|
|
9534
|
-
"selection"
|
|
9672
|
+
"selection",
|
|
9673
|
+
"finding"
|
|
9535
9674
|
];
|
|
9536
9675
|
function askSource(value) {
|
|
9537
9676
|
return ASK_SOURCES.includes(value) ? value : "free_text";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prreviewbuddy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
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",
|