prreviewbuddy 0.86.7 → 0.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 DetachedHeadError, $n as fillFileUrlTemplate, $r as collectionsStamp, $t as markAllRead, A as relativeTime$1, Ai as assessmentLookup, An as CODE_COPY_CLASS, Ar as mutateWorkspace, At as checkArchitecture, B as readIndexToken, Bi as reassessIn, Bn as isWorktreeHeld, Br as workspaceStamps, Bt as REPOSITORY_SCOPE, C as RUN_KINDS, Ca as AgentCancelledError, Ci as explanationActionOf, Cn as chatConversationSection, Cr as kissDecisionRefusal, Ct as waitingHistory, D as heldRuns, Di as ANALYSIS_ONLY, Dn as GITHUB_MARKDOWN, Dr as lineagePosition, Dt as describeRuntimes, E as endRun, Ei as withExplanation, En as hasAuthorContent, Er as lineageKey, Et as RUNTIME_WORDS, F as runBatch, Fi as findingStatus, Fn as isBusy, Fr as reviewedCommit, Ft as storeArchitecture, Gi as asString, Gr as lifecycleChip, Gt as branchOf, H as clearServerRecordOwnedBy, Hi as statusLookup, Hn as removeWorktree, Hr as activityAttributes, Ht as isArchitectureId, I as activateQueued, Ii as gradeScale, In as withReviewLock, It as storedScopes, J as stopChild, Ji as describeAuthorship, Jn as checkReview, Jr as lifecyclePillAction, Jt as createArchitectureNotification, K as serverEntryExists, Ki as extractJsonText, Kn as findExistingReview, Kr as lifecycleItemLabel, Kt as subjectOf, L as compactQueue, Li as isGradeFor, Ln as MANAGED_ROOT, Lr as summarise, Lt as trackedFiles, M as extractPullRequestLinks, Mi as currentGradeIn, Mn as renderInlineText, Mr as presentedKissRunOf, Mt as partFiles, N as parsePullRequestLink, Ni as findingAssessment, Nr as previousKissResult, Nt as readArchitecture, O as reportRun, Oi as NO_STATUS, On as renderMarkdown, Or as loadWorkspace, Ot as formatDuration, P as unsupportedReason, Pi as findingIn, Pn as ReviewBeingDeletedError, Pr as recentWorkspaces, Pt as repositoryKey, Q as DEFAULT_WORKSPACE_PORT, Qi as originRemote, Qn as forgeResolver, Qr as sectionOf, Qt as listNotifications, Ri as isOffered, Rn as createWorktree, Rr as touchWorkspace, Rt as ownersOf, S as track, Sa as runnableAgents, Si as recommendationsOf, Sn as EXPLAIN_SIMPLY_PROMPT, Sr as indexReviewGroups, St as feedbackUrl, T as completeRun, Ti as isExplanationAction, Tn as authorPreview, Tr as latestKissRun, Tt as recordRuntimeBestEffort, Ui as wasReassessed, Un as requestJobStop, Ur as activityLabel, Ut as writeJsonAtomically, Vi as sourcePhrase, Vn as readMarker, Vr as LIFECYCLES, Vt as ancestorsOf, W as readServerRecord, Wi as followUpFindingId, Wr as isLifecycle, Wt as announceJob, Xi as kissOutcome, Xn as reviewedShaOf, Xr as offersLifecycle, Xt as createPrUpdate, Yi as WITHHELD_REASON, Yn as followedBranch, Yr as lifecyclePillLabel, Yt as createPrState, Z as writeServerRecord, Zi as resolveTarget, Zn as fetchHead, Zr as parseLifecycle, Zt as eventName, _ as runningJobs, _a as agentFor$1, _i as questionsOutstanding, _n as storedConversation, _t as recordTelemetryUploadConsent, a as wasBlocked, aa as git, ai as renameCollection, an as readStateOf, ar as isTerminal, at as failureOf$1, b as startKissJob, ba as isAgentId, bi as verdictClosure, bn as isUnchanged, br as groupByLineage, bt as BUILD_VERSION, c as RefreshUnavailableError, ca as resolveModel, ci as queueAdd, cn as toneOf, cr as saveJob, ct as settleReview, d as mergeUpdateInto, da as readAgentPreference, di as queueStamp, dn as withUsageRecorded, dr as isTerminalJobPhase, ea as isCommitReview, ei as createCollection, en as markRead, er as RUN_RECORD_RETENTION_MS, et as MIN_WORKSPACE_PORT, fa as writeAgentPreference, fi as readQueue, fn as existingPaths, fr as phasesFor, g as liveUpdateFor, ga as agentById, gi as issuesOutstanding, gn as saveRefresh, gr as isClaimed, h as UpdateAlreadyRunningError, ha as AGENT_IDS, hi as isQuestionOutstanding, hn as prepareConversationWithin, i as liveJobsFor, ia as shorthandOf, ii as readCollections, in as readNotification, ir as fail$1, it as writePortPreference, j as batchIdentity, ji as closesFinding, jn as escapeHtml$1, jt as loadArchitecture, k as stopRun, ki as analysisRefOf, kn as CODE_BLOCK_CLASS, kt as ARCHITECTURE_DIR, l as cancelUpdate, la as agentIdOf, li as queueAddMany, ln as unreadAround, lt as reanalyseReview, m as askCheckout, ma as STORE_ROOT, mi as isIssueOutstanding, mn as adoptLegacyBaseline, mr as purposeOf, mt as record, n as deleteReview, na as AmbiguousRefError, ni as deleteCollection, nn as moveIds, nr as attemptIdOf, nt as resolveWorkspacePort, o as discardJob, oa as refExists, oi as setMembership, on as removeNotification, or as loadJob, ot as observeReview, p as updateReview, pi as doneVerb, pn as ASK_REREAD_CEILING_MS, pr as progressSteps, pt as readEvents, q as spawnServer, qi as objectWithAnyOf, qn as checkCodeFreshness, qr as lifecycleName$1, qt as categoryOf, r as lineageIds, ra as shortNameOf, ri as nameProblemText, rn as pruneNotifications, rr as endedAt, rt as statedWorkspacePort, s as FETCH_TIMEOUT_MS, sa as resolveAgent, si as updateCollections, sn as retireNotification, sr as pruneJobResidue, st as prepareReview, t as deleteLineage, ta as resolveBase, ti as createWithMembers, tn as markReadForReview, tr as allJobs, tt as parseWorkspacePort, u as cancellableHere, ua as clearAgentPreference, ui as queueRemove, un as unreadCount$1, ur as isLiveJob, ut as runJob, v as kissAgentId, va as agentIdentityOf, vi as resolvedByConversation, vn as unforeseenFailure, vr as decideKissRecommendation, vt as telemetryUploadConsent, w as beginRun, wa as AgentUnavailableError, wi as explanationOf, wn as UNESTABLISHED_MOTIVATION, wr as kissDecisionsOf, wt as mapRuntimes, x as isDriving, xa as parkedReason, xi as processDiscussion, xn as agentEnvOf, xr as indexKeptIds, ya as detectAgents, yi as reviewerDispositions, yn as refreshPrContext, yr as followedRefName, z as settleQueue, zi as promptGrade, zn as holdWorktree, zr as workspaceRevision, zt as QUOTE_LIMITS } from "./delete_review-CeiSTV7u.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";
@@ -395,16 +395,6 @@ function ordersExplicitly(filter) {
395
395
  return filter === "queue";
396
396
  }
397
397
  /**
398
- * The lifecycle chip on a card, or absent.
399
- *
400
- * In a lifecycle section the heading already names the lifecycle, so the chip only says what moved
401
- * (`lifecycleChip`). In Pinned nothing names it, so the chip does, in the words the review page's
402
- * pill uses: the list and the page give one answer about the same review.
403
- */
404
- function cardLifecycleChip(pinned, lifecycle, activity) {
405
- return pinned ? lifecyclePill(lifecycle, activity)?.text : lifecycleChip(lifecycle, activity);
406
- }
407
- /**
408
398
  * "45 reviews · 13 branches", and "13 reviews" when those are the same thirteen things.
409
399
  *
410
400
  * The two numbers are different questions and only both worth asking once they differ. A page of
@@ -497,6 +487,13 @@ function statusLabel(status) {
497
487
  return status === "in-progress" ? "Not reviewed" : "New";
498
488
  }
499
489
  /**
490
+ * The status pill's accessible name: where the reviewer is, then what pressing it does. The visible
491
+ * words are the state, so a screen reader hears both rather than only a state it cannot act on.
492
+ */
493
+ function statusPillLabel(status) {
494
+ return `${statusLabel(status)}. Mark as ${status === "complete" ? "not reviewed" : "reviewed"}`;
495
+ }
496
+ /**
500
497
  * The bulk bar's way to tick everything the filters show, across pages.
501
498
  *
502
499
  * Counted ("Select all 7") only while the headline is not already saying "of 7", so the number is
@@ -758,6 +755,47 @@ function renderFeedbackDraftBody() {
758
755
  </div>`;
759
756
  }
760
757
  //#endregion
758
+ //#region ../../packages/review-harness/src/workspace/state_pills.ts
759
+ /**
760
+ * The two pills that say where a review stands, and change it: status (what the reviewer has done)
761
+ * and lifecycle (what happens next). Drawn on every Reviews card and in the review page's header,
762
+ * from this one module, so the two places cannot disagree in words, shape or behaviour.
763
+ *
764
+ * Pure, with no node builtins: the server renders the markup and both pages' scripts repaint it
765
+ * from an answer, through `paintStatusPill` and `paintLifecyclePill`.
766
+ *
767
+ * The pills are shortcuts for the `…` menu's items, never a second set of rules. The status pill is
768
+ * the menu's Mark reviewed / Mark not reviewed. The lifecycle pill is Mark done on Active and Move
769
+ * to active on Done. On Waiting it opens the lifecycle items the menu would offer (`offersLifecycle`),
770
+ * because Waiting has more than one way out and a toggle would have to guess which.
771
+ *
772
+ * Every lifecycle draws a pill, Active included. The pill names the stored state only; why a parked
773
+ * card is back in Active is the change chip's job (`lifecycleChip`), a separate fact.
774
+ */
775
+ /** The status pill as a button. `attributes` carries whatever the page needs to find it by. */
776
+ function statusPillHtml(status, attributes = "") {
777
+ return `<button type="button" class="prb-card-status prb-state-pill" data-status="${status}"${attributes} aria-label="${escapeHtml$1(statusPillLabel(status))}">${statusLabel(status)}</button>`;
778
+ }
779
+ /**
780
+ * The lifecycle pill. Interactive, it is a button inside a `[data-menu]`, which the shared menus
781
+ * open only while the pill is a trigger (Waiting); otherwise the page acts on the press directly.
782
+ * The panel ships with all three items so a repaint never builds markup. Not interactive (an older
783
+ * review in a lineage, where lifecycle is not offered), it is a span that only says.
784
+ */
785
+ function lifecycleControlHtml(lifecycle, activity, options) {
786
+ const state = lifecycle?.state ?? "active";
787
+ const name = escapeHtml$1(lifecycleName$1(lifecycle));
788
+ const attributes = options.attributes ?? "";
789
+ if (!options.interactive) return `<span class="prb-state-pill prb-lifecycle-pill" data-lifecycle-pill="${state}"${attributes}>${name}</span>`;
790
+ const action = lifecyclePillAction(lifecycle);
791
+ const trigger = action === "menu" ? " data-menu-trigger aria-haspopup=\"true\" aria-expanded=\"false\"" : "";
792
+ const items = LIFECYCLES.map(({ id }) => `<button type="button" class="prb-menu-item" data-lifecycle-set="${id}"${offersLifecycle(id, lifecycle, activity) ? "" : " hidden"}>${escapeHtml$1(lifecycleItemLabel(id, lifecycle, activity))}</button>`).join("");
793
+ return `<div class="prb-menu prb-pill-menu" data-menu data-lifecycle-control>
794
+ <button type="button" class="prb-state-pill prb-lifecycle-pill" data-lifecycle-pill="${state}" data-lifecycle-press="${action}"${attributes}${trigger} aria-label="${escapeHtml$1(lifecyclePillLabel(lifecycle))}">${name}</button>
795
+ <div class="prb-menu-panel" data-menu-panel hidden>${items}</div>
796
+ </div>`;
797
+ }
798
+ //#endregion
761
799
  //#region ../../packages/review-harness/src/workspace/way_back.ts
762
800
  var CLI = {
763
801
  command: "prreviewbuddy open",
@@ -867,6 +905,20 @@ function supersededLabel(because) {
867
905
  return because === "moved-again" ? "replaced by a later move" : "replaced when the review caught up";
868
906
  }
869
907
  //#endregion
908
+ //#region ../../packages/review-harness/src/workspace/running_label.ts
909
+ /**
910
+ * The words and the digit for the count beside Runs, which the server renders and the browser
911
+ * repaints. Pure, because the client bundle reads it.
912
+ */
913
+ /** In words, for a screen reader: "1 run in progress", "3 runs in progress". */
914
+ function runningLabel(running) {
915
+ return running === 0 ? "Nothing running" : `${running} ${running === 1 ? "run" : "runs"} in progress`;
916
+ }
917
+ /** The digit an icon has room for. Past nine it says there are many rather than how many. */
918
+ function runningDigit(running) {
919
+ return running > 9 ? "9+" : String(running);
920
+ }
921
+ //#endregion
870
922
  //#region ../../packages/review-harness/src/workspace/server_status.ts
871
923
  /** The dot's accessible name, which is the only form of it a screen reader gets. */
872
924
  function serverStatusLabel(status) {
@@ -891,8 +943,8 @@ var SHOW_SIDEBAR = "Show sidebar";
891
943
  /**
892
944
  * The workspace's own places, and the one model every page draws them from.
893
945
  *
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
946
+ * Drawn in the same place on every page, directly under New review, so they never move: a review
947
+ * 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
948
  * read `WORKSPACE_PLACES`, so a place added here appears everywhere and a place renamed here is
897
949
  * renamed everywhere.
898
950
  */
@@ -936,6 +988,16 @@ function sidebarAttributes(sidebar) {
936
988
  return `${sidebar?.compact ? " data-sidebar-compact" : ""}${sidebar?.hidden ? " data-sidebar-hidden" : ""}`;
937
989
  }
938
990
  /**
991
+ * The count beside Runs. Always rendered, hidden at zero, so a poll that reports a new number can
992
+ * reveal it without the rail being rebuilt. See `client/running_counts.ts`, its one painter.
993
+ *
994
+ * In the Activity accent, as the unread count beside Activity is: both say something is happening
995
+ * that the reader did not just do. The words are its label, because a bare digit names nothing.
996
+ */
997
+ function runningCount$1(running) {
998
+ 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>`;
999
+ }
1000
+ /**
939
1001
  * The count beside Notifications. Always rendered, hidden at zero, so the page can reveal it when
940
1002
  * `/revision` or Mark all read reports a new number. See `client/unread_counts.ts`.
941
1003
  */
@@ -978,19 +1040,25 @@ function renderGlobalBar(options) {
978
1040
  </header>`;
979
1041
  }
980
1042
  /**
981
- * The bar below the strip: what page this is, and the controls that belong to it.
1043
+ * The bar at the top of the content column: what page this is, and the controls that belong to it.
1044
+ * Beside the rail rather than above it, so the rail starts under the strip on every page.
982
1045
  *
983
1046
  * The dominant bar of the composition on every page, as the review's own bar is on a review page.
984
1047
  * That is the point of the split: the strip is utility chrome and this says what you are looking at.
985
1048
  *
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.
1049
+ * Two regions. The title row is the same on every page, the same height as a review's bar, with
1050
+ * page-level actions (Mark all read) at its right, so the title never moves between pages. The
1051
+ * controls row (search and filters) is drawn only by a page that has controls, which is the one
1052
+ * way the bar grows: a page with nothing to put there gets no empty band to match the others.
989
1053
  */
990
- function renderContextBar(title, controls = "") {
1054
+ function renderContextBar(title, parts = {}) {
1055
+ const controls = parts.controls ? `
1056
+ <div class="prb-context-secondary">${parts.controls}</div>` : "";
991
1057
  return `<div class="prb-context">
992
- <h1 class="prb-page-title">${escapeHtml$1(title)}</h1>
993
- ${controls}
1058
+ <div class="prb-context-primary">
1059
+ <h1 class="prb-page-title">${escapeHtml$1(title)}</h1>
1060
+ ${parts.actions ?? ""}
1061
+ </div>${controls}
994
1062
  </div>`;
995
1063
  }
996
1064
  /**
@@ -1069,9 +1137,21 @@ function renderHistoryControls() {
1069
1137
  function renderStripReviews(options) {
1070
1138
  return `<a class="prb-strip-action" href="/"${options.current === "reviews" ? " aria-current=\"true\"" : ""} aria-label="Reviews" data-strip-reviews>${iconSvg("reviews")}</a>`;
1071
1139
  }
1072
- /** Runs, beside Reviews, in the rail's order. Its own `aria-label`, for the reason Settings has one. */
1140
+ /**
1141
+ * Runs, beside Reviews, in the rail's order. Its own `aria-label`, for the reason Settings has one.
1142
+ *
1143
+ * It carries the count beside Runs as well, as the bell carries the unread one, so the work going
1144
+ * on is visible wherever the rail is not: Compact, Hidden, a narrow window. The digit is for the
1145
+ * eye; the words, its description, are what a screen reader hears after the name.
1146
+ */
1073
1147
  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>`;
1148
+ const current = options.current === "runs" ? " aria-current=\"true\"" : "";
1149
+ const running = options.running ?? 0;
1150
+ const hidden = running === 0 ? " hidden" : "";
1151
+ 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")}
1152
+ <span class="prb-strip-count" data-running-count data-running-style="digit" aria-hidden="true"${hidden}>${running === 0 ? "" : runningDigit(running)}</span>
1153
+ <span class="prb-visually-hidden" id="prb-strip-running" data-running-count data-running-style="words">${running === 0 ? "" : escapeHtml$1(runningLabel(running))}</span>
1154
+ </a>`;
1075
1155
  }
1076
1156
  /**
1077
1157
  * Settings, as an icon, beside the bell.
@@ -1132,12 +1212,13 @@ function renderServerStatus() {
1132
1212
  function renderNewReviewButton() {
1133
1213
  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
1214
  }
1135
- /** New review, then the workspace rows, for a rail group. */
1215
+ /** New review, then the workspace rows: the first group of every rail, review pages' included. */
1136
1216
  function renderWorkspaceNav(options) {
1137
1217
  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
1218
  ${railIcon(item.icon)}
1139
1219
  <span class="prb-rail-label"><span class="prb-rail-title">${escapeHtml$1(item.label)}</span></span>
1140
1220
  ${item.place === "notifications" ? unreadCount(options.unread, "rail") : ""}
1221
+ ${item.place === "runs" ? runningCount$1(options.running ?? 0) : ""}
1141
1222
  </a>`).join("");
1142
1223
  }
1143
1224
  function renderShutdownDialog() {
@@ -1170,7 +1251,7 @@ function renderSendFeedback() {
1170
1251
  */
1171
1252
  function renderWorkspaceRail(options) {
1172
1253
  return `<nav class="prb-rail" aria-label="Workspace">
1173
- <div class="prb-rail-group">${renderWorkspaceNav(options)}</div>
1254
+ <div class="prb-rail-group prb-rail-places">${renderWorkspaceNav(options)}</div>
1174
1255
  <div class="prb-rail-group prb-rail-foot">${renderSendFeedback()}${renderSidebarToggle(options.sidebar)}</div>
1175
1256
  </nav>`;
1176
1257
  }
@@ -6396,9 +6477,28 @@ function renderWorkspace(view) {
6396
6477
  }) ? " data-guided-tour=\"offer\"" : ""}${view.tourBusy ? " data-tour-busy" : ""}>
6397
6478
  ${renderGlobalBar({
6398
6479
  unread: view.unreadNotifications ?? 0,
6480
+ running: view.runningRuns ?? 0,
6399
6481
  live: Boolean(view.token),
6400
6482
  driving: view.driving
6401
6483
  })}
6484
+ ${renderReviewBody(view, sections, renderReviewHead(view, freshness))}
6485
+ ${view.token ? renderPromptDialog() : ""}
6486
+ ${offersFeedback(view) ? renderFeedbackDialog() : ""}
6487
+ <script type="module" src="/assets/workspace.js"><\/script>
6488
+ </body>
6489
+ </html>`;
6490
+ }
6491
+ /**
6492
+ * Everything about this review that sits above its panes: the review bar, then whatever banners the
6493
+ * review's state calls for (job status, freshness, the last update's outcome, the lineage notice).
6494
+ *
6495
+ * In the content column, to the right of the rail, rather than across the window. The application
6496
+ * shell is the strip and the rail, and it is the same on every page; this is the review's own
6497
+ * header, and however many banners it holds it pushes down only the review, never the rail. The
6498
+ * workspace pages put their context bar in the same column for the same reason.
6499
+ */
6500
+ function renderReviewHead(view, freshness) {
6501
+ return `<div class="prb-review-head">
6402
6502
  ${renderReviewBar(view, freshness)}
6403
6503
  ${showsProgress(view) || updating(view) ? "" : renderJobState(view.job ?? null, {
6404
6504
  dismissed: view.dismissedNotices,
@@ -6412,12 +6512,7 @@ ${showsProgress(view) || updating(view) ? "" : renderJobState(view.job ?? null,
6412
6512
  })}</div>
6413
6513
  ${updating(view) ? "" : renderUpdateOutcome(view.lastUpdate)}
6414
6514
  ${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>`;
6515
+ </div>`;
6421
6516
  }
6422
6517
  /**
6423
6518
  * Whether this page's subject is a review that is still being made, or one that stopped.
@@ -6466,9 +6561,10 @@ function showsProgress(view) {
6466
6561
  * question box offering to discuss findings nobody has produced is a worse thing to hand somebody
6467
6562
  * than no box at all.
6468
6563
  */
6469
- function renderReviewBody(view, sections) {
6564
+ function renderReviewBody(view, sections, head = "") {
6470
6565
  const result = view.session.result;
6471
6566
  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"` : ""}>
6567
+ ${head}
6472
6568
  ${renderRail(view, sections, updating(view))}
6473
6569
  <main class="prb-doc">
6474
6570
  ${updating(view) ? renderUpdatingDoc(view) : renderReviewDoc(view, sections)}
@@ -6884,20 +6980,30 @@ function renderReviewBar(view, freshness) {
6884
6980
  </div>`;
6885
6981
  }
6886
6982
  /**
6887
- * Where this review stands, as pills that say and never do.
6983
+ * Where this review stands, as two pills that say it and change it.
6888
6984
  *
6889
6985
  * Two, because they answer two questions. Reviewed or Not reviewed is what the reviewer has done;
6890
- * Waiting or Done is what happens next with the branch. A review can be Reviewed and Waiting, so a
6891
- * single pill would have to drop one of them. Active draws nothing, as an open pull request does
6892
- * not: it is where every review starts, and a pill that never varies teaches nothing.
6986
+ * Active, Waiting or Done is what happens next with the branch. A review can be Reviewed and
6987
+ * Waiting, so a single pill would have to drop one of them. Each is a shortcut for its `…` items
6988
+ * (see `state_pills.ts`); the lifecycle pill acts only where those items are offered, on the newest
6989
+ * review of the branch, and on an older one only says.
6990
+ *
6991
+ * Why a parked review has moved is its own chip beside them, the card's chip, never the pill's text.
6893
6992
  *
6894
6993
  * Never "New" here, because being on this page is what ends it.
6895
6994
  */
6896
6995
  function renderStatePills(view) {
6897
6996
  const status = view.complete ? "complete" : "in-progress";
6898
- const pill = lifecyclePill(view.lifecycle, view.activity);
6899
- return `<span class="prb-card-status" data-status="${status}" data-page-status>${statusLabel(status)}</span>
6900
- <span class="prb-card-status prb-page-lifecycle" data-page-lifecycle${pill?.changed ? " data-changed" : ""}${pill ? "" : " hidden"}>${escapeHtml$1(pill?.text ?? "")}</span>`;
6997
+ const latest = !view.lineage || view.lineage.position >= view.lineage.total;
6998
+ const chip = lifecycleChip(view.lifecycle, view.activity);
6999
+ return `<span class="prb-state-pills">
7000
+ ${statusPillHtml(status, " data-page-status")}
7001
+ ${lifecycleControlHtml(view.lifecycle, view.activity, {
7002
+ interactive: latest,
7003
+ attributes: " data-page-lifecycle"
7004
+ })}
7005
+ <span class="prb-card-lifecycle" data-page-lifecycle-chip${chip === void 0 ? " hidden" : ""}>${escapeHtml$1(chip ?? "")}</span>
7006
+ </span>`;
6901
7007
  }
6902
7008
  /**
6903
7009
  * Everything that can be done to this review, from the page you are reading it on.
@@ -6961,46 +7067,33 @@ function renderIdMenu(view, freshness) {
6961
7067
  </div>`;
6962
7068
  }
6963
7069
  /**
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.
7070
+ * The foot of the rail: Send feedback and the sidebar's own toggle, anchored at the bottom whichever
7071
+ * review is open and however long its section is.
6974
7072
  *
6975
7073
  * 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
7074
+ * it carries is decided in `version.ts` (version, client, OS), and deliberately not the
6977
7075
  * repository or its path.
6978
7076
  */
6979
7077
  function renderRailFoot(view) {
6980
7078
  return `<div class="prb-rail-group prb-rail-foot">
6981
- ${view.token ? renderWorkspaceNav({ unread: view.unreadNotifications ?? 0 }) : ""}
6982
7079
  ${renderSendFeedback()}
6983
7080
  ${view.token ? renderSidebarToggle(view.sidebar) : ""}
6984
7081
  </div>`;
6985
7082
  }
6986
7083
  /**
6987
- * The rail: one group per review type, each opening its own summary and folding away.
7084
+ * The rail of a review page: the workspace's places, then a section for this review, then the foot.
6988
7085
  *
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.
7086
+ * The places come first and are the very rows every other page draws (`workspace_nav.ts`), so they
7087
+ * hold one position across the whole workspace. Everything about this review is contextual and sits
7088
+ * below them, under a heading rather than a row of its own: the reader is already on the review, so
7089
+ * a "Review" entry would be a door to the room they are standing in. Its first row is the review's
7090
+ * own page (`renderReviewHome`).
6993
7091
  *
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.
7092
+ * KISS is in the same section, as a group of its own with its pages under it: it is a second
7093
+ * question asked of this change, and it belongs to the review the way Issues and Files do.
7000
7094
  *
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.
7095
+ * The review path keeps a twisty of its own. It is the long part of the section and the short links
7096
+ * under it are the ones a reviewer reaches for repeatedly, so it folds on its own.
7004
7097
  *
7005
7098
  * Themes are numbered because the guide genuinely is an ordered walkthrough — `order` is the
7006
7099
  * model's answer to "what sequence should a reviewer take through this change" — so the number
@@ -7025,16 +7118,7 @@ function renderRail(view, sections, stale = false) {
7025
7118
  <span class="prb-attention" data-attention="${escapeHtml$1(section.reviewAttention)}" title="Needs ${escapeHtml$1(section.reviewAttention)} attention"></span>
7026
7119
  </a>`;
7027
7120
  }).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">
7121
+ 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
7122
  <span class="prb-rail-heading">Review path</span>
7039
7123
  <span class="prb-rail-twisty" aria-hidden="true">${TWISTY}</span>
7040
7124
  </button>
@@ -7045,49 +7129,66 @@ function renderRail(view, sections, stale = false) {
7045
7129
  ${railLink("issues", "issues", "Issues", findingsBadge(issuesOutstanding(issues, dispositions)), void 0, void 0, true)}
7046
7130
  ${railLink("questions", "questions", "Questions", remainingBadge(questionsOutstanding(questions, answered, verdictClosure(view.session).question), "question", "answered"), void 0, void 0, true)}
7047
7131
  ${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)}
7132
+ ${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);
7133
+ return `<nav class="prb-rail" aria-label="Workspace">
7134
+ ${view.token ? `<div class="prb-rail-group prb-rail-places">${renderWorkspaceNav({
7135
+ unread: view.unreadNotifications ?? 0,
7136
+ running: view.runningRuns ?? 0
7137
+ })}</div>` : ""}
7138
+ <section class="prb-rail-context" id="prb-rail-review" aria-labelledby="prb-rail-context-title"${stale ? " inert data-stale" : ""}>
7139
+ <h2 class="prb-rail-heading prb-rail-context-title" id="prb-rail-context-title">Current review</h2>
7140
+ ${renderReviewHome(view)}
7141
+ ${standard}
7142
+ ${renderKissRail(view)}
7143
+ </section>
7053
7144
  ${renderRailFoot(view)}
7054
7145
  </nav>`;
7055
7146
  }
7056
7147
  /**
7057
- * The Review group while the review is still being made.
7148
+ * The review's own first page: Overview once there is a result to present, Progress until then.
7058
7149
  *
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.
7150
+ * Decided by the result, not by whether anything is running. An update keeps the result it is
7151
+ * replacing, so the row stays Overview through it; a first analysis, or a re-run, which clears the
7152
+ * result before it starts, has nothing to present yet, and a failed one keeps Progress so its
7153
+ * failure and its Retry stay one press away. Both are the same row in the same slot, so the label
7154
+ * changing moves nothing. A review with neither (KISS alone) draws none: KISS is its page.
7063
7155
  *
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.
7156
+ * Current when the server draws it, as the default view is, so the first paint is already right;
7157
+ * the client keeps `aria-current` true from there, as it does for every `data-view-nav`.
7158
+ */
7159
+ function renderReviewHome(view) {
7160
+ const home = view.session.result ? {
7161
+ view: "overview",
7162
+ label: "Overview"
7163
+ } : showsProgress(view) ? {
7164
+ view: "progress",
7165
+ label: "Progress"
7166
+ } : null;
7167
+ if (!home) return "";
7168
+ 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">
7169
+ ${railIcon("review")}
7170
+ <span class="prb-rail-label"><span class="prb-rail-title">${home.label}</span></span>
7171
+ </a>`;
7172
+ }
7173
+ /**
7174
+ * What the review's section carries while the review is still being made.
7175
+ *
7176
+ * What actually exists: the files, once they have been read, under the Progress row. There are
7177
+ * deliberately no Issues or Questions rows waiting to be filled in. A badge is a measured figure,
7178
+ * so a `0` beside Issues would report that an analysis nobody has finished found nothing, and a
7179
+ * row with no badge is a door onto an empty room. The rail says what is there; the Progress pane
7180
+ * is where "and these arrive later" belongs.
7069
7181
  */
7070
7182
  function renderRunningRail(view) {
7071
7183
  if (!showsProgress(view)) return "";
7072
7184
  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
- });
7185
+ return view.job && changeIsRead(view.job) ? railLink("files", "files", "Files", totalBadge(files, "files")) : "";
7084
7186
  }
7085
7187
  /**
7086
- * The KISS section of the rail, present before anybody has run it.
7188
+ * The KISS group of the review's section, present before anybody has run it.
7087
7189
  *
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.
7190
+ * A group of its own rather than a row among Issues and Files, because it is a second question
7191
+ * asked of the same change: whether its complexity is justified, beside whether it is correct.
7091
7192
  *
7092
7193
  * Visible on a change nobody has run it on, which is the point. The entry is what teaches that one
7093
7194
  * change can be interrogated through more than one review type, and a reader who never sees it
@@ -8053,6 +8154,22 @@ function architectureRunsFor(repoPath) {
8053
8154
  startedAt
8054
8155
  })).sort((a, b) => Number(b.scope === "repository") - Number(a.scope === "repository") || a.startedAt - b.startedAt);
8055
8156
  }
8157
+ /**
8158
+ * Every repository being mapped now, for the Runs page. `key` is the run's own key, repository and
8159
+ * scope, so two scopes of one repository are two entries and one scope is never listed twice.
8160
+ */
8161
+ function allArchitectureRuns() {
8162
+ return [...runs.entries()].map(([key, { repoPath, scope, stage, steps, startedAt, record, partName }]) => ({
8163
+ key,
8164
+ repoPath,
8165
+ scope,
8166
+ stage,
8167
+ steps,
8168
+ startedAt,
8169
+ reviewId: record.reviewId,
8170
+ ...partName ? { partName } : {}
8171
+ }));
8172
+ }
8056
8173
  /** Whether a record that says `running` is really running, or was cut short. */
8057
8174
  function isRunLive(repoPath, scope) {
8058
8175
  return runs.has(keyOf(repoPath, scope));
@@ -8116,7 +8233,8 @@ function startArchitectureRun(workspace, scope, deps = {}) {
8116
8233
  steps: 0,
8117
8234
  startedAt,
8118
8235
  controller: new AbortController(),
8119
- record
8236
+ record,
8237
+ ...partName ? { partName } : {}
8120
8238
  };
8121
8239
  runs.set(key, held);
8122
8240
  saveRunRecord(repoPath, record);
@@ -9997,17 +10115,19 @@ function renderReviewsPage(view) {
9997
10115
  ${renderGlobalBar({
9998
10116
  current: "reviews",
9999
10117
  unread: view.unreadNotifications ?? 0,
10118
+ running: view.runningRuns ?? 0,
10000
10119
  driving: view.driving
10001
10120
  })}
10002
- ${renderContextBar("Reviews", ordered.length === 0 ? "" : renderControls$1(queue.length, authorOptions(ordered.map(({ latest }) => ({
10121
+ <div class="prb-shell">
10122
+ ${renderContextBar("Reviews", { controls: ordered.length === 0 ? void 0 : renderControls$1(queue.length, authorOptions(ordered.map(({ latest }) => ({
10003
10123
  handle: latest.authorHandle,
10004
10124
  name: latest.authorName,
10005
10125
  createdAt: latest.createdAt
10006
- }))), ordered.some((group) => group.latest.prNumber !== void 0), context.refs.length > 0))}
10007
- <div class="prb-shell">
10126
+ }))), ordered.some((group) => group.latest.prNumber !== void 0), context.refs.length > 0) })}
10008
10127
  ${renderWorkspaceRail({
10009
10128
  current: "reviews",
10010
10129
  unread: view.unreadNotifications ?? 0,
10130
+ running: view.runningRuns ?? 0,
10011
10131
  sidebar: view.sidebar
10012
10132
  })}
10013
10133
  <main class="prb-index">
@@ -10463,7 +10583,10 @@ function renderBulkBar() {
10463
10583
  * this branch as far as the product knows; the earlier ones are history and are folded away until
10464
10584
  * asked for.
10465
10585
  *
10466
- * The checkbox sits outside the link, like the pin and the menu and for the same reason: it is a
10586
+ * The two pills, the menu and the pin are one row of controls over the card's top-right corner,
10587
+ * outside its link and after it, so a reader meets the title first and then what can be done to it:
10588
+ * where the reviewer is, what happens next, then the menu and the star. Outside because a button inside a link is not a thing HTML has, and a press on any of
10589
+ * them must never open the review. The checkbox sits outside the link for the same reason: it is a
10467
10590
  * control on the row rather than a way into the review. It is named by its branch, because fifteen
10468
10591
  * boxes announcing themselves as "Select" are fifteen identical announcements. It ships on every
10469
10592
  * card and is shown only in selection mode, so entering that mode is a stylesheet change rather
@@ -10480,17 +10603,14 @@ function renderCard(group, now, activity, rank, analysis, queued, moved, prState
10480
10603
  const line = collectionsLine(instance.memberOf, instance.refs, instance.placement === "home" ? void 0 : instance.placement.collection);
10481
10604
  const hidden = inProgressOnly || instance.hidden;
10482
10605
  const status = statusOf(review);
10483
- const chip = cardLifecycleChip(review.pinned === true, review.lifecycle, activity);
10606
+ const chip = lifecycleChip(review.lifecycle, activity);
10484
10607
  const href = `/r/${encodeURIComponent(review.id)}?t=${encodeURIComponent(review.token)}`;
10485
10608
  const followed = review.targetType === "commit" ? `commit ${(review.commitSha ?? review.branch).slice(0, 7)}` : followedRefName(review.branch, review.headResolvedRef) ?? review.branch;
10486
10609
  return `<article class="prb-card" data-card data-id="${escapeHtml$1(review.id)}" data-status="${status}" data-pinned="${review.pinned}"${queued === void 0 ? "" : ` data-queue="${queued}"`}${prState === void 0 ? "" : ` data-pr-state="${prState}"`}${review.authorHandle ? ` data-author="${escapeHtml$1(review.authorHandle)}"` : ""} data-lifecycle="${review.lifecycle?.state ?? "active"}"${review.lifecycle ? ` data-lifecycle-at="${review.lifecycle.at}"` : ""}${activityAttributes(activity)} data-rank="${rank}" data-reviews="${group.history.length}" data-lineage="${escapeHtml$1(group.history.map((entry) => entry.id).join(" "))}" data-search-text="${escapeHtml$1(groupSearchText(group))}" data-placement="${escapeHtml$1(placement)}" data-collections="${escapeHtml$1(instance.memberOf.join(" "))}"${inProgressOnly ? " data-in-progress-only" : ""}${hidden ? " hidden" : ""}>
10487
10610
  <input type="checkbox" class="prb-card-select" data-select="${escapeHtml$1(review.id)}" aria-label="Select ${escapeHtml$1(followed)}">
10488
- <button type="button" class="prb-card-pin" data-pin="${escapeHtml$1(review.id)}" aria-pressed="${review.pinned}" aria-label="${review.pinned ? "Unpin" : "Pin"} this review">${review.pinned ? "&#9733;" : "&#9734;"}</button>
10489
- ${renderCardMenu(group, followed, queued !== void 0, status, activity)}
10490
10611
  <a class="prb-card-link" href="${escapeHtml$1(href)}">
10491
10612
  <div class="prb-card-head">
10492
10613
  <h2>${escapeHtml$1(review.title)}</h2>
10493
- ${renderStatus(status)}
10494
10614
  ${renderPrState(prState)}
10495
10615
  ${`<span class="prb-card-lifecycle" data-lifecycle-chip${chip === void 0 ? " hidden" : ""}>${escapeHtml$1(chip ?? "")}</span>`}
10496
10616
  </div>
@@ -10507,12 +10627,18 @@ function renderCard(group, now, activity, rank, analysis, queued, moved, prState
10507
10627
  ${moved ? `<span class="prb-card-moved">${moved === "branch" ? "Branch updated" : "PR updated"}</span>` : ""}
10508
10628
  <span>${review.changedFiles} file${review.changedFiles === 1 ? "" : "s"}</span>
10509
10629
  <span class="prb-card-diffstat"><span class="prb-add">+${review.additions}</span> <span class="prb-del">&minus;${review.deletions}</span></span>
10510
- ${analysis !== void 0 && !review.hasStandardReview ? "" : `<span class="prb-card-left">${escapeHtml$1(whatIsLeft(review))}</span>`}
10630
+ ${analysis !== void 0 && !review.hasStandardReview ? "" : `<span class="prb-card-left">${renderLeft(whatIsLeft(review))}</span>`}
10511
10631
  ${renderAnalysisState(analysis)}
10512
10632
  <span class="prb-card-collections" data-collections-line${line === void 0 ? " hidden" : ""}>${escapeHtml$1(line ?? "")}</span>
10513
10633
  <span class="prb-card-when"><time datetime="${new Date(activity.at).toISOString()}" title="${escapeHtml$1(new Date(activity.at).toLocaleString())}">${escapeHtml$1(`${activityLabel(activity.kind)} ${relativeTime(activity.at, now)}`)}</time></span>
10514
10634
  </p>
10515
10635
  </a>
10636
+ <div class="prb-card-controls">
10637
+ ${statusPillHtml(status)}
10638
+ ${lifecycleControlHtml(review.lifecycle, activity, { interactive: true })}
10639
+ ${renderCardMenu(group, followed, queued !== void 0, status, activity)}
10640
+ <button type="button" class="prb-card-pin" data-pin="${escapeHtml$1(review.id)}" aria-pressed="${review.pinned}" aria-label="${review.pinned ? "Unpin" : "Pin"} this review">${review.pinned ? "&#9733;" : "&#9734;"}</button>
10641
+ </div>
10516
10642
  ${renderHistory(group, now)}
10517
10643
  </article>`;
10518
10644
  }
@@ -10698,14 +10824,30 @@ function renderAnalysisState(analysis) {
10698
10824
  * product does not track file-by-file review, so the card does not pretend it does.
10699
10825
  */
10700
10826
  function whatIsLeft(review) {
10701
- if (!review.hasStandardReview) return "No standard review yet";
10827
+ if (!review.hasStandardReview) return [{ text: "No standard review yet" }];
10702
10828
  const parts = [];
10703
- if (review.openIssues > 0) parts.push(`${review.openIssues} issue${review.openIssues === 1 ? "" : "s"}`);
10704
- else if (review.issues > 0) parts.push("all issues resolved");
10705
- if (review.unansweredQuestions > 0) parts.push(`${review.unansweredQuestions} unanswered question${review.unansweredQuestions === 1 ? "" : "s"}`);
10706
- else if (review.questions > 0) parts.push("all questions answered");
10707
- if (review.messages > 0) parts.push(`${review.messages} Buddy AI message${review.messages === 1 ? "" : "s"}`);
10708
- return parts.length > 0 ? parts.join(" · ") : "Nothing flagged";
10829
+ if (review.openIssues > 0) parts.push({
10830
+ text: `${review.openIssues} issue${review.openIssues === 1 ? "" : "s"}`,
10831
+ tone: "attention"
10832
+ });
10833
+ else if (review.issues > 0) parts.push({
10834
+ text: "Issues resolved",
10835
+ tone: "success"
10836
+ });
10837
+ if (review.unansweredQuestions > 0) parts.push({
10838
+ text: `${review.unansweredQuestions} unanswered question${review.unansweredQuestions === 1 ? "" : "s"}`,
10839
+ tone: "attention"
10840
+ });
10841
+ else if (review.questions > 0) parts.push({
10842
+ text: "Questions answered",
10843
+ tone: "success"
10844
+ });
10845
+ if (review.messages > 0) parts.push({ text: `${review.messages} Buddy AI message${review.messages === 1 ? "" : "s"}` });
10846
+ return parts.length > 0 ? parts : [{ text: "Nothing flagged" }];
10847
+ }
10848
+ /** The parts as the meta line draws them: a badge where there is a tone, plain words otherwise. */
10849
+ function renderLeft(parts) {
10850
+ return parts.map((part) => part.tone === void 0 ? `<span>${escapeHtml$1(part.text)}</span>` : `<span class="prb-card-badge" data-tone="${part.tone}">${part.tone === "success" ? "<span aria-hidden=\"true\">✓</span> " : ""}${escapeHtml$1(part.text)}</span>`).join("");
10709
10851
  }
10710
10852
  /**
10711
10853
  * Three states, and only one of them is inferred from anything.
@@ -11265,13 +11407,15 @@ function renderSettingsPage(view) {
11265
11407
  ${renderGlobalBar({
11266
11408
  current: "settings",
11267
11409
  unread: view.unreadNotifications ?? 0,
11410
+ running: view.runningRuns ?? 0,
11268
11411
  driving: view.driving
11269
11412
  })}
11270
- ${renderContextBar("Settings", renderBack())}
11271
11413
  <div class="prb-shell">
11414
+ ${renderContextBar("Settings")}
11272
11415
  ${renderWorkspaceRail({
11273
11416
  current: "settings",
11274
11417
  unread: view.unreadNotifications ?? 0,
11418
+ running: view.runningRuns ?? 0,
11275
11419
  sidebar: view.sidebar
11276
11420
  })}
11277
11421
  <main class="prb-index prb-settings">
@@ -11301,21 +11445,6 @@ ${renderShutdownDialog()}
11301
11445
  </html>`;
11302
11446
  }
11303
11447
  /**
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
11448
  * Stopping the server, on the one page where stopping it is a decision rather than a stray click.
11320
11449
  *
11321
11450
  * It used to sit at the foot of every rail, one press from ending every open review, beside the
@@ -12044,17 +12173,19 @@ function renderHelpPage(view) {
12044
12173
  <link rel="stylesheet" href="/assets/sidebar.css">
12045
12174
  <link rel="stylesheet" href="/assets/shell.css">
12046
12175
  </head>
12047
- <body class="prb-index-body" data-way-back="${escapeHtml$1(wayBackSentence())}">
12176
+ <body class="prb-index-body" data-way-back="${escapeHtml$1(wayBackSentence())}" data-notifications-since="${Date.now()}">
12048
12177
  ${renderGlobalBar({
12049
12178
  current: "help",
12050
12179
  unread: view.unreadNotifications ?? 0,
12180
+ running: view.runningRuns ?? 0,
12051
12181
  driving: view.driving
12052
12182
  })}
12053
- ${renderContextBar("Help", "")}
12054
12183
  <div class="prb-shell">
12184
+ ${renderContextBar("Help")}
12055
12185
  ${renderWorkspaceRail({
12056
12186
  current: "help",
12057
12187
  unread: view.unreadNotifications ?? 0,
12188
+ running: view.runningRuns ?? 0,
12058
12189
  sidebar: view.sidebar
12059
12190
  })}
12060
12191
  <main class="prb-index prb-help">
@@ -12093,13 +12224,15 @@ function renderNotificationsPage(view) {
12093
12224
  ${renderGlobalBar({
12094
12225
  current: "notifications",
12095
12226
  unread: view.unreadNotifications ?? 0,
12227
+ running: view.runningRuns ?? 0,
12096
12228
  driving: view.driving
12097
12229
  })}
12098
- ${renderContextBar("Activity", view.notifications.length === 0 ? "" : renderReadAll(unread))}
12099
12230
  <div class="prb-shell">
12231
+ ${renderContextBar("Activity", { actions: view.notifications.length === 0 ? void 0 : renderReadAll(unread) })}
12100
12232
  ${renderWorkspaceRail({
12101
12233
  current: "notifications",
12102
12234
  unread: view.unreadNotifications ?? 0,
12235
+ running: view.runningRuns ?? 0,
12103
12236
  sidebar: view.sidebar
12104
12237
  })}
12105
12238
  <main class="prb-index prb-notifications-page">
@@ -13150,6 +13283,37 @@ async function chooseFolder(deps) {
13150
13283
  }
13151
13284
  //#endregion
13152
13285
  //#region ../../packages/review-harness/src/workspace/runs.ts
13286
+ /**
13287
+ * A question or a drawing, while it is still running. A stopped one is over for the reader, and an
13288
+ * answered one is only being saved: neither is executing, so neither is listed or counted.
13289
+ */
13290
+ function activeAssistantRun(run) {
13291
+ if (run.state !== "running") return null;
13292
+ return {
13293
+ key: `${run.kind}:${run.workspaceId}`,
13294
+ kind: run.kind,
13295
+ ...run.kind === "diagram" ? { drawing: run.drawing ?? "map" } : {},
13296
+ reviewId: run.workspaceId,
13297
+ startedAt: run.startedAt
13298
+ };
13299
+ }
13300
+ /**
13301
+ * A repository, or one part of it, being mapped. Its registry holds it only while it runs, so every
13302
+ * one is active. It belongs to the repository, and the review it was started from is where it opens.
13303
+ */
13304
+ function activeArchitectureRun(run) {
13305
+ return {
13306
+ key: `architecture:${run.key}`,
13307
+ kind: "diagram",
13308
+ drawing: "architecture",
13309
+ reviewId: run.reviewId,
13310
+ repoPath: run.repoPath,
13311
+ scope: run.scope,
13312
+ ...run.partName ? { scopeName: run.partName } : {},
13313
+ stage: run.stage,
13314
+ startedAt: run.startedAt
13315
+ };
13316
+ }
13153
13317
  function runKindOf(job) {
13154
13318
  if (purposeOf(job) === "update") return "update";
13155
13319
  if (job.reviewType === "kiss") return "kiss";
@@ -13176,7 +13340,7 @@ function operationRunKind(operation) {
13176
13340
  * The history is ordered newest ending first, then by key, a total order, because it is hashed into
13177
13341
  * the page's revision and a tie left to the directory would reload the page for nothing.
13178
13342
  */
13179
- function deriveRuns(jobs, isLive, rows, now) {
13343
+ function deriveRuns(jobs, isLive, rows, now, active = []) {
13180
13344
  const inProgress = deriveOperations(jobs, isLive, rows);
13181
13345
  const recent = [];
13182
13346
  for (const job of jobs) {
@@ -13236,6 +13400,7 @@ function deriveRuns(jobs, isLive, rows, now) {
13236
13400
  recent.sort((a, b) => b.endedAt - a.endedAt || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
13237
13401
  return {
13238
13402
  inProgress,
13403
+ active: sortActive(active),
13239
13404
  recent
13240
13405
  };
13241
13406
  }
@@ -13254,6 +13419,20 @@ function fromAttempt(attempt) {
13254
13419
  ...attempt.agentId ? { agentId: attempt.agentId } : {}
13255
13420
  };
13256
13421
  }
13422
+ /** Newest first, ending on the key, a total order, because the list is hashed into the revision. */
13423
+ function sortActive(active) {
13424
+ return [...active].sort((a, b) => b.startedAt - a.startedAt || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
13425
+ }
13426
+ /**
13427
+ * How many executions are going on now: every operation but the queued, and every registry run.
13428
+ *
13429
+ * The one number the Runs page's Running figure and the sidebar's count beside Runs both show, so the
13430
+ * count a reader follows and the page it opens cannot disagree. Queued work is waiting, not running.
13431
+ * A row and the job it handed over to are one operation already (`deriveOperations`).
13432
+ */
13433
+ function runningCount(operations, active) {
13434
+ return operations.filter((operation) => operation.kind !== "queued").length + active.length;
13435
+ }
13257
13436
  /**
13258
13437
  * The three numbers above the lists. Failed counts every failed attempt the page can list, earlier
13259
13438
  * attempts included and removed ones not: the same population as the history, so the count and the
@@ -13261,7 +13440,7 @@ function fromAttempt(attempt) {
13261
13440
  */
13262
13441
  function runCounts(runs) {
13263
13442
  return {
13264
- running: runs.inProgress.filter((operation) => operation.kind !== "queued").length,
13443
+ running: runningCount(runs.inProgress, runs.active),
13265
13444
  queued: runs.inProgress.filter((operation) => operation.kind === "queued").length,
13266
13445
  failed: runs.recent.filter((entry) => entry.status === "failed").length
13267
13446
  };
@@ -13280,6 +13459,7 @@ function runsRevision(runs, cleanup) {
13280
13459
  if (cleanup && (cleanup.refusals.length > 0 || cleanup.error)) hash.update(`cleanup:${cleanup.at}:${cleanup.error ?? ""}:${cleanup.refusals.join("\0")}\n`);
13281
13460
  const byKey = (a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
13282
13461
  for (const operation of [...runs.inProgress].sort(byKey)) hash.update(`op:${operation.key}:${operation.kind}:${operation.phase ?? ""}:${operation.reviewId ?? ""}\n`);
13462
+ for (const run of [...runs.active].sort(byKey)) hash.update(`act:${run.key}:${run.startedAt}:${run.drawing ?? ""}:${run.stage ?? ""}:${run.reviewId ?? ""}\n`);
13283
13463
  for (const entry of [...runs.recent].sort(byKey)) hash.update(`run:${entry.key}:${entry.status}:${entry.reviewDeleted ? "deleted" : ""}\n`);
13284
13464
  return hash.digest("hex").slice(0, 16);
13285
13465
  }
@@ -13316,7 +13496,23 @@ function liveOperations(now = Date.now(), jobs = allJobs(), rows = pendingBatchR
13316
13496
  * unchanged.
13317
13497
  */
13318
13498
  function liveRuns(now = Date.now(), jobs = allJobs(), rows = pendingBatchRows()) {
13319
- return deriveRuns(jobs, (job) => isClaimed(job.id, now), rows, now);
13499
+ return deriveRuns(jobs, (job) => isClaimed(job.id, now), rows, now, liveActiveRuns());
13500
+ }
13501
+ /**
13502
+ * The work this server's in-memory registries hold now: questions, drawings and mapping runs.
13503
+ *
13504
+ * This process's only, like the queue, and for the same reason: they run as children of the
13505
+ * workspace server and nothing else can start one. Not on disk, so nothing is left once they end.
13506
+ */
13507
+ function liveActiveRuns() {
13508
+ return sortActive([...heldRuns().map(activeAssistantRun).filter((run) => run !== null), ...allArchitectureRuns().map(activeArchitectureRun)]);
13509
+ }
13510
+ /**
13511
+ * How many executions are going on now, as the count beside Runs shows it on every page: the Runs
13512
+ * page's own Running figure, without reading the week of history to get it.
13513
+ */
13514
+ function runningNow(now = Date.now()) {
13515
+ return runningCount(liveOperations(now), liveActiveRuns());
13320
13516
  }
13321
13517
  //#endregion
13322
13518
  //#region ../../packages/review-harness/src/workspace/reviews_revision.ts
@@ -13464,7 +13660,9 @@ var RUN_KIND_LABELS = {
13464
13660
  review: "Initial review",
13465
13661
  update: "Update",
13466
13662
  "re-run": "Re-run",
13467
- kiss: "KISS review"
13663
+ kiss: "KISS review",
13664
+ ask: "Buddy AI",
13665
+ diagram: "Diagram"
13468
13666
  };
13469
13667
  var STATUS_LABELS = {
13470
13668
  completed: "Completed",
@@ -13479,10 +13677,10 @@ var SOURCE_LABELS = {
13479
13677
  };
13480
13678
  function renderRunsPage(view) {
13481
13679
  const now = view.now ?? Date.now();
13482
- const { inProgress, recent } = view.runs;
13680
+ const { inProgress, active, recent } = view.runs;
13483
13681
  const counts = runCounts(view.runs);
13484
13682
  const shown = recent.slice(0, MAX_RUNS_SENT);
13485
- const nothing = inProgress.length === 0 && recent.length === 0;
13683
+ const nothing = inProgress.length === 0 && active.length === 0 && recent.length === 0;
13486
13684
  const notResponding = recent.some((entry) => entry.status === "not-responding");
13487
13685
  return `<!doctype html>
13488
13686
  <html lang="en"${themeAttribute(view.theme)}${sidebarAttributes(view.sidebar)}>
@@ -13494,27 +13692,29 @@ function renderRunsPage(view) {
13494
13692
  <link rel="stylesheet" href="/assets/sidebar.css">
13495
13693
  <link rel="stylesheet" href="/assets/shell.css">
13496
13694
  </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}">
13695
+ <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
13696
  ${renderGlobalBar({
13499
13697
  current: "runs",
13500
13698
  unread: view.unreadNotifications ?? 0,
13699
+ running: view.runningRuns ?? 0,
13501
13700
  driving: view.driving
13502
13701
  })}
13503
- ${renderContextBar("Runs", nothing ? "" : renderControls(notResponding))}
13504
13702
  <div class="prb-shell">
13703
+ ${renderContextBar("Runs", { controls: nothing ? void 0 : renderControls(notResponding) })}
13505
13704
  ${renderWorkspaceRail({
13506
13705
  current: "runs",
13507
13706
  unread: view.unreadNotifications ?? 0,
13707
+ running: view.runningRuns ?? 0,
13508
13708
  sidebar: view.sidebar
13509
13709
  })}
13510
13710
  <main class="prb-index prb-runs">
13511
13711
  <p class="prb-runs-lede">Monitor review executions and recent activity.</p>
13512
13712
  ${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))}
13713
+ ${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
13714
  ${renderRecent(shown, recent.length, view, now, runsSectionOpen("recent", view.sectionsCollapsed))}
13515
13715
  <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
13716
  ${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>
13717
+ <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
13718
  </main>
13519
13719
  </div>
13520
13720
  <script src="/assets/runs.js" type="module"><\/script>
@@ -13576,12 +13776,28 @@ function renderControls(notResponding) {
13576
13776
  * the scope that list has on purpose: work that is running is seen machine-wide, while a request
13577
13777
  * still waiting for a slot is held by the server that accepted it and nowhere else.
13578
13778
  */
13579
- function renderInProgress(operations, reviews, now, open) {
13779
+ /**
13780
+ * Work going on now: review jobs and the rows waiting for them, then what the in-memory registries
13781
+ * hold (questions, drawings, mapping), each entry one execution.
13782
+ *
13783
+ * The two kinds are listed together by when they started, running before queued, so the list reads
13784
+ * as one account of what is happening rather than as two sources the reader has to merge.
13785
+ */
13786
+ function renderInProgress(operations, active, reviews, now, open) {
13787
+ const entries = [...operations.filter((operation) => operation.kind !== "queued").map((operation) => ({
13788
+ at: operation.startedAt ?? 0,
13789
+ html: renderOperationRun(operation, reviewOf(operation, reviews), now)
13790
+ })), ...active.map((run) => ({
13791
+ at: run.startedAt,
13792
+ html: renderActiveRun(run, reviews, now)
13793
+ }))].sort((a, b) => b.at - a.at);
13794
+ const queued = operations.filter((operation) => operation.kind === "queued").map((operation) => renderOperationRun(operation, reviewOf(operation, reviews), now));
13795
+ const count = operations.length + active.length;
13580
13796
  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>" : ""}
13797
+ <summary class="prb-index-section-head">${RUNS_SECTION_NAMES["in-progress"].title} · <span data-section-count>${count}</span></summary>
13798
+ ${count === 0 ? "<p class=\"prb-runs-idle\" data-runs-idle>Nothing is running.</p>" : ""}
13583
13799
  <div class="prb-cards" data-run-list>
13584
- ${operations.map((operation) => renderOperationRun(operation, reviewOf(operation, reviews), now)).join("")}
13800
+ ${entries.map((entry) => entry.html).join("")}${queued.join("")}
13585
13801
  </div>
13586
13802
  <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
13803
  </details>`;
@@ -13675,6 +13891,54 @@ function renderOperationRun(operation, review, now) {
13675
13891
  ${own ? `<a class="prb-card-link" href="${escapeHtml$1(reviewHref(own))}">${body}</a>` : `<div class="prb-card-link">${body}</div>`}
13676
13892
  </article>`;
13677
13893
  }
13894
+ var MAPPING_STEPS = {
13895
+ exploring: "Exploring the code",
13896
+ checking: "Checking the evidence",
13897
+ correcting: "Correcting the map"
13898
+ };
13899
+ /** What a registry run is doing, in the words its own pane uses. */
13900
+ function activeRunLine(run) {
13901
+ if (run.kind === "ask") return "Buddy AI · Answering a question";
13902
+ 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]}` : ""}`;
13903
+ return run.drawing === "implementation" ? "Drawing an implementation diagram" : "Drawing the change map";
13904
+ }
13905
+ /** The view a registry run's work appears in, on its review's page. */
13906
+ var ACTIVE_RUN_VIEWS = {
13907
+ map: "map",
13908
+ implementation: "diagram",
13909
+ architecture: "architecture"
13910
+ };
13911
+ /**
13912
+ * A question, a drawing or a mapping run, while it lasts. A link to the review whose page shows it
13913
+ * and holds its Stop, as every entry in progress is. A mapping run opens the review it was started
13914
+ * from, at the Architecture pane with its part selected (the `part` the pane reads on load). When
13915
+ * that review is no longer here the entry says so and leads nowhere: another review of the same
13916
+ * repository is not the one that asked, and opening it would be opening somebody else's map.
13917
+ */
13918
+ function renderActiveRun(run, reviews, now) {
13919
+ const review = run.reviewId !== void 0 ? reviews.get(run.reviewId) : void 0;
13920
+ const repo = review ? lastSegment(review.repoPath) : run.repoPath ? lastSegment(run.repoPath) : void 0;
13921
+ const where = review ? {
13922
+ repo,
13923
+ pr: review.prNumber,
13924
+ branch: followedRefName(review.branch, review.headResolvedRef) ?? review.branch
13925
+ } : { repo };
13926
+ const title = review?.title || repo || "Review";
13927
+ const gone = review === void 0 && run.reviewId !== void 0;
13928
+ const body = `<div class="prb-card-head"><h3>${escapeHtml$1(title)}</h3></div>
13929
+ ${renderWhere(where)}
13930
+ <p class="prb-card-meta">
13931
+ <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>
13932
+ <span class="prb-run-provenance">${SOURCE_LABELS.workspace}</span>
13933
+ ${since("Started", run.startedAt, now)}
13934
+ </p>
13935
+ ${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>" : ""}`;
13936
+ const part = run.drawing === "architecture" && run.scope !== void 0 && run.scope !== "repository" ? `&part=${encodeURIComponent(run.scope)}` : "";
13937
+ const view = run.kind === "diagram" ? `#${ACTIVE_RUN_VIEWS[run.drawing ?? "map"]}` : "";
13938
+ 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))}">
13939
+ ${review ? `<a class="prb-card-link" href="${escapeHtml$1(reviewHref(review) + part + view)}">${body}</a>` : `<div class="prb-card-link">${body}</div>`}
13940
+ </article>`;
13941
+ }
13678
13942
  /**
13679
13943
  * A run that has ended, or stopped answering.
13680
13944
  *
@@ -16160,6 +16424,7 @@ function indexView() {
16160
16424
  activity,
16161
16425
  revision: reviewsRevision(states, operations),
16162
16426
  unreadNotifications: unreadCount$1(),
16427
+ runningRuns: runningCount(operations, liveActiveRuns()),
16163
16428
  driving: isDriving(),
16164
16429
  theme: readPreferences().theme,
16165
16430
  sidebar: readPreferences().sidebar,
@@ -16253,6 +16518,7 @@ async function handleSettings(req, res, params, indexToken) {
16253
16518
  sendHtml(res, 200, renderSettingsPage({
16254
16519
  settings: readSettings(),
16255
16520
  unreadNotifications: unreadCount$1(),
16521
+ runningRuns: runningNow(),
16256
16522
  driving: isDriving(),
16257
16523
  sidebar: readPreferences().sidebar
16258
16524
  }), indexCookieHeader(indexToken));
@@ -16265,6 +16531,7 @@ function handleHelp(req, res, params, indexToken) {
16265
16531
  if (!indexAuthorised(req, params, indexToken)) return refuseIndex(req, res);
16266
16532
  sendHtml(res, 200, renderHelpPage({
16267
16533
  unreadNotifications: unreadCount$1(),
16534
+ runningRuns: runningNow(),
16268
16535
  driving: isDriving(),
16269
16536
  theme: readPreferences().theme,
16270
16537
  sidebar: readPreferences().sidebar,
@@ -16346,7 +16613,8 @@ function handleRecentNotifications(req, res, params, indexToken) {
16346
16613
  outcome: toneOf(entry)
16347
16614
  })),
16348
16615
  more,
16349
- unread: notifications.filter((entry) => entry.readAt === void 0).length
16616
+ unread: notifications.filter((entry) => entry.readAt === void 0).length,
16617
+ running: runningNow()
16350
16618
  });
16351
16619
  }
16352
16620
  /**
@@ -16361,7 +16629,10 @@ function handleRecentNotifications(req, res, params, indexToken) {
16361
16629
  function handleSeen(req, res, workspace) {
16362
16630
  if (req.method !== "POST") return send(res, 405, { error: "Method not allowed." });
16363
16631
  markReadForReview(workspace.id);
16364
- send(res, 200, { unread: unreadCount$1() });
16632
+ send(res, 200, {
16633
+ unread: unreadCount$1(),
16634
+ running: runningNow()
16635
+ });
16365
16636
  }
16366
16637
  /**
16367
16638
  * What a visible review page polls: how many are unread, and how many of those are about it.
@@ -16374,7 +16645,8 @@ function handleUnread(req, res, workspace) {
16374
16645
  const { here, elsewhere } = unreadAround(workspace.id);
16375
16646
  send(res, 200, {
16376
16647
  unread: here + elsewhere,
16377
- here
16648
+ here,
16649
+ running: runningNow()
16378
16650
  });
16379
16651
  }
16380
16652
  /**
@@ -16401,6 +16673,7 @@ async function handleNotifications(req, res, params, indexToken) {
16401
16673
  notifications,
16402
16674
  gone: new Set(notifications.filter((entry) => !loadWorkspace(entry.reviewId)).map((entry) => entry.id)),
16403
16675
  unreadNotifications: unreadCount$1(),
16676
+ runningRuns: runningNow(),
16404
16677
  driving: isDriving(),
16405
16678
  theme: readPreferences().theme,
16406
16679
  sidebar: readPreferences().sidebar
@@ -16422,6 +16695,7 @@ function handleRuns(req, res, params, indexToken) {
16422
16695
  cleanup,
16423
16696
  revision: runsRevision(runs, cleanup),
16424
16697
  unreadNotifications: unreadCount$1(),
16698
+ runningRuns: runCounts(runs).running,
16425
16699
  driving: isDriving(),
16426
16700
  theme: preferences.theme,
16427
16701
  sidebar: preferences.sidebar,
@@ -16439,8 +16713,9 @@ function handleRunsRevision(req, res, indexToken) {
16439
16713
  const runs = liveRuns();
16440
16714
  send(res, 200, {
16441
16715
  revision: runsRevision(runs, lastSweep()),
16442
- live: runs.inProgress.length > 0,
16443
- unread: unreadCount$1()
16716
+ live: runs.inProgress.length > 0 || runs.active.length > 0,
16717
+ unread: unreadCount$1(),
16718
+ running: runCounts(runs).running
16444
16719
  });
16445
16720
  }
16446
16721
  /** Removing one ended run from the Runs page. See `run_removal.ts` for what that does and does not touch. */
@@ -16492,6 +16767,7 @@ function handleLiveNotifications(req, res, params, indexToken) {
16492
16767
  const wanted = new Set((params.get("ids") ?? "").split(",").filter(Boolean).slice(0, 200));
16493
16768
  send(res, 200, {
16494
16769
  unread: notifications.filter((entry) => entry.readAt === void 0).length,
16770
+ running: runningNow(),
16495
16771
  states: Object.fromEntries(notifications.map((entry) => [entry.id, rowStateOf(entry)])),
16496
16772
  rows: notifications.filter((entry) => wanted.has(entry.id)).map((entry) => renderNotificationRow(entry, !loadWorkspace(entry.reviewId)))
16497
16773
  });
@@ -16730,7 +17006,8 @@ function handleRevision(req, res, indexToken) {
16730
17006
  revision: reviewsRevision(states, operations),
16731
17007
  live: anythingRunning(states, operations),
16732
17008
  driving: isDriving(),
16733
- unread: unreadCount$1()
17009
+ unread: unreadCount$1(),
17010
+ running: runningCount(operations, liveActiveRuns())
16734
17011
  });
16735
17012
  }
16736
17013
  var REVIEW_ID = /^[0-9a-f-]{36}$/;
@@ -17271,6 +17548,7 @@ async function sendWorkspacePage(res, workspace) {
17271
17548
  complete: workspace.completedAt !== void 0,
17272
17549
  pinned: workspace.pinned === true,
17273
17550
  unreadNotifications: unreadAround(workspace.id).elsewhere,
17551
+ runningRuns: runningNow(),
17274
17552
  driving: isDriving(),
17275
17553
  prContext: workspace.prContext ? {
17276
17554
  prContext: workspace.prContext,