tumwater 0.4.0 → 0.4.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/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -17,6 +17,9 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
17
17
|
// ---- views ----
|
|
18
18
|
// view-routing:start
|
|
19
19
|
const VIEWS = { team: "team-view", history: "history", usage: "report", failures: "failures", pending: "pending", settings: "settings-view" }; // vocabulary-ok: view ids
|
|
20
|
+
// The team band's "Open Health check" control navigates to Settings and asks for the
|
|
21
|
+
// #doctorcard scrolled into view once the pane paints (see runAct's view branch).
|
|
22
|
+
let pendingDoctorScroll = false;
|
|
20
23
|
function switchView(v) {
|
|
21
24
|
if (v === "fleet") v = "team"; // vocabulary-ok: legacy dashboard hash, still routed
|
|
22
25
|
if (!VIEWS[v]) v = "team";
|
|
@@ -34,6 +37,13 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
34
37
|
if (v === "failures") fetchFailures();
|
|
35
38
|
if (v === "pending") fetchPending();
|
|
36
39
|
if (v === "settings") { fetchSettings(); fetchDoctor(); fetchRetirable(); fetchBrief(); }
|
|
40
|
+
// The band's "Open Health check" control asks for #doctorcard scrolled into view once the
|
|
41
|
+
// Settings pane paints; buildSettingsView ran synchronously inside fetchSettings above, so
|
|
42
|
+
// the card exists now. Only that control sets the flag, so a normal Settings tab never moves.
|
|
43
|
+
if (pendingDoctorScroll && v === "settings" && $("doctorcard")) {
|
|
44
|
+
pendingDoctorScroll = false;
|
|
45
|
+
$("doctorcard").scrollIntoView({ behavior: "smooth", block: "start" });
|
|
46
|
+
}
|
|
37
47
|
}
|
|
38
48
|
// Tabs are plain #fragment links, so Back/Forward and bookmarks work; re-clicking the open
|
|
39
49
|
// tab refetches its data. #role/<role> opens that role's drawer (over Team on a fresh load),
|
|
@@ -83,12 +93,12 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
83
93
|
// The Health check card's Re-run button: runs the doctor checks again.
|
|
84
94
|
document.addEventListener("click", (ev) => {
|
|
85
95
|
if (clickClosest(ev, "button[data-action='doctor-rerun']")) { ev.preventDefault(); fetchDoctor(); }
|
|
86
|
-
if (clickClosest(ev, "button[data-action='doctor-reprobe']")) { ev.preventDefault(); reprobeDoctorClick(); }
|
|
87
96
|
if (clickClosest(ev, "button[data-action='doctor-kill-orphans']")) { ev.preventDefault(); killOrphansClick(); }
|
|
88
97
|
if (clickClosest(ev, "button[data-action='doctor-drop-broken-pins']")) { ev.preventDefault(); dropBrokenPinsClick(); }
|
|
89
98
|
if (clickClosest(ev, "button[data-action='doctor-commit-backlog']")) { ev.preventDefault(); commitBacklogClick(); }
|
|
90
99
|
if (clickClosest(ev, "button[data-action='doctor-resync-install']")) { ev.preventDefault(); resyncInstallClick(); }
|
|
91
100
|
if (clickClosest(ev, "button[data-action='doctor-retire-leftovers']")) { ev.preventDefault(); retireLeftoversClick(); }
|
|
101
|
+
if (clickClosest(ev, "button[data-action='doctor-archive-backlog']")) { ev.preventDefault(); archiveBacklogClick(); }
|
|
92
102
|
if (clickClosest(ev, "button[data-action='notify-test']")) { ev.preventDefault(); testNotify(); }
|
|
93
103
|
const retire = clickClosest(ev, "button[data-action='retire']");
|
|
94
104
|
if (retire) { ev.preventDefault(); confirmRetire(retire.dataset.role); }
|
|
@@ -103,11 +113,18 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
103
113
|
});
|
|
104
114
|
// ---- global actions: alert and tile buttons, role names anywhere, questions ----
|
|
105
115
|
// run-act:start
|
|
116
|
+
// The backlog drawer rows, the refused-banner card action and the mention action all receive
|
|
117
|
+
// their target as a "file:index" string in a button's data-act arg; one place parses that
|
|
118
|
+
// shape into the file name and the 0-based row number every consumer dereferences, so the
|
|
119
|
+
// Number() coercion and split cannot drift across the seven call sites.
|
|
120
|
+
function entryRef(arg) {
|
|
121
|
+
const [file, index] = arg.split(":");
|
|
122
|
+
return { file, index: Number(index) };
|
|
123
|
+
}
|
|
106
124
|
// The backlog drawer rows resolve a list entry's title from the last polled status by its
|
|
107
|
-
// backlog file and 0-based position (entry-cancel, entry-edit, and the mention action).
|
|
108
|
-
// call sites keep their arg's "file:index" split because that shape is theirs alone.
|
|
125
|
+
// backlog file and 0-based position (entry-cancel, entry-edit, and the mention action).
|
|
109
126
|
function entryTitleAt(file, index) {
|
|
110
|
-
return ((lastStatus && lastStatus[file]) || [])[
|
|
127
|
+
return ((lastStatus && lastStatus[file]) || [])[index];
|
|
111
128
|
}
|
|
112
129
|
// The backlog drawer rows carry their file as "bugs" or "plans"; three other words derive
|
|
113
130
|
// from it — the singular kind the drawer's POST carries, the display filename the toasts name,
|
|
@@ -116,6 +133,20 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
116
133
|
function kindOf(file) { return file === "bugs" ? "bug" : "plan"; }
|
|
117
134
|
function entryFile(file) { return file === "bugs" ? "BUGS.md" : "PLANS.md"; }
|
|
118
135
|
function pickupRole(file) { return file === "bugs" ? "bugfix" : "feature"; }
|
|
136
|
+
// The typed-number guard the interval/instances save arms share: a type=number input
|
|
137
|
+
// reports "" both for a field cleared on purpose (its default) and for text the browser
|
|
138
|
+
// rejected (badInput), and still accepts exponent/hex spellings Number() reads that no one
|
|
139
|
+
// means here — so each arm maps badInput to one message and a disallowed shape to another,
|
|
140
|
+
// then reads the trimmed raw value. Returns undefined after flashing, so the save must
|
|
141
|
+
// stop; the caller turns the empty raw value into its own null (clear-the-override) and
|
|
142
|
+
// Number() otherwise. The team cap save does not use this — parseBudgetField owns that
|
|
143
|
+
// field's own 0-or-more bound and empty→0 policy (see parseBudgetField).
|
|
144
|
+
function typedNumberValue(input, badInputMsg, shapeMsg) {
|
|
145
|
+
if (input.validity && input.validity.badInput) { showFlash("error: " + badInputMsg); return undefined; }
|
|
146
|
+
const raw = input.value.trim();
|
|
147
|
+
if (raw !== "" && !/^\d*\.?\d+$/.test(raw)) { showFlash("error: " + shapeMsg); return undefined; }
|
|
148
|
+
return raw;
|
|
149
|
+
}
|
|
119
150
|
function runAct(act, arg) {
|
|
120
151
|
// Six drawer edits end identically: clear the edit's in-progress flag, reload the role
|
|
121
152
|
// row, and toast the server's message (or a fallback when it sends none). Only the flag to
|
|
@@ -128,7 +159,13 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
128
159
|
};
|
|
129
160
|
if (act === "role") openRole(arg);
|
|
130
161
|
else if (act === "close") closeDrawer();
|
|
131
|
-
else if (act === "view")
|
|
162
|
+
else if (act === "view") {
|
|
163
|
+
// The band's "Open Health check" control routes to the Settings view at its Health check
|
|
164
|
+
// card (arg "settings") and asks switchView to scroll #doctorcard into view after it
|
|
165
|
+
// paints; any other view arg scrolls nowhere.
|
|
166
|
+
if (arg === "settings") pendingDoctorScroll = true;
|
|
167
|
+
location.hash = arg;
|
|
168
|
+
}
|
|
132
169
|
else if (act === "budget") openBudgetEditor();
|
|
133
170
|
else if (act === "resume") setTeamPause(false);
|
|
134
171
|
else if (act === "copy") copyText(arg);
|
|
@@ -146,9 +183,9 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
146
183
|
else if (act === "backlog" && arg) {
|
|
147
184
|
// The refused banner's action names the entry it points at as file:index; open the
|
|
148
185
|
// card on that file's tab and land on the row, the way a .backloglink click does.
|
|
149
|
-
const
|
|
186
|
+
const { file, index } = entryRef(arg);
|
|
150
187
|
setBacklogTab(file);
|
|
151
|
-
toggleEntry(file,
|
|
188
|
+
toggleEntry(file, index);
|
|
152
189
|
}
|
|
153
190
|
$("backlogcard").scrollIntoView({ behavior: "smooth", block: "start" });
|
|
154
191
|
} else if (act === "answer") {
|
|
@@ -167,35 +204,35 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
167
204
|
: "That question was already answered or moved — refreshed";
|
|
168
205
|
});
|
|
169
206
|
} else if (act === "entry-cancel") {
|
|
170
|
-
const
|
|
207
|
+
const { file, index } = entryRef(arg);
|
|
171
208
|
const title = entryTitleAt(file, index);
|
|
172
209
|
if (title === undefined) return;
|
|
173
210
|
if (!entryCancelArmed(arg, title)) { armEntryCancel(arg, title); return; }
|
|
174
211
|
disarmEntryCancel();
|
|
175
|
-
postAction("/api/backlog-cancel", { kind: kindOf(file), position:
|
|
212
|
+
postAction("/api/backlog-cancel", { kind: kindOf(file), position: index + 1, title }, (d) => {
|
|
176
213
|
closeDrawer();
|
|
177
214
|
return d && d.status === "cancelled"
|
|
178
215
|
? "Cancelled “" + splitTitle(title).title + "” from " + entryFile(file) + uncommittedTail(d) + (d.workingNote ? " — " + d.workingNote : "") + (d.releasedNote ? " — " + d.releasedNote : "")
|
|
179
216
|
: "That entry was already removed or moved — refreshed";
|
|
180
217
|
});
|
|
181
218
|
} else if (act === "entry-release") {
|
|
182
|
-
const
|
|
219
|
+
const { file, index } = entryRef(arg);
|
|
183
220
|
const title = entryTitleAt(file, index);
|
|
184
221
|
if (title === undefined) return;
|
|
185
|
-
postAction("/api/backlog-release", { kind: kindOf(file), position:
|
|
222
|
+
postAction("/api/backlog-release", { kind: kindOf(file), position: index + 1, title }, (d) => {
|
|
186
223
|
closeDrawer();
|
|
187
224
|
if (d && d.status === "released") {
|
|
188
|
-
return "Released “" + splitTitle(title).title + "” in " + entryFile(file) + " — lifted
|
|
225
|
+
return "Released “" + splitTitle(title).title + "” in " + entryFile(file) + " — lifted the Refused note — " + (d.heldNote || pickupRole(file) + " will pick it up") + uncommittedTail(d);
|
|
189
226
|
}
|
|
190
227
|
return d && d.status === "not_refused"
|
|
191
|
-
? "That entry carries no Refused
|
|
228
|
+
? "That entry carries no Refused note — refreshed"
|
|
192
229
|
: "That entry was removed or moved — refreshed";
|
|
193
230
|
});
|
|
194
231
|
} else if (act === "entry-top") {
|
|
195
|
-
const
|
|
232
|
+
const { file, index } = entryRef(arg);
|
|
196
233
|
const title = entryTitleAt(file, index);
|
|
197
234
|
if (title === undefined) return;
|
|
198
|
-
postAction("/api/backlog-top", { kind: kindOf(file), position:
|
|
235
|
+
postAction("/api/backlog-top", { kind: kindOf(file), position: index + 1, title }, (d) => {
|
|
199
236
|
closeDrawer();
|
|
200
237
|
if (d && d.status === "moved_to_top") {
|
|
201
238
|
return "Moved “" + splitTitle(title).title + "” to the top of " + entryFile(file) + " — " + (d.heldNote || pickupRole(file) + " takes it next") + uncommittedTail(d);
|
|
@@ -205,14 +242,14 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
205
242
|
: "That entry was removed or moved — refreshed";
|
|
206
243
|
});
|
|
207
244
|
} else if (act === "entry-edit") {
|
|
208
|
-
const
|
|
245
|
+
const { file, index } = entryRef(arg);
|
|
209
246
|
const title = entryTitleAt(file, index);
|
|
210
247
|
if (title !== undefined) startEntryEdit(arg, title);
|
|
211
248
|
} else if (act === "entry-edit-discard") {
|
|
212
249
|
stopEntryEdit();
|
|
213
250
|
refreshEntry();
|
|
214
251
|
} else if (act === "entry-edit-save") {
|
|
215
|
-
const
|
|
252
|
+
const { file, index } = entryRef(arg);
|
|
216
253
|
// The title captured when editing began, not the list's current one: a shifted list must
|
|
217
254
|
// reach the server's title check as a mismatch rather than silently retarget the save.
|
|
218
255
|
const title = entryEditing && entryEditing.key === arg ? entryEditing.title : undefined;
|
|
@@ -221,7 +258,7 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
221
258
|
const newTitle = titleBox ? titleBox.value.trim() : "";
|
|
222
259
|
if (title === undefined) return;
|
|
223
260
|
if (!newTitle) { showFlash("Write a title first"); return; }
|
|
224
|
-
postAction("/api/backlog-edit", { kind: kindOf(file), position:
|
|
261
|
+
postAction("/api/backlog-edit", { kind: kindOf(file), position: index + 1, title, newTitle, newBody: bodyBox ? bodyBox.value : "" }, (d) => {
|
|
225
262
|
stopEntryEdit();
|
|
226
263
|
closeDrawer();
|
|
227
264
|
return d && d.status === "edited"
|
|
@@ -279,23 +316,16 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
279
316
|
} else if (act === "interval-save") {
|
|
280
317
|
const input = $("intervaleditinput");
|
|
281
318
|
if (!input) return;
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// 100, "0x10" → 16), so badInput alone cannot reject them — the plain-decimal shape rule
|
|
293
|
-
// the CLI's --min-run-interval and the TUI Ctrl+S editor apply catches them here too.
|
|
294
|
-
if (raw !== "" && !/^\d*\.?\d+$/.test(raw)) {
|
|
295
|
-
showFlash("error: the interval must be a plain second count like 300, 0.5, or .5 — no +/-, exponent, or hex notation");
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
const value = raw === "" ? null : Number(raw);
|
|
319
|
+
// typedNumberValue holds the badInput and plain-decimal shape guards; undefined means
|
|
320
|
+
// one flashed and the save stops. The empty raw value posts null (the server clears the
|
|
321
|
+
// override); "1e2" and "0x10" are rejected as shape, not silently read as a count.
|
|
322
|
+
const typed = typedNumberValue(
|
|
323
|
+
input,
|
|
324
|
+
"the interval must be a number of 0 or more — leave it empty for the team default",
|
|
325
|
+
"the interval must be a plain second count like 300, 0.5, or .5 — no +/-, exponent, or hex notation",
|
|
326
|
+
);
|
|
327
|
+
if (typed === undefined) return;
|
|
328
|
+
const value = typed === "" ? null : Number(typed);
|
|
299
329
|
postAction("/api/role-interval", { role: arg, minRunIntervalSeconds: value }, settingDone(() => { intervalEditing = null; }, "Min run interval updated"));
|
|
300
330
|
} else if (act === "instances-edit") {
|
|
301
331
|
startInstancesEdit(arg, drawerRole?.view?.instances ?? "");
|
|
@@ -304,27 +334,18 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
304
334
|
} else if (act === "instances-save") {
|
|
305
335
|
const input = $("instanceseditinput");
|
|
306
336
|
if (!input) return;
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// badInput alone cannot reject them — the plain-decimal shape rule the CLI's --instances
|
|
320
|
-
// and the TUI Ctrl+G editor apply catches them here too, so a silent misread is an error
|
|
321
|
-
// instead of silently writing 2. (A rejected "2.5" is a real number the browser accepts,
|
|
322
|
-
// so it posts and validateConfig's 1–8 integer rule 400s it.)
|
|
323
|
-
if (raw !== "" && !/^\d*\.?\d+$/.test(raw)) {
|
|
324
|
-
showFlash("error: the instances must be a plain integer like 2 — no +/-, exponent, or hex notation — leave it empty for the default 1");
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
const value = raw === "" ? null : Number(raw);
|
|
337
|
+
// typedNumberValue holds the badInput and plain-decimal shape guards; undefined means
|
|
338
|
+
// one flashed and the save stops. The empty raw value posts null (the server restores
|
|
339
|
+
// the default 1); "2e0" is rejected as shape, not silently read as 2. (A rejected "2.5"
|
|
340
|
+
// is a real number the browser accepts, so it posts and validateConfig's 1–8 integer
|
|
341
|
+
// rule 400s it, flashed through postAction's error path.)
|
|
342
|
+
const typed = typedNumberValue(
|
|
343
|
+
input,
|
|
344
|
+
"the instances must be an integer from 1 to 8 — leave it empty for the default 1",
|
|
345
|
+
"the instances must be a plain integer like 2 — no +/-, exponent, or hex notation — leave it empty for the default 1",
|
|
346
|
+
);
|
|
347
|
+
if (typed === undefined) return;
|
|
348
|
+
const value = typed === "" ? null : Number(typed);
|
|
328
349
|
postAction("/api/role-instances", { role: arg, instances: value }, settingDone(() => { instancesEditing = null; }, "Instances updated"));
|
|
329
350
|
} else if (act === "instructions-edit") {
|
|
330
351
|
startInstructionsEdit(arg, drawerRole?.view?.instructions ?? "");
|
|
@@ -369,7 +390,7 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written
|
|
|
369
390
|
return d && d.message ? d.message : "Review tier updated";
|
|
370
391
|
});
|
|
371
392
|
} else if (act === "mention") {
|
|
372
|
-
const
|
|
393
|
+
const { file, index } = entryRef(arg);
|
|
373
394
|
const title = entryTitleAt(file, index);
|
|
374
395
|
if (title !== undefined) draftForDirector("About “" + splitTitle(title).title + "”: ");
|
|
375
396
|
}
|
|
@@ -23,6 +23,10 @@ export const GUI_CLIENT_DRAWER_CONFIG_JS = String.raw `
|
|
|
23
23
|
// pi provider/model cases and a budget pair (whose provider already holds the backend) stay
|
|
24
24
|
// byte-identical; only a model-fallback non-pi pair (modelFallbackView drops pi's provider
|
|
25
25
|
// for a non-pi backend) gains the namespace the CLI already shows.
|
|
26
|
+
// Local wall-clock HH:MM for the role-cap forecast's ~cap at stamp — the drawer is browser
|
|
27
|
+
// code and cannot import ../text/datetime.js (the quietWindowEnd precedent's own note), so
|
|
28
|
+
// it routes through the bundle's shared clockHm in FORMAT_JS, the same helper the operator
|
|
29
|
+
// view's window clock uses.
|
|
26
30
|
const fallbackLabel = (fb, fbDefault) => {
|
|
27
31
|
const agentBackend = fb.backend !== undefined && fb.backend !== "pi" ? fb.backend : undefined;
|
|
28
32
|
if (fb.model !== undefined) {
|
|
@@ -32,10 +36,10 @@ export const GUI_CLIENT_DRAWER_CONFIG_JS = String.raw `
|
|
|
32
36
|
if (fb.provider !== undefined) return fb.provider;
|
|
33
37
|
return fbDefault;
|
|
34
38
|
};
|
|
35
|
-
const singleLine = (s) => String(s).replace(/\s+/g, " ").trim();
|
|
36
39
|
// A verbatim value's single-line clamped rendering (80 chars + ellipsis) with the full
|
|
37
40
|
// text riding the title tooltip — the same clamp+tooltip idiom the run summaries use.
|
|
38
|
-
|
|
41
|
+
// The single-line squash rides the shared format-region squash (text.ts's browser twin).
|
|
42
|
+
const clamp = (t) => { const one = squash(t); return one.length > 80 ? one.slice(0, 80) + "…" : one; };
|
|
39
43
|
const rows = [];
|
|
40
44
|
// A role that runs on pi's own saved default carries no provider/model but a piDefault
|
|
41
45
|
// selector (rolePayload's resolvedModelFields leaves both undefined), and the CLI's
|
|
@@ -222,7 +226,8 @@ export const GUI_CLIENT_DRAWER_CONFIG_JS = String.raw `
|
|
|
222
226
|
const editing = capEditing && capEditing.role === role;
|
|
223
227
|
const capBody = dc
|
|
224
228
|
? fmtUsd(Number(dc.spentUsd) || 0) + " of " + fmtUsd(Number(dc.capUsd) || 0) + " spent today (" + (dc.over ? "holding now — no new runs until local midnight" : "not holding") + ")" +
|
|
225
|
-
(Array.isArray(dc.roleIds) && dc.roleIds.length > 1 ? " · shared by " + dc.roleIds.join(", ") : "")
|
|
229
|
+
(Array.isArray(dc.roleIds) && dc.roleIds.length > 1 ? " · shared by " + dc.roleIds.join(", ") : "") +
|
|
230
|
+
(typeof view.dailyCapHitAt === "number" ? " · ~cap at " + clockHm(view.dailyCapHitAt) : "")
|
|
226
231
|
: "no cap";
|
|
227
232
|
const capControl = editing
|
|
228
233
|
? "<input id='capeditinput' type='number' min='0' step='0.01' value='" + (capEditing.value ? String(capEditing.value) : "") + "' placeholder='no cap'>" +
|
|
@@ -173,7 +173,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
173
173
|
: "in " + next;
|
|
174
174
|
const runWord = info.live ? "this run" : "last run";
|
|
175
175
|
const stats = "<dl class='kv'>" + kvRow("Commits", String(l.commits)) + kvRow("Runs", String(l.runs)) +
|
|
176
|
-
kvRow("Tokens, " + runWord, fmtTokens(l.generated)) + kvRow("Peak context, " + runWord, fmtTokens(l.peakCtx)) +
|
|
176
|
+
kvRow("Tokens, " + runWord, fmtTokens(l.generated)) + kvRow("Peak context, " + runWord, (l.peakCtxLabel || fmtTokens(l.peakCtx))) +
|
|
177
177
|
kvRow("Spent today", fmtUsd(l.todayUsd)) + kvRow("Spent in total", fmtUsd(l.costUsd)) +
|
|
178
178
|
kvRow("Last run ended", l.lastRunEndedAt ? fmtAgo(l.lastRunEndedAt) : "never", fmtLastRun(l.lastRunEndedAt)) +
|
|
179
179
|
kvRow("Last merged", l.lastMergedAt ? fmtAgo(l.lastMergedAt) : "never") +
|
|
@@ -290,6 +290,11 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
290
290
|
// order the prompt block and "tumwater role qa" use): each flow's last exercise and result.
|
|
291
291
|
// Ages ride fmtAgo like the other list rows; a flow never exercised reads "never exercised".
|
|
292
292
|
const flowCoverage = Array.isArray(view.flowCoverage) ? view.flowCoverage : null;
|
|
293
|
+
// The feedback observer's per-install filing ledger (rolePayload's filings, the same
|
|
294
|
+
// filingRows order the prompt block and "tumwater role feedback" use): each issue this
|
|
295
|
+
// install filed, newest-first, its number, title, and age. Ages ride fmtAgo like the other
|
|
296
|
+
// list rows.
|
|
297
|
+
const filings = Array.isArray(view.filings) ? view.filings : null;
|
|
293
298
|
const canEdit = r.role !== "director";
|
|
294
299
|
const editing = canEdit && noteEditing && noteEditing.role === r.role;
|
|
295
300
|
const buttons = !canEdit ? "" : "<span style='margin-left:auto'><button type='button' class='btn btn-sm' data-act='note-edit' data-arg='" + esc(r.role) + "'>Edit</button>" +
|
|
@@ -317,6 +322,17 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
317
322
|
: "passed");
|
|
318
323
|
return "<li><span class='when'>" + esc(flow) + "</span><span class='clamp1'>" + esc(text) + "</span></li>";
|
|
319
324
|
}).join("") + "</ul></section>" : "") +
|
|
325
|
+
// The feedback filing ledger (rolePayload's filings), the drawer twin of the CLI's
|
|
326
|
+
// "## Filings" section — each issue this install filed, newest-first, its number, title,
|
|
327
|
+
// and age. No backticks allowed here (this whole script is one big String.raw template).
|
|
328
|
+
(filings && filings.length ? "<section><div class='sec-head'><h3>Filings" +
|
|
329
|
+
"<span class='muted' style='font-size:13px'>· " + filings.length + "</span></h3></div>" +
|
|
330
|
+
"<ul class='run-list'>" + filings.map((f) => {
|
|
331
|
+
const n = typeof f.issue === "number" ? String(f.issue) : "";
|
|
332
|
+
const title = typeof f.title === "string" ? f.title : "";
|
|
333
|
+
return "<li><span class='when'>" + esc(fmtAgo(Number(f.filedAt))) + "</span>" +
|
|
334
|
+
"<span class='clamp1'>#" + esc(n) + " " + esc(title) + "</span></li>";
|
|
335
|
+
}).join("") + "</ul></section>" : "") +
|
|
320
336
|
// The role's recent run outcomes (rolePayload's recentOutcomes, read from the event
|
|
321
337
|
// log), rendered like the Recent runs list above: each with its relative age, result
|
|
322
338
|
// label, summary, and duration. Mirrors the Recent run outcomes section of
|
|
@@ -476,7 +492,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
476
492
|
"<button type='button' class='btn btn-sm btn-primary' data-act='answer-record' data-arg='" + index + "'>" + icon("chat") + "Record answer</button>" +
|
|
477
493
|
"<button type='button' class='btn btn-sm' data-act='answer' data-arg='" + index + "'>Ask the director instead</button>"
|
|
478
494
|
: "<button type='button' class='btn btn-sm' data-act='mention' data-arg='" + esc(file + ":" + index) + "'>" + icon("chat") + "Ask the director about this</button>" +
|
|
479
|
-
(d.refused
|
|
495
|
+
(d.refused ? "<button type='button' class='btn btn-sm btn-primary' data-act='entry-release' data-arg='" + esc(file + ":" + index) + "'>Release this entry</button>" : "") +
|
|
480
496
|
(index > 0 ? "<button type='button' class='btn btn-sm' data-act='entry-top' data-arg='" + esc(file + ":" + index) + "'>Move to top</button>" : "") +
|
|
481
497
|
"<button type='button' class='btn btn-sm' data-act='entry-edit' data-arg='" + esc(file + ":" + index) + "'>Edit this entry</button>" +
|
|
482
498
|
"<button type='button' class='btn btn-sm btn-danger' data-act='entry-cancel' data-arg='" + esc(file + ":" + index) + "'>" + (entryCancelArmed(file + ":" + index) ? "Click again to cancel" : "Cancel this entry") + "</button>";
|
|
@@ -28,17 +28,12 @@ export const GUI_CLIENT_FEED_JS = String.raw `
|
|
|
28
28
|
// Whether the window can step one more length back: the next earlier window's sinceMs must
|
|
29
29
|
// stay within the server's cap.
|
|
30
30
|
const feedCanStepEarlier = (windowMs, step) => windowMs !== 0 && (step + 2) * windowMs <= FEED_MAX_MS;
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
function feedSpan(ms) {
|
|
34
|
-
const d = ms % 86_400_000 === 0;
|
|
35
|
-
const n = d ? ms / 86_400_000 : ms / 3_600_000;
|
|
36
|
-
return n + (d ? " day" : " hour") + (n === 1 ? "" : "s");
|
|
37
|
-
}
|
|
31
|
+
// The window bounds' lengths render through the format region's shared timeSpan ("1 hour"/
|
|
32
|
+
// "N hours", or "1 day"/"N days" for a whole number of days).
|
|
38
33
|
function feedWindowPhrase() {
|
|
39
34
|
const w = Number(feedWindow);
|
|
40
35
|
return feedStep
|
|
41
|
-
?
|
|
36
|
+
? betweenAgo((feedStep + 1) * w, feedStep * w)
|
|
42
37
|
: (FEED_WINDOWS.find((x) => x[0] === feedWindow) || [])[2];
|
|
43
38
|
}
|
|
44
39
|
let feedFilter = recall("feed") === "all" ? "all" : "notable";
|
|
@@ -168,6 +168,12 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Runs to sh
|
|
|
168
168
|
function histDetailKey(role, run) {
|
|
169
169
|
return role + "#" + run;
|
|
170
170
|
}
|
|
171
|
+
// The one ok entry a /api/run answer becomes: text plus the three optional detail fields,
|
|
172
|
+
// each guarded to its own type so a server that omits one still renders a stable card. Both
|
|
173
|
+
// the newest-failure fetch and the per-row fetch build it, so the two cannot drift.
|
|
174
|
+
function histDetailOk(d) {
|
|
175
|
+
return { state: "ok", text: d.text, stat: typeof d.stat === "string" ? d.stat : "", revertSha: typeof d.revertSha === "string" ? d.revertSha : null, hasCommit: d.hasCommit === true };
|
|
176
|
+
}
|
|
171
177
|
// The pinned newest-failure card: the Newest failure toolbar button opens the team's newest
|
|
172
178
|
// failing run (GET /api/run?last=1&result=problems, no role — the role-less last endpoint)
|
|
173
179
|
// as a card above the table, regardless of the current role, window or count, and its ×
|
|
@@ -188,7 +194,7 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Runs to sh
|
|
|
188
194
|
"/api/run answered the newest failing run without a text field — the running tumwater may not match this dashboard",
|
|
189
195
|
);
|
|
190
196
|
const key = histDetailKey(d.role, d.run);
|
|
191
|
-
histDetails.set(key,
|
|
197
|
+
histDetails.set(key, histDetailOk(d));
|
|
192
198
|
histNewest = { state: "ok", key };
|
|
193
199
|
} catch (err) {
|
|
194
200
|
histNewest = { state: "error", message: errMessage(err, "Newest failure unavailable") };
|
|
@@ -449,7 +455,7 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Runs to sh
|
|
|
449
455
|
throw new Error(
|
|
450
456
|
"/api/run answered " + role + " run " + run + " without a text field — the running tumwater may not match this dashboard",
|
|
451
457
|
);
|
|
452
|
-
histDetails.set(key,
|
|
458
|
+
histDetails.set(key, histDetailOk(d));
|
|
453
459
|
} catch (err) {
|
|
454
460
|
histDetails.set(key, { state: "error", message: errMessage(err, "Run detail unavailable") });
|
|
455
461
|
}
|
|
@@ -508,23 +514,17 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Runs to sh
|
|
|
508
514
|
// The server's LOGS_SINCE_MAX_MS (src/events/event-window.ts), the cap /api/history's sinceMs
|
|
509
515
|
// rides; the two must agree (a test pins it).
|
|
510
516
|
const HIST_MAX_MS = 604_800_000;
|
|
511
|
-
//
|
|
512
|
-
function histSpan(ms) {
|
|
513
|
-
const d = ms % 86_400_000 === 0;
|
|
514
|
-
const n = d ? ms / 86_400_000 : ms / 3_600_000;
|
|
515
|
-
return n + (d ? " day" : " hour") + (n === 1 ? "" : "s");
|
|
516
|
-
}
|
|
517
|
+
// The window bounds' lengths render through the format region's shared timeSpan.
|
|
517
518
|
// Whether the window can step one more length back: the next earlier window's sinceMs must
|
|
518
519
|
// stay under the server's cap.
|
|
519
520
|
function histCanStepEarlier(sinceKey, step) {
|
|
520
521
|
const w = HIST_WINDOWS[sinceKey];
|
|
521
522
|
return Boolean(w) && (step + 2) * w[0] <= HIST_MAX_MS;
|
|
522
523
|
}
|
|
523
|
-
// The window in words: the trailing phrase at step 0, else the bounds in cmdLogs' wording.
|
|
524
524
|
function histWindowPhrase(sinceKey, step) {
|
|
525
525
|
const w = HIST_WINDOWS[sinceKey];
|
|
526
526
|
if (!w) return "";
|
|
527
|
-
return step ?
|
|
527
|
+
return step ? betweenAgo((step + 1) * w[0], step * w[0]) : w[1];
|
|
528
528
|
}
|
|
529
529
|
// The note above a stepped window's table; empty at step 0 or for an unknown key.
|
|
530
530
|
function historyStepHtml(sinceKey, step) {
|
|
@@ -534,7 +534,7 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Runs to sh
|
|
|
534
534
|
function histWindowOptions() {
|
|
535
535
|
return Object.keys(HIST_WINDOWS).map((k) => {
|
|
536
536
|
const label = HIST_WINDOWS[k][1];
|
|
537
|
-
return "<option value='" + k + "'>" +
|
|
537
|
+
return "<option value='" + k + "'>" + capitalize(label) + "</option>";
|
|
538
538
|
}).join("");
|
|
539
539
|
}
|
|
540
540
|
// The server's sparse-window note above the table; empty for no note.
|
|
@@ -586,7 +586,7 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Runs to sh
|
|
|
586
586
|
"<td title='" + esc(r.time) + "'><span class='clamp1'>" + esc(fmtAgo(r.ts)) + "</span></td>" +
|
|
587
587
|
"<td class='mono'>" + esc(roleLabel(r.role)) + "</td>" +
|
|
588
588
|
"<td class='c-run num muted'>#" + esc(r.run) + "</td>" +
|
|
589
|
-
"<td><span class='res t-" + res.tone + "'>" + esc(res.label) + "</span
|
|
589
|
+
"<td><span class='res t-" + res.tone + "'>" + esc(res.label) + "</span>" + (r.cutOff === true ? " <span class='muted' title='cut off at the context ceiling'>cut off</span>" : "") + "</td>" +
|
|
590
590
|
"<td class='c-model'><span class='clamp1' title='" + esc(r.model || "") + "'>" + (r.model ? esc(r.model) : "<span class='muted'>—</span>") + "</span></td>" +
|
|
591
591
|
"<td class='c-detail'><span class='clamp2' title='" + esc(r.detail) + "'>" + (r.detail ? esc(r.detail) : "<span class='muted'>—</span>") + "</span></td>" +
|
|
592
592
|
"<td class='num c-dur'>" + durCell(r.durationMs) + "</td>" +
|
|
@@ -598,8 +598,8 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Runs to sh
|
|
|
598
598
|
// history-table:end
|
|
599
599
|
// history-summary:start
|
|
600
600
|
// The Summary table: one row per role plus the total, in the same column set the CLI's
|
|
601
|
-
// history --summary prints (runs, changes, none, refused, problems, tokens,
|
|
602
|
-
// last change), so the dashboard and the CLI cannot drift column for column. Every number
|
|
601
|
+
// history --summary prints (runs, changes, none, refused, problems, tokens, peak ctx, cost,
|
|
602
|
+
// time, last change), so the dashboard and the CLI cannot drift column for column. Every number
|
|
603
603
|
// comes from the
|
|
604
604
|
// server's summary (built on the shared summarizeRunRows) — nothing here re-derives a
|
|
605
605
|
// tally — and the cost/token cells reuse the page's report formatters. The time cell is the
|
|
@@ -612,10 +612,10 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Runs to sh
|
|
|
612
612
|
"<tr" + (e.total ? " class='hist-total'" : "") + ">" +
|
|
613
613
|
"<td class='mono'>" + (e.labelHtml || esc(e.label)) + "</td>" +
|
|
614
614
|
cell(r, "runs") + cell(r, "changes") + cell(r, "none") + cell(r, "refused") + cell(r, "problems") +
|
|
615
|
-
cell(r, "tokens", (n) => fmtTokens(Number(n))) + cell(r, "costUsd", (n) => fmtUsd(Number(n))) +
|
|
615
|
+
cell(r, "tokens", (n) => fmtTokens(Number(n))) + cell(r, "peakContextTokens", (n) => (n === undefined || n === null ? "—" : fmtTokens(Number(n)))) + cell(r, "costUsd", (n) => fmtUsd(Number(n))) +
|
|
616
616
|
"<td class='num'>" + durCell(r.durationMs) + "</td>" +
|
|
617
617
|
"<td>" + esc(r.lastChangeTs === null || r.lastChangeTs === undefined ? "—" : fmtAgo(Number(r.lastChangeTs))) + "</td></tr>"); }).join("");
|
|
618
|
-
return "<div class='table-wrap'><table class='table history'><thead><tr><th>" + esc(header) + "</th><th class='num'>Runs</th><th class='num'>Merged</th><th class='num'>None</th><th class='num'>Refused</th><th class='num'>Problems</th><th class='num'>Tokens</th><th class='num'>Cost</th><th class='num'>Time</th><th>Last change</th></tr></thead><tbody>" +
|
|
618
|
+
return "<div class='table-wrap'><table class='table history'><thead><tr><th>" + esc(header) + "</th><th class='num'>Runs</th><th class='num'>Merged</th><th class='num'>None</th><th class='num'>Refused</th><th class='num'>Problems</th><th class='num'>Tokens</th><th class='num'>Peak ctx</th><th class='num'>Cost</th><th class='num'>Time</th><th>Last change</th></tr></thead><tbody>" +
|
|
619
619
|
rowsHtml + "</tbody></table></div>";
|
|
620
620
|
}
|
|
621
621
|
function histSummaryHtml(summary) {
|
|
@@ -17,11 +17,11 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
17
17
|
// How a role's phase label (status-model.ts rolePhase) reads here: a status word and tone for
|
|
18
18
|
// its pill, whether it is live work, and the detail the label carried after its first word.
|
|
19
19
|
function phaseInfo(phase) {
|
|
20
|
-
// status-model.ts rolePhase appends a " · no change ×N"
|
|
21
|
-
// cell's label. Strip it so the exact/startsWith matches and the parsed
|
|
22
|
-
// plain phase; phaseDetailBase re-appends the streak once itself from the
|
|
23
|
-
// noChangeStreak, so the GUI renders it exactly once.
|
|
24
|
-
const p = String(phase || "").replace(/ · no change ×\d+$/, "");
|
|
20
|
+
// status-model.ts rolePhase appends a " · no change ×N" or " · cut off ×N" streak suffix
|
|
21
|
+
// to a resting idle cell's label. Strip it so the exact/startsWith matches and the parsed
|
|
22
|
+
// detail see the plain phase; phaseDetailBase re-appends the streak once itself from the
|
|
23
|
+
// payload's noChangeStreak/cutOffStreak, so the GUI renders it exactly once.
|
|
24
|
+
const p = String(phase || "").replace(/ · (?:no change|cut off) ×\d+$/, "");
|
|
25
25
|
const after = (n) => p.slice(n).replace(/^[\s·,]+/, "");
|
|
26
26
|
if (p.startsWith("director working")) return { key: "working", label: "Working (director)", tone: "blue", live: true, detail: after(16) };
|
|
27
27
|
if (p.startsWith("working")) return { key: "working", label: "Working", tone: "blue", live: true, detail: after(7) };
|
|
@@ -42,7 +42,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
if (p === "failing") return { key: "failing", label: "Failing", tone: "red", live: false, detail: "failed runs in a row" };
|
|
45
|
-
if (p === "main's check is failing") return { key: "mainred", label: "Main's check failing", tone: "red", live: false, detail: "
|
|
45
|
+
if (p === "main's check is failing") return { key: "mainred", label: "Main's check failing", tone: "red", live: false, detail: "change pinned until main goes green" };
|
|
46
46
|
if (p.startsWith("paused")) return { key: "paused", label: "Paused", tone: "amber", live: false, detail: "starts no new runs" };
|
|
47
47
|
if (p === "waiting: budget spent") return { key: "budget", label: "Waiting", tone: "amber", live: false, detail: "budget" };
|
|
48
48
|
if (p === "waiting: daily cap spent") return { key: "cap", label: "Waiting", tone: "amber", live: false, detail: "daily cap" };
|
|
@@ -60,7 +60,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
60
60
|
}
|
|
61
61
|
// A stalled tool call or a long silence, as the phase label names them (run-progress-model.ts's
|
|
62
62
|
// inFlightDetail) — the cue that a working role may be stuck. The pattern is that module's
|
|
63
|
-
// STALL_SOURCE (interpolated at module build), shared with team-
|
|
63
|
+
// STALL_SOURCE (interpolated at module build), shared with team-alert-status.ts's server-side
|
|
64
64
|
// matcher so the two cannot drift from the wording the cell renders.
|
|
65
65
|
const STALL = new RegExp(${JSON.stringify(STALL_SOURCE)});
|
|
66
66
|
// A run result (run-outcome.ts RunResult) in words, with the tone of its outcome.
|
|
@@ -82,7 +82,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
82
82
|
};
|
|
83
83
|
function resultInfo(result) {
|
|
84
84
|
const words = result ? eventTypeLabel(result) : "—";
|
|
85
|
-
return { label:
|
|
85
|
+
return { label: capitalize(words), tone: hasOwn(RESULT_TONES, result) ? RESULT_TONES[result] : "gray" };
|
|
86
86
|
}
|
|
87
87
|
// The problem results and the named event sets are tone.ts's lists, interpolated at module
|
|
88
88
|
// build (like STALL above) so the page cannot drift from the server's eventKind.
|
|
@@ -156,7 +156,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
156
156
|
}
|
|
157
157
|
// A backlog title's trailing "(planned 2026-09-29)"-style note, split off as metadata —
|
|
158
158
|
// the pattern is backlog-structure.ts's ENTRY_STAMP_META_SOURCE (interpolated at module
|
|
159
|
-
// load, compiled once here), the same regex team-
|
|
159
|
+
// load, compiled once here), the same regex team-alert-shared.ts strips through via
|
|
160
160
|
// stripEntryStamp, so a new stamp verb reaches both dashboards from one edit.
|
|
161
161
|
const ENTRY_STAMP_RE = new RegExp(${JSON.stringify(ENTRY_STAMP_META_SOURCE)}, "i");
|
|
162
162
|
function splitTitle(t) {
|
|
@@ -79,10 +79,6 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
79
79
|
if (status === "rejected") return "limited";
|
|
80
80
|
return status === "reset" ? "reset" : "ok";
|
|
81
81
|
}
|
|
82
|
-
function windowClock(ms) {
|
|
83
|
-
const d = new Date(ms);
|
|
84
|
-
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
85
|
-
}
|
|
86
82
|
function windowResets(ms) {
|
|
87
83
|
const minutes = Math.floor((ms - Date.now()) / 60000);
|
|
88
84
|
if (minutes < 1) return "resets now";
|
|
@@ -105,7 +101,7 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
105
101
|
return ["small", "default", "strong"].filter((tier) => chain && chain[tier]).map((tier) => {
|
|
106
102
|
const t = chain[tier];
|
|
107
103
|
const money = t.spentUsd !== undefined && t.capUsd !== undefined ? " " + fmtUsd(t.spentUsd) + "/" + fmtCap(t.capUsd) : "";
|
|
108
|
-
const until = t.returnsAt !== undefined ? " until " +
|
|
104
|
+
const until = t.returnsAt !== undefined ? " until " + clockHm(t.returnsAt) : "";
|
|
109
105
|
return "<span class='side-card-value window-line'>" + esc(tier + " → " + t.pair + money + until) + "</span>";
|
|
110
106
|
}).join("");
|
|
111
107
|
}
|
|
@@ -114,7 +110,7 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
114
110
|
const word = windowWord(w.status);
|
|
115
111
|
parts.push(typeof w.utilization === "number" ? windowPct(w) + "%" : word === "ok" ? "ok · usage not reported" : word);
|
|
116
112
|
if (w.isUsingOverage) parts.push("overage");
|
|
117
|
-
if (w.status === "rejected" && block.heldUntil != null) parts.push((chainMoved(chain) ? "on fallback until " : block.label + " model jobs wait until ") +
|
|
113
|
+
if (w.status === "rejected" && block.heldUntil != null) parts.push((chainMoved(chain) ? "on fallback until " : block.label + " model jobs wait until ") + clockHm(block.heldUntil));
|
|
118
114
|
else if (w.status !== "reset" && typeof w.resetsAt === "number") parts.push(windowResets(w.resetsAt));
|
|
119
115
|
return parts.join(" · ");
|
|
120
116
|
}
|
|
@@ -156,8 +152,8 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
156
152
|
// actually exists; an all-free subscription-only team (only and free, no windows) falls
|
|
157
153
|
// through to the "Free models" spend card below so it never renders an empty window card.
|
|
158
154
|
if (only && blocks && blocks.length > 0) {
|
|
159
|
-
return "<div class='side-card' title='
|
|
160
|
-
"<span class='side-card-label'>" + icon("clock") + "
|
|
155
|
+
return "<div class='side-card' title='Subscription usage windows — a rejected window pauses model jobs on that backend until it resets'>" +
|
|
156
|
+
"<span class='side-card-label'>" + icon("clock") + "subscription window</span>" + windowLinesHtml(blocks, only, chain) + "</div>";
|
|
161
157
|
}
|
|
162
158
|
if (b.free) {
|
|
163
159
|
return "<div class='side-card' title='Every model the team can use is cost n/a, so there is nothing to cap'>" +
|
|
@@ -170,7 +166,7 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
170
166
|
esc(capped ? "Today's spend against the daily cap — click to change the cap" : "No daily cap — click to set one") + "'>" +
|
|
171
167
|
"<span class='side-card-label'>" + icon("dollar") + "Spent today<span class='edit'>" + (capped ? "Edit cap" : "Set a cap") + "</span></span>" +
|
|
172
168
|
"<span class='side-card-value'>" + esc(fmtUsd(b.spentUsd)) + " <small>" + esc(capped ? "of " + fmtCap(b.capUsd) : "no daily cap") + "</small></span>" +
|
|
173
|
-
(capped ? meterBar(tone, pct.toFixed(1)) : "") + (b.capHitAt != null ? "<span class='side-card-value window-line'>~cap at " + esc(
|
|
169
|
+
(capped ? meterBar(tone, pct.toFixed(1)) : "") + (b.capHitAt != null ? "<span class='side-card-value window-line'>~cap at " + esc(clockHm(b.capHitAt)) + "</span>" : "") + windowLinesHtml(blocks, only, chain) + "</button>";
|
|
174
170
|
}
|
|
175
171
|
function renderBudgetBadge(d) {
|
|
176
172
|
if (budgetEditing) return; // the open editor owns the slot until save or cancel
|
|
@@ -279,7 +275,7 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
279
275
|
const PAUSE_CHOICES = [[1800, "For 30 minutes"], [3600, "For 1 hour"], [7200, "For 2 hours"], [28800, "For 8 hours"], [0, "Until I resume it"]];
|
|
280
276
|
// How long a timed team pause has left (humanSeconds), or "" for a standing one.
|
|
281
277
|
function pauseLeft(d) {
|
|
282
|
-
return d.pausedUntil ? humanSeconds(
|
|
278
|
+
return d.pausedUntil ? humanSeconds(secsUntil(d.pausedUntil, Date.now())) : "";
|
|
283
279
|
}
|
|
284
280
|
// The sidebar's pause control: Resume (with a timed pause's countdown) while the team is
|
|
285
281
|
// paused, otherwise the button that opens the menu of timed pauses.
|