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/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as createPrState, An as AGENT_IDS, At as loadJob, B as toneOf, Bt as followedRefName, C as readEvents, Ct as checkReview, D as announceJob, Dn as writeAgentPreference, Dt as endedAt, E as telemetryUploadConsent, En as readAgentPreference, Et as allJobs, F as markAllRead, Fn as AgentCancelledError, Ft as progressSteps, G as MANAGED_ROOT, Gt as lineagePosition, H as withUsageRecorded, Ht as indexReviewGroups, I as markRead, In as AgentUnavailableError, It as purposeOf, Jt as mutateWorkspace, K as readMarker, Kt as loadWorkspace, L as pruneNotifications, M as eventName, N as headline, Nn as agentFor, Nt as isLiveJob, O as branchOf, Ot as fail, P as listNotifications, Pt as phasesFor, Q as clearServerRecord, Qt as reviewedCommit, R as readNotification, Sn as git, St as checkCodeFreshness, T as recordTelemetryUploadConsent, Tn as clearAgentPreference, Tt as reviewedShaOf, U as EXPLAIN_SIMPLY_PROMPT, Ut as latestKissRun, V as unreadCount$1, W as ReviewBeingDeletedError, X as readIndexToken, Xt as previousKissResult, Y as relativeTime$1, Zt as recentWorkspaces, _ as agentEnvOf, _n as processDiscussion, _t as claimRequestUrl, a as wasBlocked, an as queueAdd, bt as remoteHost, c as RefreshUnavailableError, cn as queueStamp, d as refreshPrContext, dn as doneVerb, dt as MAX_WORKSPACE_PORT, et as readServerRecord, f as isUnchanged, fn as isIssueOutstanding, ft as MIN_WORKSPACE_PORT, gn as reviewerDispositions, gt as writePortPreference, h as liveUpdateFor, hn as questionsOutstanding, ht as statedWorkspacePort, i as liveJobsFor, in as dequeueOnComplete, j as createPrUpdate, jt as saveJob, k as subjectOf, kn as STORE_ROOT, kt as isTerminal, l as mergeUpdateInto, ln as readQueue, lt as feedbackUrl, m as UpdateAlreadyRunningError, mn as issuesOutstanding, mt as resolveWorkspacePort, n as deleteReview, nn as workspaceRevision, nt as serverEntryExists, o as discardJob, on as queueAddMany, ot as writeServerRecord, p as askCheckout, pn as isQuestionOutstanding, pt as parseWorkspacePort, q as removeWorktree, r as lineageIds, rn as workspaceStamps, rt as spawnServer, s as FETCH_TIMEOUT_MS, sn as queueRemove, st as BUILD_VERSION, t as deleteLineage, tn as touchWorkspace, u as updateReview, un as writeQueue, ut as DEFAULT_WORKSPACE_PORT, vn as describeAuthorship, w as record, wn as resolveModel, wt as followedBranch, x as runJob, xn as isCommitReview, xt as fillFileUrlTemplate, y as startKissJob, yt as forgeFor, z as removeNotification, zt as isClaimed } from "./delete_review-DPRL5IPt.js";
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
- * No action-row wrapper, because the card already has one to sit in. It shows a word at rest where
751
- * a chat reply shows only the icon: under a reply the icon is the whole affordance and the text is
752
- * the subject, but a card's row is a line of worded buttons, and an icon alone among them reads as
753
- * decoration. Both resting words are carried on the button so the reset after "Copied" returns to
754
- * them rather than to the chat reply's.
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: "Copy",
759
- aria: `Copy ${kind}`
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 control = `<button type="button" class="${MESSAGE_COPY_CLASS}" ${handle}${idle ? ` ${IDLE_LABEL_ATTR}="${escapeHtml$1(label)}" ${IDLE_ARIA_ATTR}="${escapeHtml$1(aria)}"` : ""} 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>`;
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, Copy) is a bare label, and an emoji here read as decoration on a control
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
- <p class="pr-helper-issue-title">${renderInlineText(issue.title)}</p>
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
- <p class="pr-helper-issue-title">${renderInlineText(q.question)}</p>
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 ? ` &middot; ~${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
- <h3 class="prb-doc-title">Files</h3>
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
- <h3 class="prb-doc-title">Issues</h3>
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
- <h3 class="prb-doc-title">Questions</h3>
4243
- ${renderQuestionsTab(session, null, { suppressFileJump: true })}
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
- <span>Your prompts</span>
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">${escapeHtml$1(review.summary)}</p>` : ""}
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(readFileSync(PROMPTS_PATH, "utf8")));
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.43.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",