prreviewbuddy 0.86.6 → 0.87.2

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 { $ as MIN_WORKSPACE_PORT, $i as AmbiguousRefError, $n as RUN_RECORD_RETENTION_MS, $r as deleteCollection, $t as markRead, A as batchIdentity, Ai as findingAssessment, An as escapeHtml$1, At as loadArchitecture, Bi as wasReassessed, Bn as readMarker, Br as LIFECYCLES, Bt as ancestorsOf, C as RUN_KINDS, Ci as withExplanation, Cn as UNESTABLISHED_MOTIVATION, Cr as kissDecisionsOf, Ct as mapRuntimes, D as reportRun, Di as assessmentLookup, Dn as renderMarkdown, Dr as loadWorkspace, Dt as formatDuration, E as endRun, Ei as analysisRefOf, En as GITHUB_MARKDOWN, Er as lineagePosition, Et as describeRuntimes, F as activateQueued, Fi as isOffered, Fn as withReviewLock, Ft as storedScopes, G as serverEntryExists, Gi as describeAuthorship, Gn as findExistingReview, Gr as lifecycleItemLabel, Gt as subjectOf, Hi as asString, Hn as requestJobStop, Hr as activityLabel, Ht as writeJsonAtomically, I as compactQueue, Ii as promptGrade, In as MANAGED_ROOT, Ir as summarise, It as trackedFiles, Ji as resolveTarget, Jn as followedBranch, Jr as parseLifecycle, Jt as createPrState, K as spawnServer, Ki as WITHHELD_REASON, Kn as checkCodeFreshness, Kr as lifecyclePill, Kt as categoryOf, Li as reassessIn, Ln as createWorktree, Lr as touchWorkspace, Lt as ownersOf, M as parsePullRequestLink, Mi as findingStatus, Mr as previousKissResult, Mt as readArchitecture, N as unsupportedReason, Ni as gradeScale, Nn as ReviewBeingDeletedError, Nr as recentWorkspaces, Nt as repositoryKey, O as stopRun, Oi as closesFinding, On as CODE_BLOCK_CLASS, Ot as ARCHITECTURE_DIR, P as runBatch, Pi as isGradeFor, Pn as isBusy, Pr as reviewedCommit, Pt as storeArchitecture, Q as MAX_WORKSPACE_PORT, Qi as resolveBase, Qn as fillFileUrlTemplate, Qr as createWithMembers, Qt as markAllRead, R as settleQueue, Ri as sourcePhrase, Rn as holdWorktree, Rr as workspaceRevision, Rt as QUOTE_LIMITS, S as track, Si as isExplanationAction, Sn as chatConversationSection, Sr as kissDecisionRefusal, St as waitingHistory, T as completeRun, Ti as NO_STATUS, Tn as hasAuthorContent, Tr as lineageKey, Tt as RUNTIME_WORDS, U as readServerRecord, Ui as extractJsonText, Ur as isLifecycle, Ut as announceJob, V as clearServerRecordOwnedBy, Vi as followUpFindingId, Vn as removeWorktree, Vr as activityAttributes, Vt as isArchitectureId, Wi as objectWithAnyOf, Wr as lifecycleChip, Wt as branchOf, X as writeServerRecord, Xi as DetachedHeadError, Xn as fetchHead, Xr as collectionsStamp, Xt as eventName, Yi as originRemote, Yn as reviewedShaOf, Yr as sectionOf, Yt as createPrUpdate, Z as DEFAULT_WORKSPACE_PORT, Zi as isCommitReview, Zn as forgeResolver, Zr as createCollection, Zt as listNotifications, _ as runningJobs, _a as isAgentId, _i as verdictClosure, _n as unforeseenFailure, _r as decideKissRecommendation, _t as telemetryUploadConsent, a as wasBlocked, aa as resolveModel, ai as queueAdd, an as removeNotification, ar as loadJob, at as observeReview, b as startKissJob, ba as AgentCancelledError, bi as explanationActionOf, bn as agentEnvOf, br as indexKeptIds, c as RefreshUnavailableError, ca as readAgentPreference, ci as queueStamp, cn as unreadAround, ct as reanalyseReview, d as mergeUpdateInto, da as STORE_ROOT, di as isIssueOutstanding, dn as existingPaths, dr as phasesFor, ea as shortNameOf, ei as nameProblemText, en as markReadForReview, er as allJobs, et as parseWorkspacePort, fa as AGENT_IDS, fi as isQuestionOutstanding, fn as ASK_REREAD_CEILING_MS, fr as progressSteps, ft as readEvents, g as liveUpdateFor, ga as detectAgents, gi as reviewerDispositions, gn as storedConversation, gt as recordTelemetryUploadConsent, h as UpdateAlreadyRunningError, ha as agentIdentityOf, hi as resolvedByConversation, hn as saveRefresh, hr as isClaimed, i as liveJobsFor, ia as resolveAgent, ii as updateCollections, in as readStateOf, ir as isTerminal, it as failureOf$1, j as extractPullRequestLinks, ji as findingIn, jn as renderInlineText, jr as presentedKissRunOf, jt as partFiles, k as relativeTime$1, ki as currentGradeIn, kn as CODE_COPY_CLASS, kr as mutateWorkspace, kt as checkArchitecture, l as cancelUpdate, la as writeAgentPreference, li as readQueue, ln as unreadCount$1, lr as isLiveJob, lt as runJob, m as askCheckout, ma as agentFor$1, mi as questionsOutstanding, mn as prepareConversationWithin, n as deleteReview, na as git, ni as renameCollection, nn as pruneNotifications, nr as endedAt, nt as statedWorkspacePort, o as discardJob, oa as agentIdOf, oi as queueAddMany, on as retireNotification, or as pruneJobResidue, ot as prepareReview, p as updateReview, pa as agentById, pi as issuesOutstanding, pn as adoptLegacyBaseline, pr as purposeOf, pt as record, q as stopChild, qi as kissOutcome, qn as checkReview, qr as offersLifecycle, qt as createArchitectureNotification, r as lineageIds, ra as refExists, ri as setMembership, rn as readNotification, rr as fail$1, rt as writePortPreference, s as FETCH_TIMEOUT_MS, sa as clearAgentPreference, si as queueRemove, sn as toneOf, sr as saveJob, st as settleReview, t as deleteLineage, ta as shorthandOf, ti as readCollections, tn as moveIds, tr as attemptIdOf, tt as resolveWorkspacePort, u as cancellableHere, ui as doneVerb, un as withUsageRecorded, ur as isTerminalJobPhase, v as kissAgentId, va as parkedReason, vi as processDiscussion, vn as refreshPrContext, vr as followedRefName, w as beginRun, wi as ANALYSIS_ONLY, wn as authorPreview, wr as latestKissRun, wt as recordRuntimeBestEffort, x as isDriving, xa as AgentUnavailableError, xi as explanationOf, xn as EXPLAIN_SIMPLY_PROMPT, xr as indexReviewGroups, xt as feedbackUrl, ya as runnableAgents, yi as recommendationsOf, yn as isUnchanged, yr as groupByLineage, yt as BUILD_VERSION, z as readIndexToken, zi as statusLookup, zn as isWorktreeHeld, zr as workspaceStamps, zt as REPOSITORY_SCOPE } from "./delete_review-CwKZQlMg.js";
1
+ import { $ as MAX_WORKSPACE_PORT, $i as resolveBase, $n as fillFileUrlTemplate, $r as createWithMembers, $t as markAllRead, A as relativeTime$1, Ai as currentGradeIn, An as CODE_COPY_CLASS, Ar as mutateWorkspace, At as checkArchitecture, B as readIndexToken, Bi as statusLookup, Bn as isWorktreeHeld, Br as workspaceStamps, Bt as REPOSITORY_SCOPE, C as RUN_KINDS, Ci as isExplanationAction, Cn as chatConversationSection, Cr as kissDecisionRefusal, Ct as waitingHistory, D as heldRuns, Di as analysisRefOf, Dn as GITHUB_MARKDOWN, Dr as lineagePosition, Dt as describeRuntimes, E as endRun, Ei as NO_STATUS, En as hasAuthorContent, Er as lineageKey, Et as RUNTIME_WORDS, F as runBatch, Fi as isGradeFor, Fn as isBusy, Fr as reviewedCommit, Ft as storeArchitecture, Gi as objectWithAnyOf, Gr as lifecycleChip, Gt as branchOf, H as clearServerRecordOwnedBy, Hi as followUpFindingId, Hn as removeWorktree, Hr as activityAttributes, Ht as isArchitectureId, I as activateQueued, Ii as isOffered, In as withReviewLock, It as storedScopes, J as stopChild, Ji as kissOutcome, Jn as checkReview, Jr as offersLifecycle, Jt as createArchitectureNotification, K as serverEntryExists, Ki as describeAuthorship, Kn as findExistingReview, Kr as lifecycleItemLabel, Kt as subjectOf, L as compactQueue, Li as promptGrade, Ln as MANAGED_ROOT, Lr as summarise, Lt as trackedFiles, M as extractPullRequestLinks, Mi as findingIn, Mn as renderInlineText, Mr as presentedKissRunOf, Mt as partFiles, N as parsePullRequestLink, Ni as findingStatus, Nr as previousKissResult, Nt as readArchitecture, O as reportRun, Oi as assessmentLookup, On as renderMarkdown, Or as loadWorkspace, Ot as formatDuration, P as unsupportedReason, Pi as gradeScale, Pn as ReviewBeingDeletedError, Pr as recentWorkspaces, Pt as repositoryKey, Q as DEFAULT_WORKSPACE_PORT, Qi as isCommitReview, Qn as forgeResolver, Qr as createCollection, Qt as listNotifications, Ri as reassessIn, Rn as createWorktree, Rr as touchWorkspace, Rt as ownersOf, S as track, Sa as AgentUnavailableError, Si as explanationOf, Sn as EXPLAIN_SIMPLY_PROMPT, Sr as indexReviewGroups, St as feedbackUrl, T as completeRun, Ti as ANALYSIS_ONLY, Tn as authorPreview, Tr as latestKissRun, Tt as recordRuntimeBestEffort, Ui as asString, Un as requestJobStop, Ur as activityLabel, Ut as writeJsonAtomically, Vi as wasReassessed, Vn as readMarker, Vr as LIFECYCLES, Vt as ancestorsOf, W as readServerRecord, Wi as extractJsonText, Wr as isLifecycle, Wt as announceJob, Xi as originRemote, Xn as reviewedShaOf, Xr as sectionOf, Xt as createPrUpdate, Yi as resolveTarget, Yn as followedBranch, Yr as parseLifecycle, Yt as createPrState, Z as writeServerRecord, Zi as DetachedHeadError, Zn as fetchHead, Zr as collectionsStamp, Zt as eventName, _ as runningJobs, _a as detectAgents, _i as reviewerDispositions, _n as storedConversation, _t as recordTelemetryUploadConsent, a as wasBlocked, aa as resolveAgent, ai as updateCollections, an as readStateOf, ar as isTerminal, at as failureOf$1, b as startKissJob, ba as runnableAgents, bi as recommendationsOf, bn as isUnchanged, br as groupByLineage, bt as BUILD_VERSION, c as RefreshUnavailableError, ca as clearAgentPreference, ci as queueRemove, cn as toneOf, cr as saveJob, ct as settleReview, d as mergeUpdateInto, di as doneVerb, dn as withUsageRecorded, dr as isTerminalJobPhase, ea as AmbiguousRefError, ei as deleteCollection, en as markRead, er as RUN_RECORD_RETENTION_MS, et as MIN_WORKSPACE_PORT, fa as STORE_ROOT, fi as isIssueOutstanding, fn as existingPaths, fr as phasesFor, g as liveUpdateFor, ga as agentIdentityOf, gi as resolvedByConversation, gn as saveRefresh, gr as isClaimed, h as UpdateAlreadyRunningError, ha as agentFor$1, hi as questionsOutstanding, hn as prepareConversationWithin, i as liveJobsFor, ia as refExists, ii as setMembership, in as readNotification, ir as fail$1, it as writePortPreference, j as batchIdentity, ji as findingAssessment, jn as escapeHtml$1, jt as loadArchitecture, k as stopRun, ki as closesFinding, kn as CODE_BLOCK_CLASS, kt as ARCHITECTURE_DIR, l as cancelUpdate, la as readAgentPreference, li as queueStamp, ln as unreadAround, lt as reanalyseReview, m as askCheckout, ma as agentById, mi as issuesOutstanding, mn as adoptLegacyBaseline, mr as purposeOf, mt as record, n as deleteReview, na as shorthandOf, ni as readCollections, nn as moveIds, nr as attemptIdOf, nt as resolveWorkspacePort, o as discardJob, oa as resolveModel, oi as queueAdd, on as removeNotification, or as loadJob, ot as observeReview, p as updateReview, pa as AGENT_IDS, pi as isQuestionOutstanding, pn as ASK_REREAD_CEILING_MS, pr as progressSteps, pt as readEvents, q as spawnServer, qi as WITHHELD_REASON, qn as checkCodeFreshness, qr as lifecyclePill, qt as categoryOf, r as lineageIds, ra as git, ri as renameCollection, rn as pruneNotifications, rr as endedAt, rt as statedWorkspacePort, s as FETCH_TIMEOUT_MS, sa as agentIdOf, si as queueAddMany, sn as retireNotification, sr as pruneJobResidue, st as prepareReview, t as deleteLineage, ta as shortNameOf, ti as nameProblemText, tn as markReadForReview, tr as allJobs, tt as parseWorkspacePort, u as cancellableHere, ua as writeAgentPreference, ui as readQueue, un as unreadCount$1, ur as isLiveJob, ut as runJob, v as kissAgentId, va as isAgentId, vi as verdictClosure, vn as unforeseenFailure, vr as decideKissRecommendation, vt as telemetryUploadConsent, w as beginRun, wi as withExplanation, wn as UNESTABLISHED_MOTIVATION, wr as kissDecisionsOf, wt as mapRuntimes, x as isDriving, xa as AgentCancelledError, xi as explanationActionOf, xn as agentEnvOf, xr as indexKeptIds, ya as parkedReason, yi as processDiscussion, yn as refreshPrContext, yr as followedRefName, z as settleQueue, zi as sourcePhrase, zn as holdWorktree, zr as workspaceRevision, zt as QUOTE_LIMITS } from "./delete_review-D7TXn5ck.js";
2
2
  import { basename, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -867,6 +867,20 @@ function supersededLabel(because) {
867
867
  return because === "moved-again" ? "replaced by a later move" : "replaced when the review caught up";
868
868
  }
869
869
  //#endregion
870
+ //#region ../../packages/review-harness/src/workspace/running_label.ts
871
+ /**
872
+ * The words and the digit for the count beside Runs, which the server renders and the browser
873
+ * repaints. Pure, because the client bundle reads it.
874
+ */
875
+ /** In words, for a screen reader: "1 run in progress", "3 runs in progress". */
876
+ function runningLabel(running) {
877
+ return running === 0 ? "Nothing running" : `${running} ${running === 1 ? "run" : "runs"} in progress`;
878
+ }
879
+ /** The digit an icon has room for. Past nine it says there are many rather than how many. */
880
+ function runningDigit(running) {
881
+ return running > 9 ? "9+" : String(running);
882
+ }
883
+ //#endregion
870
884
  //#region ../../packages/review-harness/src/workspace/server_status.ts
871
885
  /** The dot's accessible name, which is the only form of it a screen reader gets. */
872
886
  function serverStatusLabel(status) {
@@ -891,8 +905,8 @@ var SHOW_SIDEBAR = "Show sidebar";
891
905
  /**
892
906
  * The workspace's own places, and the one model every page draws them from.
893
907
  *
894
- * A review page adds its review-level groups above these; every other workspace page draws these
895
- * alone. There is no second list: the rail, the narrow header and the review page's rail foot all
908
+ * Drawn in the same place on every page, directly under New review, so they never move: a review
909
+ * page adds its own section below them, and every other workspace page draws these alone. There is no second list: the rail, the narrow header and the review page's rail foot all
896
910
  * read `WORKSPACE_PLACES`, so a place added here appears everywhere and a place renamed here is
897
911
  * renamed everywhere.
898
912
  */
@@ -936,6 +950,16 @@ function sidebarAttributes(sidebar) {
936
950
  return `${sidebar?.compact ? " data-sidebar-compact" : ""}${sidebar?.hidden ? " data-sidebar-hidden" : ""}`;
937
951
  }
938
952
  /**
953
+ * The count beside Runs. Always rendered, hidden at zero, so a poll that reports a new number can
954
+ * reveal it without the rail being rebuilt. See `client/running_counts.ts`, its one painter.
955
+ *
956
+ * In the Activity accent, as the unread count beside Activity is: both say something is happening
957
+ * that the reader did not just do. The words are its label, because a bare digit names nothing.
958
+ */
959
+ function runningCount$1(running) {
960
+ return `<span class="prb-rail-count prb-rail-running" data-running-count data-running-style="rail" aria-label="${escapeHtml$1(runningLabel(running))}"${running === 0 ? " hidden" : ""}>${running === 0 ? "" : running}</span>`;
961
+ }
962
+ /**
939
963
  * The count beside Notifications. Always rendered, hidden at zero, so the page can reveal it when
940
964
  * `/revision` or Mark all read reports a new number. See `client/unread_counts.ts`.
941
965
  */
@@ -978,19 +1002,25 @@ function renderGlobalBar(options) {
978
1002
  </header>`;
979
1003
  }
980
1004
  /**
981
- * The bar below the strip: what page this is, and the controls that belong to it.
1005
+ * The bar at the top of the content column: what page this is, and the controls that belong to it.
1006
+ * Beside the rail rather than above it, so the rail starts under the strip on every page.
982
1007
  *
983
1008
  * The dominant bar of the composition on every page, as the review's own bar is on a review page.
984
1009
  * That is the point of the split: the strip is utility chrome and this says what you are looking at.
985
1010
  *
986
- * `controls` is the row that page used to draw at the top of its own column — the reviews filters,
987
- * the Mark all read line — moved up rather than added, so the strip above is the only band the
988
- * redesign actually costs.
1011
+ * Two regions. The title row is the same on every page, the same height as a review's bar, with
1012
+ * page-level actions (Mark all read) at its right, so the title never moves between pages. The
1013
+ * controls row (search and filters) is drawn only by a page that has controls, which is the one
1014
+ * way the bar grows: a page with nothing to put there gets no empty band to match the others.
989
1015
  */
990
- function renderContextBar(title, controls = "") {
1016
+ function renderContextBar(title, parts = {}) {
1017
+ const controls = parts.controls ? `
1018
+ <div class="prb-context-secondary">${parts.controls}</div>` : "";
991
1019
  return `<div class="prb-context">
992
- <h1 class="prb-page-title">${escapeHtml$1(title)}</h1>
993
- ${controls}
1020
+ <div class="prb-context-primary">
1021
+ <h1 class="prb-page-title">${escapeHtml$1(title)}</h1>
1022
+ ${parts.actions ?? ""}
1023
+ </div>${controls}
994
1024
  </div>`;
995
1025
  }
996
1026
  /**
@@ -1069,9 +1099,21 @@ function renderHistoryControls() {
1069
1099
  function renderStripReviews(options) {
1070
1100
  return `<a class="prb-strip-action" href="/"${options.current === "reviews" ? " aria-current=\"true\"" : ""} aria-label="Reviews" data-strip-reviews>${iconSvg("reviews")}</a>`;
1071
1101
  }
1072
- /** Runs, beside Reviews, in the rail's order. Its own `aria-label`, for the reason Settings has one. */
1102
+ /**
1103
+ * Runs, beside Reviews, in the rail's order. Its own `aria-label`, for the reason Settings has one.
1104
+ *
1105
+ * It carries the count beside Runs as well, as the bell carries the unread one, so the work going
1106
+ * on is visible wherever the rail is not: Compact, Hidden, a narrow window. The digit is for the
1107
+ * eye; the words, its description, are what a screen reader hears after the name.
1108
+ */
1073
1109
  function renderStripRuns(options) {
1074
- return `<a class="prb-strip-action" href="/runs"${options.current === "runs" ? " aria-current=\"true\"" : ""} aria-label="Runs" data-strip-runs>${iconSvg("runs")}</a>`;
1110
+ const current = options.current === "runs" ? " aria-current=\"true\"" : "";
1111
+ const running = options.running ?? 0;
1112
+ const hidden = running === 0 ? " hidden" : "";
1113
+ return `<a class="prb-strip-action prb-strip-runs" href="/runs"${current} aria-label="Runs" aria-describedby="prb-strip-running" data-strip-runs>${iconSvg("runs")}
1114
+ <span class="prb-strip-count" data-running-count data-running-style="digit" aria-hidden="true"${hidden}>${running === 0 ? "" : runningDigit(running)}</span>
1115
+ <span class="prb-visually-hidden" id="prb-strip-running" data-running-count data-running-style="words">${running === 0 ? "" : escapeHtml$1(runningLabel(running))}</span>
1116
+ </a>`;
1075
1117
  }
1076
1118
  /**
1077
1119
  * Settings, as an icon, beside the bell.
@@ -1132,12 +1174,13 @@ function renderServerStatus() {
1132
1174
  function renderNewReviewButton() {
1133
1175
  return `<div class="prb-rail-new"><button type="button" class="prb-new-review" data-new-review="rail">${iconSvg("plus")}<span class="prb-new-review-label">New review</span><kbd class="prb-new-review-key" data-new-review-key aria-hidden="true"></kbd></button></div>`;
1134
1176
  }
1135
- /** New review, then the workspace rows, for a rail group. */
1177
+ /** New review, then the workspace rows: the first group of every rail, review pages' included. */
1136
1178
  function renderWorkspaceNav(options) {
1137
1179
  return renderNewReviewButton() + WORKSPACE_PLACES.map((item) => `<a class="prb-rail-item prb-rail-link" href="${item.href}"${item.place === options.current ? " aria-current=\"true\"" : ""}>
1138
1180
  ${railIcon(item.icon)}
1139
1181
  <span class="prb-rail-label"><span class="prb-rail-title">${escapeHtml$1(item.label)}</span></span>
1140
1182
  ${item.place === "notifications" ? unreadCount(options.unread, "rail") : ""}
1183
+ ${item.place === "runs" ? runningCount$1(options.running ?? 0) : ""}
1141
1184
  </a>`).join("");
1142
1185
  }
1143
1186
  function renderShutdownDialog() {
@@ -1170,7 +1213,7 @@ function renderSendFeedback() {
1170
1213
  */
1171
1214
  function renderWorkspaceRail(options) {
1172
1215
  return `<nav class="prb-rail" aria-label="Workspace">
1173
- <div class="prb-rail-group">${renderWorkspaceNav(options)}</div>
1216
+ <div class="prb-rail-group prb-rail-places">${renderWorkspaceNav(options)}</div>
1174
1217
  <div class="prb-rail-group prb-rail-foot">${renderSendFeedback()}${renderSidebarToggle(options.sidebar)}</div>
1175
1218
  </nav>`;
1176
1219
  }
@@ -6396,9 +6439,28 @@ function renderWorkspace(view) {
6396
6439
  }) ? " data-guided-tour=\"offer\"" : ""}${view.tourBusy ? " data-tour-busy" : ""}>
6397
6440
  ${renderGlobalBar({
6398
6441
  unread: view.unreadNotifications ?? 0,
6442
+ running: view.runningRuns ?? 0,
6399
6443
  live: Boolean(view.token),
6400
6444
  driving: view.driving
6401
6445
  })}
6446
+ ${renderReviewBody(view, sections, renderReviewHead(view, freshness))}
6447
+ ${view.token ? renderPromptDialog() : ""}
6448
+ ${offersFeedback(view) ? renderFeedbackDialog() : ""}
6449
+ <script type="module" src="/assets/workspace.js"><\/script>
6450
+ </body>
6451
+ </html>`;
6452
+ }
6453
+ /**
6454
+ * Everything about this review that sits above its panes: the review bar, then whatever banners the
6455
+ * review's state calls for (job status, freshness, the last update's outcome, the lineage notice).
6456
+ *
6457
+ * In the content column, to the right of the rail, rather than across the window. The application
6458
+ * shell is the strip and the rail, and it is the same on every page; this is the review's own
6459
+ * header, and however many banners it holds it pushes down only the review, never the rail. The
6460
+ * workspace pages put their context bar in the same column for the same reason.
6461
+ */
6462
+ function renderReviewHead(view, freshness) {
6463
+ return `<div class="prb-review-head">
6402
6464
  ${renderReviewBar(view, freshness)}
6403
6465
  ${showsProgress(view) || updating(view) ? "" : renderJobState(view.job ?? null, {
6404
6466
  dismissed: view.dismissedNotices,
@@ -6412,12 +6474,7 @@ ${showsProgress(view) || updating(view) ? "" : renderJobState(view.job ?? null,
6412
6474
  })}</div>
6413
6475
  ${updating(view) ? "" : renderUpdateOutcome(view.lastUpdate)}
6414
6476
  ${updating(view) ? "" : renderLineageNotice(view)}
6415
- ${renderReviewBody(view, sections)}
6416
- ${view.token ? renderPromptDialog() : ""}
6417
- ${offersFeedback(view) ? renderFeedbackDialog() : ""}
6418
- <script type="module" src="/assets/workspace.js"><\/script>
6419
- </body>
6420
- </html>`;
6477
+ </div>`;
6421
6478
  }
6422
6479
  /**
6423
6480
  * Whether this page's subject is a review that is still being made, or one that stopped.
@@ -6466,9 +6523,10 @@ function showsProgress(view) {
6466
6523
  * question box offering to discuss findings nobody has produced is a worse thing to hand somebody
6467
6524
  * than no box at all.
6468
6525
  */
6469
- function renderReviewBody(view, sections) {
6526
+ function renderReviewBody(view, sections, head = "") {
6470
6527
  const result = view.session.result;
6471
6528
  return `<div class="prb-body"${result ? view.reviewLayout?.ask ? ` data-ask="${view.reviewLayout.ask}"` : "" : ` data-ask="closed"`} data-review-layout="${escapeHtml$1(JSON.stringify(view.reviewLayout ?? {}))}"${view.askWidth ? ` style="--ask-width: ${Math.round(view.askWidth)}px"` : ""}>
6529
+ ${head}
6472
6530
  ${renderRail(view, sections, updating(view))}
6473
6531
  <main class="prb-doc">
6474
6532
  ${updating(view) ? renderUpdatingDoc(view) : renderReviewDoc(view, sections)}
@@ -6961,46 +7019,33 @@ function renderIdMenu(view, freshness) {
6961
7019
  </div>`;
6962
7020
  }
6963
7021
  /**
6964
- * Everything below the review: where else to go, and what to do to the server.
6965
- *
6966
- * These are in the rail rather than the header because they are not about this review. The header
6967
- * answers "what am I looking at" — branch, size, where the review stands — and
6968
- * anything global sitting up there competed with that. Leaving the review is navigation, and it
6969
- * belongs with the other navigation.
6970
- *
6971
- * Stopping the server is separated from the links because it is operational and it ends every open
6972
- * review, not just this one. The build sits beside it: it is provenance, wanted when something is
6973
- * wrong, and that is the same corner as the feedback link that carries it.
7022
+ * The foot of the rail: Send feedback and the sidebar's own toggle, anchored at the bottom whichever
7023
+ * review is open and however long its section is.
6974
7024
  *
6975
7025
  * The same feedback form the extension uses, so replies land in one place rather than two. What
6976
- * it carries is decided in `version.ts` — version, client, OS — and deliberately not the
7026
+ * it carries is decided in `version.ts` (version, client, OS), and deliberately not the
6977
7027
  * repository or its path.
6978
7028
  */
6979
7029
  function renderRailFoot(view) {
6980
7030
  return `<div class="prb-rail-group prb-rail-foot">
6981
- ${view.token ? renderWorkspaceNav({ unread: view.unreadNotifications ?? 0 }) : ""}
6982
7031
  ${renderSendFeedback()}
6983
7032
  ${view.token ? renderSidebarToggle(view.sidebar) : ""}
6984
7033
  </div>`;
6985
7034
  }
6986
7035
  /**
6987
- * The rail: one group per review type, each opening its own summary and folding away.
7036
+ * The rail of a review page: the workspace's places, then a section for this review, then the foot.
6988
7037
  *
6989
- * `Review` and `KISS` are peers, and the rail is the only place that can say so. Before this the
6990
- * standard review had no container at all — `Overview` was a page label doing duty as one — so a
6991
- * `KISS` entry beside it would have read as a sixth page of the standard review rather than as the
6992
- * other question being asked of the same change.
7038
+ * The places come first and are the very rows every other page draws (`workspace_nav.ts`), so they
7039
+ * hold one position across the whole workspace. Everything about this review is contextual and sits
7040
+ * below them, under a heading rather than a row of its own: the reader is already on the review, so
7041
+ * a "Review" entry would be a door to the room they are standing in. Its first row is the review's
7042
+ * own page (`renderReviewHome`).
6993
7043
  *
6994
- * The group's own row navigates as well as containing. Each review type has exactly one summary
6995
- * page and some subordinate pages, so the container and the summary are the same thing, and
6996
- * splitting them would have put a second row under `Review` labelled `Overview` and a second under
6997
- * `KISS` labelled something invented. Collapsing is a plain button beside the row rather than a
6998
- * <details>: the row has to stay a link, and a control inside a <summary> is toggled by the
6999
- * browser as well as by us in some engines.
7044
+ * KISS is in the same section, as a group of its own with its pages under it: it is a second
7045
+ * question asked of this change, and it belongs to the review the way Issues and Files do.
7000
7046
  *
7001
- * The review path keeps a twisty of its own, one level in. It is the long part of the group and
7002
- * the short links under it are the ones a reviewer reaches for repeatedly, so the two have to fold
7003
- * separately or folding the path would mean folding away Issues and Questions with it.
7047
+ * The review path keeps a twisty of its own. It is the long part of the section and the short links
7048
+ * under it are the ones a reviewer reaches for repeatedly, so it folds on its own.
7004
7049
  *
7005
7050
  * Themes are numbered because the guide genuinely is an ordered walkthrough — `order` is the
7006
7051
  * model's answer to "what sequence should a reviewer take through this change" — so the number
@@ -7025,16 +7070,7 @@ function renderRail(view, sections, stale = false) {
7025
7070
  <span class="prb-attention" data-attention="${escapeHtml$1(section.reviewAttention)}" title="Needs ${escapeHtml$1(section.reviewAttention)} attention"></span>
7026
7071
  </a>`;
7027
7072
  }).join("");
7028
- const standard = view.session.result ? railGroup({
7029
- id: "prb-rail-review",
7030
- title: "Review",
7031
- icon: "review",
7032
- view: "overview",
7033
- current: true,
7034
- badge: null,
7035
- collapses: "the review",
7036
- open: railFoldOpen(view, "prb-rail-review"),
7037
- body: `<button type="button" class="prb-rail-subhead" data-rail-toggle="the review path" ${foldAttributes(railFoldOpen(view, "prb-rail-path"), "the review path")} aria-controls="prb-rail-path">
7073
+ const standard = view.session.result ? `<button type="button" class="prb-rail-subhead" data-rail-toggle="the review path" ${foldAttributes(railFoldOpen(view, "prb-rail-path"), "the review path")} aria-controls="prb-rail-path">
7038
7074
  <span class="prb-rail-heading">Review path</span>
7039
7075
  <span class="prb-rail-twisty" aria-hidden="true">${TWISTY}</span>
7040
7076
  </button>
@@ -7045,49 +7081,66 @@ function renderRail(view, sections, stale = false) {
7045
7081
  ${railLink("issues", "issues", "Issues", findingsBadge(issuesOutstanding(issues, dispositions)), void 0, void 0, true)}
7046
7082
  ${railLink("questions", "questions", "Questions", remainingBadge(questionsOutstanding(questions, answered, verdictClosure(view.session).question), "question", "answered"), void 0, void 0, true)}
7047
7083
  ${railLink("files", "files", "Files", totalBadge(view.changeSet.data.files.length, "files"))}
7048
- ${view.prContext ? railLink("prcontext", "prcontext", "PR context", totalBadge(view.prContext.processed.stats.totalThreads, "PR comment threads")) : railLink("prcontext", "prcontext", "PR context", null, "No pull request was connected to this review")}`
7049
- }) : "";
7050
- return `<nav class="prb-rail" aria-label="Review path"${stale ? " inert data-stale" : ""}>
7051
- ${standard || renderRunningRail(view)}
7052
- ${renderKissRail(view)}
7084
+ ${view.prContext ? railLink("prcontext", "prcontext", "PR context", totalBadge(view.prContext.processed.stats.totalThreads, "PR comment threads")) : railLink("prcontext", "prcontext", "PR context", null, "No pull request was connected to this review")}` : renderRunningRail(view);
7085
+ return `<nav class="prb-rail" aria-label="Workspace">
7086
+ ${view.token ? `<div class="prb-rail-group prb-rail-places">${renderWorkspaceNav({
7087
+ unread: view.unreadNotifications ?? 0,
7088
+ running: view.runningRuns ?? 0
7089
+ })}</div>` : ""}
7090
+ <section class="prb-rail-context" id="prb-rail-review" aria-labelledby="prb-rail-context-title"${stale ? " inert data-stale" : ""}>
7091
+ <h2 class="prb-rail-heading prb-rail-context-title" id="prb-rail-context-title">Current review</h2>
7092
+ ${renderReviewHome(view)}
7093
+ ${standard}
7094
+ ${renderKissRail(view)}
7095
+ </section>
7053
7096
  ${renderRailFoot(view)}
7054
7097
  </nav>`;
7055
7098
  }
7056
7099
  /**
7057
- * The Review group while the review is still being made.
7100
+ * The review's own first page: Overview once there is a result to present, Progress until then.
7058
7101
  *
7059
- * Without this the rail on a running review was a single purple `KISS` row, which says the change
7060
- * has one kind of review available to it and that KISS is where the reader is meant to be. Both
7061
- * are wrong, and the second is why the page looked broken rather than busy: the one thing on the
7062
- * rail was the one thing that had nothing to do with what the page was waiting for.
7102
+ * Decided by the result, not by whether anything is running. An update keeps the result it is
7103
+ * replacing, so the row stays Overview through it; a first analysis, or a re-run, which clears the
7104
+ * result before it starts, has nothing to present yet, and a failed one keeps Progress so its
7105
+ * failure and its Retry stay one press away. Both are the same row in the same slot, so the label
7106
+ * changing moves nothing. A review with neither (KISS alone) draws none: KISS is its page.
7063
7107
  *
7064
- * It carries what actually exists — the run itself, and the files once they have been read. There
7065
- * are deliberately no Issues or Questions rows waiting to be filled in. A badge is a measured
7066
- * figure, so a `0` beside Issues would report that an analysis nobody has finished found nothing,
7067
- * and a row with no badge is a door onto an empty room. The rail says what is there; the Progress
7068
- * pane is where "and these arrive later" belongs.
7108
+ * Current when the server draws it, as the default view is, so the first paint is already right;
7109
+ * the client keeps `aria-current` true from there, as it does for every `data-view-nav`.
7110
+ */
7111
+ function renderReviewHome(view) {
7112
+ const home = view.session.result ? {
7113
+ view: "overview",
7114
+ label: "Overview"
7115
+ } : showsProgress(view) ? {
7116
+ view: "progress",
7117
+ label: "Progress"
7118
+ } : null;
7119
+ if (!home) return "";
7120
+ return `<a class="prb-rail-item prb-rail-link" href="${escapeHtml$1(hashForView(home.view))}" data-view-nav="${home.view}" data-review-home aria-current="true">
7121
+ ${railIcon("review")}
7122
+ <span class="prb-rail-label"><span class="prb-rail-title">${home.label}</span></span>
7123
+ </a>`;
7124
+ }
7125
+ /**
7126
+ * What the review's section carries while the review is still being made.
7127
+ *
7128
+ * What actually exists: the files, once they have been read, under the Progress row. There are
7129
+ * deliberately no Issues or Questions rows waiting to be filled in. A badge is a measured figure,
7130
+ * so a `0` beside Issues would report that an analysis nobody has finished found nothing, and a
7131
+ * row with no badge is a door onto an empty room. The rail says what is there; the Progress pane
7132
+ * is where "and these arrive later" belongs.
7069
7133
  */
7070
7134
  function renderRunningRail(view) {
7071
7135
  if (!showsProgress(view)) return "";
7072
7136
  const files = view.changeSet.data.files.length;
7073
- return railGroup({
7074
- id: "prb-rail-review",
7075
- title: "Review",
7076
- icon: "review",
7077
- view: "progress",
7078
- current: true,
7079
- badge: null,
7080
- collapses: "the review",
7081
- open: railFoldOpen(view, "prb-rail-review"),
7082
- body: view.job && changeIsRead(view.job) ? railLink("files", "files", "Files", totalBadge(files, "files")) : ""
7083
- });
7137
+ return view.job && changeIsRead(view.job) ? railLink("files", "files", "Files", totalBadge(files, "files")) : "";
7084
7138
  }
7085
7139
  /**
7086
- * The KISS section of the rail, present before anybody has run it.
7140
+ * The KISS group of the review's section, present before anybody has run it.
7087
7141
  *
7088
- * A sibling of the standard review rather than an item inside it, because the two are peers: one
7089
- * asks whether the change is correct and safe, the other whether its complexity is justified, and
7090
- * nesting either under the other would say one is a facet of the other.
7142
+ * A group of its own rather than a row among Issues and Files, because it is a second question
7143
+ * asked of the same change: whether its complexity is justified, beside whether it is correct.
7091
7144
  *
7092
7145
  * Visible on a change nobody has run it on, which is the point. The entry is what teaches that one
7093
7146
  * change can be interrogated through more than one review type, and a reader who never sees it
@@ -8053,6 +8106,22 @@ function architectureRunsFor(repoPath) {
8053
8106
  startedAt
8054
8107
  })).sort((a, b) => Number(b.scope === "repository") - Number(a.scope === "repository") || a.startedAt - b.startedAt);
8055
8108
  }
8109
+ /**
8110
+ * Every repository being mapped now, for the Runs page. `key` is the run's own key, repository and
8111
+ * scope, so two scopes of one repository are two entries and one scope is never listed twice.
8112
+ */
8113
+ function allArchitectureRuns() {
8114
+ return [...runs.entries()].map(([key, { repoPath, scope, stage, steps, startedAt, record, partName }]) => ({
8115
+ key,
8116
+ repoPath,
8117
+ scope,
8118
+ stage,
8119
+ steps,
8120
+ startedAt,
8121
+ reviewId: record.reviewId,
8122
+ ...partName ? { partName } : {}
8123
+ }));
8124
+ }
8056
8125
  /** Whether a record that says `running` is really running, or was cut short. */
8057
8126
  function isRunLive(repoPath, scope) {
8058
8127
  return runs.has(keyOf(repoPath, scope));
@@ -8116,7 +8185,8 @@ function startArchitectureRun(workspace, scope, deps = {}) {
8116
8185
  steps: 0,
8117
8186
  startedAt,
8118
8187
  controller: new AbortController(),
8119
- record
8188
+ record,
8189
+ ...partName ? { partName } : {}
8120
8190
  };
8121
8191
  runs.set(key, held);
8122
8192
  saveRunRecord(repoPath, record);
@@ -9997,17 +10067,19 @@ function renderReviewsPage(view) {
9997
10067
  ${renderGlobalBar({
9998
10068
  current: "reviews",
9999
10069
  unread: view.unreadNotifications ?? 0,
10070
+ running: view.runningRuns ?? 0,
10000
10071
  driving: view.driving
10001
10072
  })}
10002
- ${renderContextBar("Reviews", ordered.length === 0 ? "" : renderControls$1(queue.length, authorOptions(ordered.map(({ latest }) => ({
10073
+ <div class="prb-shell">
10074
+ ${renderContextBar("Reviews", { controls: ordered.length === 0 ? void 0 : renderControls$1(queue.length, authorOptions(ordered.map(({ latest }) => ({
10003
10075
  handle: latest.authorHandle,
10004
10076
  name: latest.authorName,
10005
10077
  createdAt: latest.createdAt
10006
- }))), ordered.some((group) => group.latest.prNumber !== void 0), context.refs.length > 0))}
10007
- <div class="prb-shell">
10078
+ }))), ordered.some((group) => group.latest.prNumber !== void 0), context.refs.length > 0) })}
10008
10079
  ${renderWorkspaceRail({
10009
10080
  current: "reviews",
10010
10081
  unread: view.unreadNotifications ?? 0,
10082
+ running: view.runningRuns ?? 0,
10011
10083
  sidebar: view.sidebar
10012
10084
  })}
10013
10085
  <main class="prb-index">
@@ -11265,13 +11337,15 @@ function renderSettingsPage(view) {
11265
11337
  ${renderGlobalBar({
11266
11338
  current: "settings",
11267
11339
  unread: view.unreadNotifications ?? 0,
11340
+ running: view.runningRuns ?? 0,
11268
11341
  driving: view.driving
11269
11342
  })}
11270
- ${renderContextBar("Settings", renderBack())}
11271
11343
  <div class="prb-shell">
11344
+ ${renderContextBar("Settings")}
11272
11345
  ${renderWorkspaceRail({
11273
11346
  current: "settings",
11274
11347
  unread: view.unreadNotifications ?? 0,
11348
+ running: view.runningRuns ?? 0,
11275
11349
  sidebar: view.sidebar
11276
11350
  })}
11277
11351
  <main class="prb-index prb-settings">
@@ -11301,21 +11375,6 @@ ${renderShutdownDialog()}
11301
11375
  </html>`;
11302
11376
  }
11303
11377
  /**
11304
- * The way back out.
11305
- *
11306
- * Settings is reached from two places and only one of them is the index: the other is the rail of a
11307
- * review somebody was in the middle of reading. A link to `/` is the right answer for the first and
11308
- * a detour for the second, and the server cannot tell which happened or address the review if it
11309
- * could — a workspace URL carries that review's own token, and putting one in a link on this page
11310
- * would hand a page that is not that review a credential that opens it.
11311
- *
11312
- * So the server renders the answer that is always true and the browser improves it when it can, out
11313
- * of a referrer it already has. See `client/settings.ts`.
11314
- */
11315
- function renderBack() {
11316
- return `<a class="prb-settings-back" href="/" data-settings-back><span aria-hidden="true">&lsaquo;</span> All reviews</a>`;
11317
- }
11318
- /**
11319
11378
  * Stopping the server, on the one page where stopping it is a decision rather than a stray click.
11320
11379
  *
11321
11380
  * It used to sit at the foot of every rail, one press from ending every open review, beside the
@@ -12044,17 +12103,19 @@ function renderHelpPage(view) {
12044
12103
  <link rel="stylesheet" href="/assets/sidebar.css">
12045
12104
  <link rel="stylesheet" href="/assets/shell.css">
12046
12105
  </head>
12047
- <body class="prb-index-body" data-way-back="${escapeHtml$1(wayBackSentence())}">
12106
+ <body class="prb-index-body" data-way-back="${escapeHtml$1(wayBackSentence())}" data-notifications-since="${Date.now()}">
12048
12107
  ${renderGlobalBar({
12049
12108
  current: "help",
12050
12109
  unread: view.unreadNotifications ?? 0,
12110
+ running: view.runningRuns ?? 0,
12051
12111
  driving: view.driving
12052
12112
  })}
12053
- ${renderContextBar("Help", "")}
12054
12113
  <div class="prb-shell">
12114
+ ${renderContextBar("Help")}
12055
12115
  ${renderWorkspaceRail({
12056
12116
  current: "help",
12057
12117
  unread: view.unreadNotifications ?? 0,
12118
+ running: view.runningRuns ?? 0,
12058
12119
  sidebar: view.sidebar
12059
12120
  })}
12060
12121
  <main class="prb-index prb-help">
@@ -12093,13 +12154,15 @@ function renderNotificationsPage(view) {
12093
12154
  ${renderGlobalBar({
12094
12155
  current: "notifications",
12095
12156
  unread: view.unreadNotifications ?? 0,
12157
+ running: view.runningRuns ?? 0,
12096
12158
  driving: view.driving
12097
12159
  })}
12098
- ${renderContextBar("Activity", view.notifications.length === 0 ? "" : renderReadAll(unread))}
12099
12160
  <div class="prb-shell">
12161
+ ${renderContextBar("Activity", { actions: view.notifications.length === 0 ? void 0 : renderReadAll(unread) })}
12100
12162
  ${renderWorkspaceRail({
12101
12163
  current: "notifications",
12102
12164
  unread: view.unreadNotifications ?? 0,
12165
+ running: view.runningRuns ?? 0,
12103
12166
  sidebar: view.sidebar
12104
12167
  })}
12105
12168
  <main class="prb-index prb-notifications-page">
@@ -13150,6 +13213,37 @@ async function chooseFolder(deps) {
13150
13213
  }
13151
13214
  //#endregion
13152
13215
  //#region ../../packages/review-harness/src/workspace/runs.ts
13216
+ /**
13217
+ * A question or a drawing, while it is still running. A stopped one is over for the reader, and an
13218
+ * answered one is only being saved: neither is executing, so neither is listed or counted.
13219
+ */
13220
+ function activeAssistantRun(run) {
13221
+ if (run.state !== "running") return null;
13222
+ return {
13223
+ key: `${run.kind}:${run.workspaceId}`,
13224
+ kind: run.kind,
13225
+ ...run.kind === "diagram" ? { drawing: run.drawing ?? "map" } : {},
13226
+ reviewId: run.workspaceId,
13227
+ startedAt: run.startedAt
13228
+ };
13229
+ }
13230
+ /**
13231
+ * A repository, or one part of it, being mapped. Its registry holds it only while it runs, so every
13232
+ * one is active. It belongs to the repository, and the review it was started from is where it opens.
13233
+ */
13234
+ function activeArchitectureRun(run) {
13235
+ return {
13236
+ key: `architecture:${run.key}`,
13237
+ kind: "diagram",
13238
+ drawing: "architecture",
13239
+ reviewId: run.reviewId,
13240
+ repoPath: run.repoPath,
13241
+ scope: run.scope,
13242
+ ...run.partName ? { scopeName: run.partName } : {},
13243
+ stage: run.stage,
13244
+ startedAt: run.startedAt
13245
+ };
13246
+ }
13153
13247
  function runKindOf(job) {
13154
13248
  if (purposeOf(job) === "update") return "update";
13155
13249
  if (job.reviewType === "kiss") return "kiss";
@@ -13176,7 +13270,7 @@ function operationRunKind(operation) {
13176
13270
  * The history is ordered newest ending first, then by key, a total order, because it is hashed into
13177
13271
  * the page's revision and a tie left to the directory would reload the page for nothing.
13178
13272
  */
13179
- function deriveRuns(jobs, isLive, rows, now) {
13273
+ function deriveRuns(jobs, isLive, rows, now, active = []) {
13180
13274
  const inProgress = deriveOperations(jobs, isLive, rows);
13181
13275
  const recent = [];
13182
13276
  for (const job of jobs) {
@@ -13236,6 +13330,7 @@ function deriveRuns(jobs, isLive, rows, now) {
13236
13330
  recent.sort((a, b) => b.endedAt - a.endedAt || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
13237
13331
  return {
13238
13332
  inProgress,
13333
+ active: sortActive(active),
13239
13334
  recent
13240
13335
  };
13241
13336
  }
@@ -13254,6 +13349,20 @@ function fromAttempt(attempt) {
13254
13349
  ...attempt.agentId ? { agentId: attempt.agentId } : {}
13255
13350
  };
13256
13351
  }
13352
+ /** Newest first, ending on the key, a total order, because the list is hashed into the revision. */
13353
+ function sortActive(active) {
13354
+ return [...active].sort((a, b) => b.startedAt - a.startedAt || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
13355
+ }
13356
+ /**
13357
+ * How many executions are going on now: every operation but the queued, and every registry run.
13358
+ *
13359
+ * The one number the Runs page's Running figure and the sidebar's count beside Runs both show, so the
13360
+ * count a reader follows and the page it opens cannot disagree. Queued work is waiting, not running.
13361
+ * A row and the job it handed over to are one operation already (`deriveOperations`).
13362
+ */
13363
+ function runningCount(operations, active) {
13364
+ return operations.filter((operation) => operation.kind !== "queued").length + active.length;
13365
+ }
13257
13366
  /**
13258
13367
  * The three numbers above the lists. Failed counts every failed attempt the page can list, earlier
13259
13368
  * attempts included and removed ones not: the same population as the history, so the count and the
@@ -13261,7 +13370,7 @@ function fromAttempt(attempt) {
13261
13370
  */
13262
13371
  function runCounts(runs) {
13263
13372
  return {
13264
- running: runs.inProgress.filter((operation) => operation.kind !== "queued").length,
13373
+ running: runningCount(runs.inProgress, runs.active),
13265
13374
  queued: runs.inProgress.filter((operation) => operation.kind === "queued").length,
13266
13375
  failed: runs.recent.filter((entry) => entry.status === "failed").length
13267
13376
  };
@@ -13280,6 +13389,7 @@ function runsRevision(runs, cleanup) {
13280
13389
  if (cleanup && (cleanup.refusals.length > 0 || cleanup.error)) hash.update(`cleanup:${cleanup.at}:${cleanup.error ?? ""}:${cleanup.refusals.join("\0")}\n`);
13281
13390
  const byKey = (a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
13282
13391
  for (const operation of [...runs.inProgress].sort(byKey)) hash.update(`op:${operation.key}:${operation.kind}:${operation.phase ?? ""}:${operation.reviewId ?? ""}\n`);
13392
+ for (const run of [...runs.active].sort(byKey)) hash.update(`act:${run.key}:${run.startedAt}:${run.drawing ?? ""}:${run.stage ?? ""}:${run.reviewId ?? ""}\n`);
13283
13393
  for (const entry of [...runs.recent].sort(byKey)) hash.update(`run:${entry.key}:${entry.status}:${entry.reviewDeleted ? "deleted" : ""}\n`);
13284
13394
  return hash.digest("hex").slice(0, 16);
13285
13395
  }
@@ -13316,7 +13426,23 @@ function liveOperations(now = Date.now(), jobs = allJobs(), rows = pendingBatchR
13316
13426
  * unchanged.
13317
13427
  */
13318
13428
  function liveRuns(now = Date.now(), jobs = allJobs(), rows = pendingBatchRows()) {
13319
- return deriveRuns(jobs, (job) => isClaimed(job.id, now), rows, now);
13429
+ return deriveRuns(jobs, (job) => isClaimed(job.id, now), rows, now, liveActiveRuns());
13430
+ }
13431
+ /**
13432
+ * The work this server's in-memory registries hold now: questions, drawings and mapping runs.
13433
+ *
13434
+ * This process's only, like the queue, and for the same reason: they run as children of the
13435
+ * workspace server and nothing else can start one. Not on disk, so nothing is left once they end.
13436
+ */
13437
+ function liveActiveRuns() {
13438
+ return sortActive([...heldRuns().map(activeAssistantRun).filter((run) => run !== null), ...allArchitectureRuns().map(activeArchitectureRun)]);
13439
+ }
13440
+ /**
13441
+ * How many executions are going on now, as the count beside Runs shows it on every page: the Runs
13442
+ * page's own Running figure, without reading the week of history to get it.
13443
+ */
13444
+ function runningNow(now = Date.now()) {
13445
+ return runningCount(liveOperations(now), liveActiveRuns());
13320
13446
  }
13321
13447
  //#endregion
13322
13448
  //#region ../../packages/review-harness/src/workspace/reviews_revision.ts
@@ -13464,7 +13590,9 @@ var RUN_KIND_LABELS = {
13464
13590
  review: "Initial review",
13465
13591
  update: "Update",
13466
13592
  "re-run": "Re-run",
13467
- kiss: "KISS review"
13593
+ kiss: "KISS review",
13594
+ ask: "Buddy AI",
13595
+ diagram: "Diagram"
13468
13596
  };
13469
13597
  var STATUS_LABELS = {
13470
13598
  completed: "Completed",
@@ -13479,10 +13607,10 @@ var SOURCE_LABELS = {
13479
13607
  };
13480
13608
  function renderRunsPage(view) {
13481
13609
  const now = view.now ?? Date.now();
13482
- const { inProgress, recent } = view.runs;
13610
+ const { inProgress, active, recent } = view.runs;
13483
13611
  const counts = runCounts(view.runs);
13484
13612
  const shown = recent.slice(0, MAX_RUNS_SENT);
13485
- const nothing = inProgress.length === 0 && recent.length === 0;
13613
+ const nothing = inProgress.length === 0 && active.length === 0 && recent.length === 0;
13486
13614
  const notResponding = recent.some((entry) => entry.status === "not-responding");
13487
13615
  return `<!doctype html>
13488
13616
  <html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
@@ -13494,27 +13622,29 @@ function renderRunsPage(view) {
13494
13622
  <link rel="stylesheet" href="/assets/sidebar.css">
13495
13623
  <link rel="stylesheet" href="/assets/shell.css">
13496
13624
  </head>
13497
- <body class="prb-index-body" data-way-back="${escapeHtml$1(wayBackSentence())}"${view.revision === void 0 ? "" : ` data-runs-revision="${escapeHtml$1(view.revision)}"`} data-live="${inProgress.length > 0}">
13625
+ <body class="prb-index-body" data-way-back="${escapeHtml$1(wayBackSentence())}"${view.revision === void 0 ? "" : ` data-runs-revision="${escapeHtml$1(view.revision)}"`} data-live="${inProgress.length > 0 || active.length > 0}">
13498
13626
  ${renderGlobalBar({
13499
13627
  current: "runs",
13500
13628
  unread: view.unreadNotifications ?? 0,
13629
+ running: view.runningRuns ?? 0,
13501
13630
  driving: view.driving
13502
13631
  })}
13503
- ${renderContextBar("Runs", nothing ? "" : renderControls(notResponding))}
13504
13632
  <div class="prb-shell">
13633
+ ${renderContextBar("Runs", { controls: nothing ? void 0 : renderControls(notResponding) })}
13505
13634
  ${renderWorkspaceRail({
13506
13635
  current: "runs",
13507
13636
  unread: view.unreadNotifications ?? 0,
13637
+ running: view.runningRuns ?? 0,
13508
13638
  sidebar: view.sidebar
13509
13639
  })}
13510
13640
  <main class="prb-index prb-runs">
13511
13641
  <p class="prb-runs-lede">Monitor review executions and recent activity.</p>
13512
13642
  ${renderSummary(counts)}
13513
- ${nothing ? `<p class="prb-index-empty" data-runs-empty>Nothing has run in the last seven days. Reviews, updates and re-runs appear here as soon as they start, whether from this workspace, a terminal or an MCP client.</p>` : `${renderInProgress(inProgress, view.reviews, now, runsSectionOpen("in-progress", view.sectionsCollapsed))}
13643
+ ${nothing ? `<p class="prb-index-empty" data-runs-empty>Nothing has run in the last seven days. Reviews, updates and re-runs appear here as soon as they start, whether from this workspace, a terminal or an MCP client.</p>` : `${renderInProgress(inProgress, active, view.reviews, now, runsSectionOpen("in-progress", view.sectionsCollapsed))}
13514
13644
  ${renderRecent(shown, recent.length, view, now, runsSectionOpen("recent", view.sectionsCollapsed))}
13515
13645
  <p class="prb-index-none" data-no-matches hidden>No runs match these filters. <button type="button" class="prb-index-clear" data-clear-filters-inline>Clear filters</button></p>`}
13516
13646
  ${renderCleanup(view.cleanup, now)}
13517
- <p class="prb-index-note">Runs are kept for seven days, on this machine only. Older ones are never shown, and their records are deleted by the workspace server, which checks every ten minutes while it is running and once when it starts. Removing a run takes it off this page and changes nothing about its review.</p>
13647
+ <p class="prb-index-note">Runs are kept for seven days, on this machine only. Older ones are never shown, and their records are deleted by the workspace server, which checks every ten minutes while it is running and once when it starts. Removing a run takes it off this page and changes nothing about its review. Questions to Buddy AI and diagrams being drawn are listed while they run and are not kept afterwards.</p>
13518
13648
  </main>
13519
13649
  </div>
13520
13650
  <script src="/assets/runs.js" type="module"><\/script>
@@ -13576,12 +13706,28 @@ function renderControls(notResponding) {
13576
13706
  * the scope that list has on purpose: work that is running is seen machine-wide, while a request
13577
13707
  * still waiting for a slot is held by the server that accepted it and nowhere else.
13578
13708
  */
13579
- function renderInProgress(operations, reviews, now, open) {
13709
+ /**
13710
+ * Work going on now: review jobs and the rows waiting for them, then what the in-memory registries
13711
+ * hold (questions, drawings, mapping), each entry one execution.
13712
+ *
13713
+ * The two kinds are listed together by when they started, running before queued, so the list reads
13714
+ * as one account of what is happening rather than as two sources the reader has to merge.
13715
+ */
13716
+ function renderInProgress(operations, active, reviews, now, open) {
13717
+ const entries = [...operations.filter((operation) => operation.kind !== "queued").map((operation) => ({
13718
+ at: operation.startedAt ?? 0,
13719
+ html: renderOperationRun(operation, reviewOf(operation, reviews), now)
13720
+ })), ...active.map((run) => ({
13721
+ at: run.startedAt,
13722
+ html: renderActiveRun(run, reviews, now)
13723
+ }))].sort((a, b) => b.at - a.at);
13724
+ const queued = operations.filter((operation) => operation.kind === "queued").map((operation) => renderOperationRun(operation, reviewOf(operation, reviews), now));
13725
+ const count = operations.length + active.length;
13580
13726
  return `<details class="prb-index-section prb-runs-section" data-runs-section="in-progress"${open ? " open" : ""}>
13581
- <summary class="prb-index-section-head">${RUNS_SECTION_NAMES["in-progress"].title} · <span data-section-count>${operations.length}</span></summary>
13582
- ${operations.length === 0 ? "<p class=\"prb-runs-idle\" data-runs-idle>Nothing is running.</p>" : ""}
13727
+ <summary class="prb-index-section-head">${RUNS_SECTION_NAMES["in-progress"].title} · <span data-section-count>${count}</span></summary>
13728
+ ${count === 0 ? "<p class=\"prb-runs-idle\" data-runs-idle>Nothing is running.</p>" : ""}
13583
13729
  <div class="prb-cards" data-run-list>
13584
- ${operations.map((operation) => renderOperationRun(operation, reviewOf(operation, reviews), now)).join("")}
13730
+ ${entries.map((entry) => entry.html).join("")}${queued.join("")}
13585
13731
  </div>
13586
13732
  <p class="prb-runs-scope">Queued shows the reviews added from this workspace. Pull requests waiting in a terminal batch appear once they start.</p>
13587
13733
  </details>`;
@@ -13675,6 +13821,54 @@ function renderOperationRun(operation, review, now) {
13675
13821
  ${own ? `<a class="prb-card-link" href="${escapeHtml$1(reviewHref(own))}">${body}</a>` : `<div class="prb-card-link">${body}</div>`}
13676
13822
  </article>`;
13677
13823
  }
13824
+ var MAPPING_STEPS = {
13825
+ exploring: "Exploring the code",
13826
+ checking: "Checking the evidence",
13827
+ correcting: "Correcting the map"
13828
+ };
13829
+ /** What a registry run is doing, in the words its own pane uses. */
13830
+ function activeRunLine(run) {
13831
+ if (run.kind === "ask") return "Buddy AI · Answering a question";
13832
+ if (run.drawing === "architecture") return `${run.scope === void 0 || run.scope === "repository" ? "Mapping the architecture" : `Mapping ${run.scopeName ?? "one part"} in depth`}${run.stage ? ` · ${MAPPING_STEPS[run.stage]}` : ""}`;
13833
+ return run.drawing === "implementation" ? "Drawing an implementation diagram" : "Drawing the change map";
13834
+ }
13835
+ /** The view a registry run's work appears in, on its review's page. */
13836
+ var ACTIVE_RUN_VIEWS = {
13837
+ map: "map",
13838
+ implementation: "diagram",
13839
+ architecture: "architecture"
13840
+ };
13841
+ /**
13842
+ * A question, a drawing or a mapping run, while it lasts. A link to the review whose page shows it
13843
+ * and holds its Stop, as every entry in progress is. A mapping run opens the review it was started
13844
+ * from, at the Architecture pane with its part selected (the `part` the pane reads on load). When
13845
+ * that review is no longer here the entry says so and leads nowhere: another review of the same
13846
+ * repository is not the one that asked, and opening it would be opening somebody else's map.
13847
+ */
13848
+ function renderActiveRun(run, reviews, now) {
13849
+ const review = run.reviewId !== void 0 ? reviews.get(run.reviewId) : void 0;
13850
+ const repo = review ? lastSegment(review.repoPath) : run.repoPath ? lastSegment(run.repoPath) : void 0;
13851
+ const where = review ? {
13852
+ repo,
13853
+ pr: review.prNumber,
13854
+ branch: followedRefName(review.branch, review.headResolvedRef) ?? review.branch
13855
+ } : { repo };
13856
+ const title = review?.title || repo || "Review";
13857
+ const gone = review === void 0 && run.reviewId !== void 0;
13858
+ const body = `<div class="prb-card-head"><h3>${escapeHtml$1(title)}</h3></div>
13859
+ ${renderWhere(where)}
13860
+ <p class="prb-card-meta">
13861
+ <span class="prb-card-analysis" data-analysis="running" role="status"><i aria-hidden="true"></i><span class="prb-visually-hidden">Running: </span>${escapeHtml$1(activeRunLine(run))}</span>
13862
+ <span class="prb-run-provenance">${SOURCE_LABELS.workspace}</span>
13863
+ ${since("Started", run.startedAt, now)}
13864
+ </p>
13865
+ ${gone ? "<p class=\"prb-card-meta\" data-run-review-gone>Started from a review that is no longer here. Open the map from any review of this repository.</p>" : ""}`;
13866
+ const part = run.drawing === "architecture" && run.scope !== void 0 && run.scope !== "repository" ? `&part=${encodeURIComponent(run.scope)}` : "";
13867
+ const view = run.kind === "diagram" ? `#${ACTIVE_RUN_VIEWS[run.drawing ?? "map"]}` : "";
13868
+ return `<article class="prb-card prb-run" data-run data-run-status="in-progress" data-run-kind="${run.kind}" data-key="${escapeHtml$1(run.key)}" data-search-text="${escapeHtml$1(searchOf(`${title} ${run.scopeName ?? ""}`, where))}">
13869
+ ${review ? `<a class="prb-card-link" href="${escapeHtml$1(reviewHref(review) + part + view)}">${body}</a>` : `<div class="prb-card-link">${body}</div>`}
13870
+ </article>`;
13871
+ }
13678
13872
  /**
13679
13873
  * A run that has ended, or stopped answering.
13680
13874
  *
@@ -16160,6 +16354,7 @@ function indexView() {
16160
16354
  activity,
16161
16355
  revision: reviewsRevision(states, operations),
16162
16356
  unreadNotifications: unreadCount$1(),
16357
+ runningRuns: runningCount(operations, liveActiveRuns()),
16163
16358
  driving: isDriving(),
16164
16359
  theme: readPreferences().theme,
16165
16360
  sidebar: readPreferences().sidebar,
@@ -16253,6 +16448,7 @@ async function handleSettings(req, res, params, indexToken) {
16253
16448
  sendHtml(res, 200, renderSettingsPage({
16254
16449
  settings: readSettings(),
16255
16450
  unreadNotifications: unreadCount$1(),
16451
+ runningRuns: runningNow(),
16256
16452
  driving: isDriving(),
16257
16453
  sidebar: readPreferences().sidebar
16258
16454
  }), indexCookieHeader(indexToken));
@@ -16265,6 +16461,7 @@ function handleHelp(req, res, params, indexToken) {
16265
16461
  if (!indexAuthorised(req, params, indexToken)) return refuseIndex(req, res);
16266
16462
  sendHtml(res, 200, renderHelpPage({
16267
16463
  unreadNotifications: unreadCount$1(),
16464
+ runningRuns: runningNow(),
16268
16465
  driving: isDriving(),
16269
16466
  theme: readPreferences().theme,
16270
16467
  sidebar: readPreferences().sidebar,
@@ -16346,7 +16543,8 @@ function handleRecentNotifications(req, res, params, indexToken) {
16346
16543
  outcome: toneOf(entry)
16347
16544
  })),
16348
16545
  more,
16349
- unread: notifications.filter((entry) => entry.readAt === void 0).length
16546
+ unread: notifications.filter((entry) => entry.readAt === void 0).length,
16547
+ running: runningNow()
16350
16548
  });
16351
16549
  }
16352
16550
  /**
@@ -16361,7 +16559,10 @@ function handleRecentNotifications(req, res, params, indexToken) {
16361
16559
  function handleSeen(req, res, workspace) {
16362
16560
  if (req.method !== "POST") return send(res, 405, { error: "Method not allowed." });
16363
16561
  markReadForReview(workspace.id);
16364
- send(res, 200, { unread: unreadCount$1() });
16562
+ send(res, 200, {
16563
+ unread: unreadCount$1(),
16564
+ running: runningNow()
16565
+ });
16365
16566
  }
16366
16567
  /**
16367
16568
  * What a visible review page polls: how many are unread, and how many of those are about it.
@@ -16374,7 +16575,8 @@ function handleUnread(req, res, workspace) {
16374
16575
  const { here, elsewhere } = unreadAround(workspace.id);
16375
16576
  send(res, 200, {
16376
16577
  unread: here + elsewhere,
16377
- here
16578
+ here,
16579
+ running: runningNow()
16378
16580
  });
16379
16581
  }
16380
16582
  /**
@@ -16401,6 +16603,7 @@ async function handleNotifications(req, res, params, indexToken) {
16401
16603
  notifications,
16402
16604
  gone: new Set(notifications.filter((entry) => !loadWorkspace(entry.reviewId)).map((entry) => entry.id)),
16403
16605
  unreadNotifications: unreadCount$1(),
16606
+ runningRuns: runningNow(),
16404
16607
  driving: isDriving(),
16405
16608
  theme: readPreferences().theme,
16406
16609
  sidebar: readPreferences().sidebar
@@ -16422,6 +16625,7 @@ function handleRuns(req, res, params, indexToken) {
16422
16625
  cleanup,
16423
16626
  revision: runsRevision(runs, cleanup),
16424
16627
  unreadNotifications: unreadCount$1(),
16628
+ runningRuns: runCounts(runs).running,
16425
16629
  driving: isDriving(),
16426
16630
  theme: preferences.theme,
16427
16631
  sidebar: preferences.sidebar,
@@ -16439,8 +16643,9 @@ function handleRunsRevision(req, res, indexToken) {
16439
16643
  const runs = liveRuns();
16440
16644
  send(res, 200, {
16441
16645
  revision: runsRevision(runs, lastSweep()),
16442
- live: runs.inProgress.length > 0,
16443
- unread: unreadCount$1()
16646
+ live: runs.inProgress.length > 0 || runs.active.length > 0,
16647
+ unread: unreadCount$1(),
16648
+ running: runCounts(runs).running
16444
16649
  });
16445
16650
  }
16446
16651
  /** Removing one ended run from the Runs page. See `run_removal.ts` for what that does and does not touch. */
@@ -16492,6 +16697,7 @@ function handleLiveNotifications(req, res, params, indexToken) {
16492
16697
  const wanted = new Set((params.get("ids") ?? "").split(",").filter(Boolean).slice(0, 200));
16493
16698
  send(res, 200, {
16494
16699
  unread: notifications.filter((entry) => entry.readAt === void 0).length,
16700
+ running: runningNow(),
16495
16701
  states: Object.fromEntries(notifications.map((entry) => [entry.id, rowStateOf(entry)])),
16496
16702
  rows: notifications.filter((entry) => wanted.has(entry.id)).map((entry) => renderNotificationRow(entry, !loadWorkspace(entry.reviewId)))
16497
16703
  });
@@ -16730,7 +16936,8 @@ function handleRevision(req, res, indexToken) {
16730
16936
  revision: reviewsRevision(states, operations),
16731
16937
  live: anythingRunning(states, operations),
16732
16938
  driving: isDriving(),
16733
- unread: unreadCount$1()
16939
+ unread: unreadCount$1(),
16940
+ running: runningCount(operations, liveActiveRuns())
16734
16941
  });
16735
16942
  }
16736
16943
  var REVIEW_ID = /^[0-9a-f-]{36}$/;
@@ -17271,6 +17478,7 @@ async function sendWorkspacePage(res, workspace) {
17271
17478
  complete: workspace.completedAt !== void 0,
17272
17479
  pinned: workspace.pinned === true,
17273
17480
  unreadNotifications: unreadAround(workspace.id).elsewhere,
17481
+ runningRuns: runningNow(),
17274
17482
  driving: isDriving(),
17275
17483
  prContext: workspace.prContext ? {
17276
17484
  prContext: workspace.prContext,