tumwater 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The dashboard's view model, browser-side — the pure layer between the status payload and
|
|
2
3
|
* what the page shows, with no DOM of its own: each loop's phase pill (label, tone, live
|
|
3
4
|
* detail), its last result in words with the tone of its outcome, the loop sort order and
|
|
@@ -17,53 +18,71 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
17
18
|
// How a loop's phase label (status-model.ts loopPhase) reads here: a status word and tone for
|
|
18
19
|
// its pill, whether it is live work, and the detail the label carried after its first word.
|
|
19
20
|
function phaseInfo(phase) {
|
|
20
|
-
|
|
21
|
+
// status-model.ts loopPhase appends a " · no change ×N" streak suffix to a resting idle
|
|
22
|
+
// cell's label. Strip it so the exact/startsWith matches and the parsed detail see the
|
|
23
|
+
// plain phase; phaseDetailBase re-appends the streak once itself from the payload's
|
|
24
|
+
// noChangeStreak, so the GUI renders it exactly once.
|
|
25
|
+
const p = String(phase || "").replace(/ · no change ×\d+$/, "");
|
|
21
26
|
const after = (n) => p.slice(n).replace(/^[\s·,]+/, "");
|
|
22
27
|
if (p.startsWith("director working")) return { key: "working", label: "Working (director)", tone: "blue", live: true, detail: after(16) };
|
|
23
28
|
if (p.startsWith("working")) return { key: "working", label: "Working", tone: "blue", live: true, detail: after(7) };
|
|
24
29
|
if (p.startsWith("reviewing")) return { key: "reviewing", label: "Reviewing", tone: "violet", live: true, detail: after(9) };
|
|
25
|
-
if (p.startsWith("
|
|
26
|
-
if (p.startsWith("
|
|
27
|
-
if (p.startsWith("
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (p
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
if (p.startsWith("merging")) return { key: "landing", label: "Merging", tone: "orange", live: true, detail: after(7) }; // vocabulary-ok: internal key the row classes and filters match
|
|
31
|
+
if (p.startsWith("approved")) return { key: "vetted", label: "Approved", tone: "indigo", live: false, detail: "waiting to merge" }; // vocabulary-ok: internal key the row classes and filters match
|
|
32
|
+
if (p.startsWith("waiting to start")) return { key: "awaiting", label: "Waiting to start", tone: "gray", live: false, detail: after(16) ? "for " + after(16) : "" };
|
|
33
|
+
// A waiter parked on a full provider key (provider-concurrency part 5/5): the phase reads
|
|
34
|
+
// waiting for <key> 2m, and this chip names the provider alongside the wait, so the
|
|
35
|
+
// operator sees which cap is full — the same chip and tone as a team-total waiter.
|
|
36
|
+
if (p.startsWith("waiting for ")) {
|
|
37
|
+
const m = after(12).match(/^(\S+)(?:\s+(.*))?$/);
|
|
38
|
+
const prov = m ? m[1] : "";
|
|
39
|
+
const dur = m && m[2] ? m[2].trim() : "";
|
|
40
|
+
return {
|
|
41
|
+
key: "awaiting", label: "Waiting to start", tone: "gray", live: false,
|
|
42
|
+
detail: (prov ? "on " + prov : "") + (dur ? " for " + dur : ""),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (p === "failing") return { key: "failing", label: "Failing", tone: "red", live: false, detail: "failed runs in a row" };
|
|
46
|
+
if (p === "main's check is failing") return { key: "mainred", label: "Main's check failing", tone: "red", live: false, detail: "blocked until main's check passes" };
|
|
47
|
+
if (p.startsWith("paused")) return { key: "paused", label: "Paused", tone: "amber", live: false, detail: "starts no new runs" };
|
|
48
|
+
if (p === "waiting: budget spent") return { key: "budget", label: "Waiting", tone: "amber", live: false, detail: "budget" };
|
|
49
|
+
if (p === "waiting: daily cap spent") return { key: "cap", label: "Waiting", tone: "amber", live: false, detail: "daily cap" };
|
|
50
|
+
if (p === "waiting: low disk") return { key: "disk", label: "Waiting", tone: "amber", live: false, detail: "low disk" };
|
|
51
|
+
if (p === "waiting: bootstrap") return { key: "bootstrap", label: "Waiting", tone: "amber", live: false, detail: "bootstrap" };
|
|
52
|
+
if (p.startsWith("waiting: maintenance allowance used")) return { key: "quota", label: "Waiting", tone: "amber", live: false, detail: "maintenance " + after(35) };
|
|
36
53
|
if (p.startsWith("sleeping")) return { key: "sleeping", label: "Sleeping", tone: "gray", live: false, detail: "" };
|
|
37
|
-
if (p
|
|
38
|
-
if (p
|
|
39
|
-
if (p === "
|
|
54
|
+
if (p.startsWith("idle: ")) return { key: "idle", label: "Idle", tone: "gray", live: false, detail: p.slice(6) };
|
|
55
|
+
if (p.startsWith("deferred (")) return { key: "deferred", label: "Deferred", tone: "gray", live: false, detail: p.slice(10, -1) };
|
|
56
|
+
if (p === "due") return { key: "queued", label: "Due", tone: "gray", live: false, detail: "starts on the next poll" };
|
|
57
|
+
if (p === "ready for messages") return { key: "waiting", label: "Ready", tone: "gray", live: false, detail: "runs when you send it a message" };
|
|
58
|
+
if (p === "stopped") return { key: "stopped", label: "Stopped", tone: "gray", live: false, detail: "the team is not running" };
|
|
40
59
|
return { key: "other", label: p || "unknown", tone: "gray", live: false, detail: "" };
|
|
41
60
|
}
|
|
42
61
|
// A stalled tool call or a long silence, as the phase label names them (tick-progress-model.ts's
|
|
43
62
|
// inFlightDetail) — the cue that a working loop may be stuck. The pattern is that module's
|
|
44
|
-
// STALL_SOURCE (interpolated at module build), shared with
|
|
63
|
+
// STALL_SOURCE (interpolated at module build), shared with team-alerts.ts's server-side
|
|
45
64
|
// matcher so the two cannot drift from the wording the cell renders.
|
|
46
65
|
const STALL = new RegExp(${JSON.stringify(STALL_SOURCE)});
|
|
47
66
|
// A tick result (tick-outcome.ts TickResult) in words, with the tone of its outcome.
|
|
48
|
-
const
|
|
49
|
-
changed:
|
|
50
|
-
queued:
|
|
51
|
-
no_change:
|
|
52
|
-
skipped:
|
|
53
|
-
refused:
|
|
54
|
-
rejected:
|
|
55
|
-
review_error:
|
|
56
|
-
merge_conflict:
|
|
57
|
-
merge_blocked:
|
|
58
|
-
error:
|
|
59
|
-
aborted:
|
|
60
|
-
quiet_killed:
|
|
61
|
-
user_aborted:
|
|
62
|
-
main_red:
|
|
67
|
+
const RESULT_TONES = {
|
|
68
|
+
changed: "green",
|
|
69
|
+
queued: "indigo",
|
|
70
|
+
no_change: "gray",
|
|
71
|
+
skipped: "gray",
|
|
72
|
+
refused: "amber",
|
|
73
|
+
rejected: "amber",
|
|
74
|
+
review_error: "red",
|
|
75
|
+
merge_conflict: "red",
|
|
76
|
+
merge_blocked: "red",
|
|
77
|
+
error: "red",
|
|
78
|
+
aborted: "amber",
|
|
79
|
+
quiet_killed: "amber",
|
|
80
|
+
user_aborted: "gray",
|
|
81
|
+
main_red: "red",
|
|
63
82
|
};
|
|
64
83
|
function resultInfo(result) {
|
|
65
|
-
const
|
|
66
|
-
return
|
|
84
|
+
const words = result ? eventTypeLabel(result) : "—";
|
|
85
|
+
return { label: words.charAt(0).toUpperCase() + words.slice(1), tone: hasOwn(RESULT_TONES, result) ? RESULT_TONES[result] : "gray" };
|
|
67
86
|
}
|
|
68
87
|
// The problem results and the named event sets are tone.ts's lists, interpolated at module
|
|
69
88
|
// build (like STALL above) so the page cannot drift from the server's eventKind.
|
|
@@ -75,7 +94,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
75
94
|
// loop-sort:start
|
|
76
95
|
// Loop order — status-model.ts's loopRank/sortLoopsByState, the TUI's order too: the running
|
|
77
96
|
// director first (its own live work, outside the maxConcurrent cap it bypasses — BUGS.md
|
|
78
|
-
// 2026-10-06), then live work (working, reviewing,
|
|
97
|
+
// 2026-10-06), then live work (working, reviewing, merge), then work waiting in the pipe
|
|
79
98
|
// line (approved, awaiting a slot), then loops that need attention (failing, main red), then
|
|
80
99
|
// paused, then idle; within a rank the most recent tick first, a never-ticked loop last, ties
|
|
81
100
|
// by name. The phase → rank rules are status-model.ts's LOOP_RANK_RULES (interpolated at
|
|
@@ -95,19 +114,19 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
95
114
|
const ra = loopRank(a.phase);
|
|
96
115
|
const rb = loopRank(b.phase);
|
|
97
116
|
if (ra !== rb) return ra - rb;
|
|
98
|
-
const ta = a.
|
|
99
|
-
const tb = b.
|
|
117
|
+
const ta = a.lastRunEndedAt ?? 0;
|
|
118
|
+
const tb = b.lastRunEndedAt ?? 0;
|
|
100
119
|
if (ta !== tb) return tb - ta;
|
|
101
120
|
return a.role.localeCompare(b.role);
|
|
102
121
|
});
|
|
103
122
|
}
|
|
104
123
|
// loop-sort:end
|
|
105
|
-
// The
|
|
124
|
+
// The team table's section for each rank. The running director has its own section so it
|
|
106
125
|
// does not join the In-progress rows an operator scans against maxConcurrent; the permit
|
|
107
|
-
// holders (rank 0) and the pipeline waiters (rank 1, labeled "Waiting
|
|
126
|
+
// holders (rank 0) and the pipeline waiters (rank 1, labeled "Waiting to start") keep
|
|
108
127
|
// sharing In progress (BUGS.md 2026-10-06).
|
|
109
128
|
const LOOP_GROUPS = [["Director", -1], ["In progress", 0, 1], ["Needs attention", 2], ["Paused", 3], ["Idle", 4]];
|
|
110
|
-
// The payload's alerts (
|
|
129
|
+
// The payload's alerts (team-alerts.ts teamAlerts — the TUI's attention lines use the same
|
|
111
130
|
// list), plus the one only the page can know: that its server stopped answering.
|
|
112
131
|
const OFFLINE_ALERT = { key: "offline", tone: "red", title: "Lost contact with the dashboard server",
|
|
113
132
|
detail: "Retrying every second. Everything below is the last state it reported.", actions: [] };
|
|
@@ -125,7 +144,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
125
144
|
const needsYouKeys = (alerts) => alerts.filter(NEEDS_YOU).map((a) => a.key);
|
|
126
145
|
// The needs-you keys of the given alerts that prevKeys (the previous poll's set, or null on
|
|
127
146
|
// the first poll) lacks — exactly the alerts a new cue should announce. A null prev yields
|
|
128
|
-
// every needs-you key, so a page opened onto an already-alerting
|
|
147
|
+
// every needs-you key, so a page opened onto an already-alerting team cues once.
|
|
129
148
|
const newNeedsYouKeys = (prevKeys, alerts) => {
|
|
130
149
|
const prior = new Set(prevKeys || []);
|
|
131
150
|
return needsYouKeys(alerts).filter((k) => !prior.has(k));
|
|
@@ -135,9 +154,10 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
135
154
|
const ROUTINE_EVENTS = ${JSON.stringify(ROUTINE_EVENTS)};
|
|
136
155
|
const PROBLEM_EVENTS = ${JSON.stringify(PROBLEM_EVENTS)};
|
|
137
156
|
function eventKind(item) {
|
|
138
|
-
if (item.type === "merged") return "landing";
|
|
157
|
+
if (item.type === "merged") return "landing"; // vocabulary-ok: internal key the row classes and filters match
|
|
139
158
|
if (item.type === "question_posted") return "attention";
|
|
140
|
-
if (item.type === "
|
|
159
|
+
if (item.type === "entry_refused") return "attention";
|
|
160
|
+
if (item.type === "run_end") return PROBLEM_RESULTS.includes(item.result) ? "problem" : "routine";
|
|
141
161
|
if (item.type === "build_check") return ROUTINE_BUILD_RESULTS.includes(item.result) ? "routine" : "problem";
|
|
142
162
|
if (PROBLEM_EVENTS.includes(item.type)) return "problem";
|
|
143
163
|
if (ROUTINE_EVENTS.includes(item.type)) return "routine";
|
|
@@ -145,7 +165,7 @@ export const GUI_CLIENT_MODEL_JS = String.raw ` // view-model:start
|
|
|
145
165
|
}
|
|
146
166
|
// A backlog title's trailing "(planned 2026-09-29)"-style note, split off as metadata —
|
|
147
167
|
// the pattern is backlog-structure.ts's ENTRY_STAMP_META_SOURCE (interpolated at module
|
|
148
|
-
// load, compiled once here), the same regex
|
|
168
|
+
// load, compiled once here), the same regex team-alerts.ts strips through via
|
|
149
169
|
// stripEntryStamp, so a new stamp verb reaches both dashboards from one edit.
|
|
150
170
|
const ENTRY_STAMP_RE = new RegExp(${JSON.stringify(ENTRY_STAMP_META_SOURCE)}, "i");
|
|
151
171
|
function splitTitle(t) {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import { PAUSE_REASON_MAX } from "../../team/team-state.js";
|
|
3
|
+
/** The dashboard's team-wide operator controls, browser-side, all living in the sidebar so
|
|
3
4
|
* they are in reach from every view: the daily spend badge — today's spend against the cap
|
|
4
5
|
* with a meter, a button that opens a small editor for the cap (POST /api/budget, the same
|
|
5
6
|
* setter as the TUI's Ctrl+B) — the pause control, a menu of timed pauses with an
|
|
6
|
-
* optional reason (POST /api/pause with `forSeconds` and `reason`, the CLI's
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* optional reason (POST /api/pause with `forSeconds` and `reason`, the CLI's
|
|
8
|
+
* `pause --for`/`--reason`) that turns into a Resume button with the countdown while the
|
|
9
|
+
* team is paused, and the sound toggle that mutes the alert cue (a per-browser preference,
|
|
10
|
+
* no server call). Neither keeps optimistic state: after a successful
|
|
9
11
|
* POST the next status poll re-renders them from the server's answer, and a failed one toasts
|
|
10
12
|
* the error and leaves the editor open. Spliced into gui-client.ts's script, where it reaches
|
|
11
13
|
* the other sections' shared plumbing through that concatenation: the DOM and HTML helpers
|
|
@@ -55,9 +57,17 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
55
57
|
const pct = (b.spentUsd / b.capUsd) * 100;
|
|
56
58
|
return pct >= 100 ? " t-red" : pct >= 85 ? " t-amber" : "";
|
|
57
59
|
}
|
|
60
|
+
// The bar under a spend/window readout: a span class='meter{tone}' wrapping a fill whose
|
|
61
|
+
// width is the spent share. The one home of the markup both the claude-window line
|
|
62
|
+
// (windowLinesHtml) and the budget card (budgetCardHtml) render, so the class/nesting
|
|
63
|
+
// cannot drift between them; each call site shapes its own percent (an integer round for
|
|
64
|
+
// the window, one decimal for the card).
|
|
65
|
+
function meterBar(tone, pct) {
|
|
66
|
+
return "<span class='meter" + tone + "'><span style='width:" + pct + "%'></span></span>";
|
|
67
|
+
}
|
|
58
68
|
// The subscription usage window (plans/agent-backends.md §8, PLANS.md part 6d/7): the
|
|
59
69
|
// sidebar's spend card renders it beside the daily spend, or in its place for a
|
|
60
|
-
// subscription-only
|
|
70
|
+
// subscription-only team. The label/status/clock rules mirror badges.ts's
|
|
61
71
|
// claudeWindowBadge — the preformatted header fragment the TUI uses cannot be reused here
|
|
62
72
|
// because the card lists EVERY window, not just the most constrained one.
|
|
63
73
|
function windowLabel(type) {
|
|
@@ -76,13 +86,33 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
76
86
|
const d = new Date(ms);
|
|
77
87
|
return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0");
|
|
78
88
|
}
|
|
79
|
-
function
|
|
89
|
+
function windowResets(ms) {
|
|
90
|
+
const minutes = Math.floor((ms - Date.now()) / 60000);
|
|
91
|
+
if (minutes < 1) return "resets now";
|
|
92
|
+
if (minutes < 60) return "resets in " + minutes + "m";
|
|
93
|
+
const hours = Math.floor(minutes / 60);
|
|
94
|
+
if (hours < 24) return "resets in " + hours + "h " + (minutes % 60) + "m";
|
|
95
|
+
return "resets in " + Math.floor(hours / 24) + "d " + (hours % 24) + "h";
|
|
96
|
+
}
|
|
97
|
+
function chainMoved(chain) {
|
|
98
|
+
return Object.values(chain || {}).some((t) => t.reason === "window" || t.reason === "reserve");
|
|
99
|
+
}
|
|
100
|
+
// One line per tier off its primary, the same text as badges.ts's chainFragments.
|
|
101
|
+
function chainLinesHtml(chain) {
|
|
102
|
+
return ["small", "default", "strong"].filter((tier) => chain && chain[tier]).map((tier) => {
|
|
103
|
+
const t = chain[tier];
|
|
104
|
+
const money = t.spentUsd !== undefined && t.capUsd !== undefined ? " " + fmtUsd(t.spentUsd) + "/" + fmtCap(t.capUsd) : "";
|
|
105
|
+
const until = t.returnsAt !== undefined ? " until " + windowClock(t.returnsAt) : "";
|
|
106
|
+
return "<span class='side-card-value window-line'>" + esc(tier + " → " + t.pair + money + until) + "</span>";
|
|
107
|
+
}).join("");
|
|
108
|
+
}
|
|
109
|
+
function windowLine(w, cw, chain) {
|
|
80
110
|
const parts = ["Claude " + windowLabel(w.type)];
|
|
81
111
|
const word = windowWord(w.status);
|
|
82
112
|
parts.push(typeof w.utilization === "number" ? Math.round(w.utilization * 100) + "%" : word === "ok" ? "ok · usage not reported" : word);
|
|
83
113
|
if (w.isUsingOverage) parts.push("overage");
|
|
84
|
-
if (w.status === "rejected" && cw.heldUntil != null) parts.push("held until " + windowClock(cw.heldUntil));
|
|
85
|
-
else if (w.status !== "reset" && typeof w.resetsAt === "number") parts.push(
|
|
114
|
+
if (w.status === "rejected" && cw.heldUntil != null) parts.push((chainMoved(chain) ? "on fallback until " : "held until ") + windowClock(cw.heldUntil));
|
|
115
|
+
else if (w.status !== "reset" && typeof w.resetsAt === "number") parts.push(windowResets(w.resetsAt));
|
|
86
116
|
return parts.join(" · ");
|
|
87
117
|
}
|
|
88
118
|
function windowLineTone(w) {
|
|
@@ -90,28 +120,35 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
90
120
|
if (w.status === "allowed_warning") return " t-amber";
|
|
91
121
|
return "";
|
|
92
122
|
}
|
|
93
|
-
function windowLinesHtml(cw) {
|
|
94
|
-
if (!cw) return
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
function windowLinesHtml(cw, chain) {
|
|
124
|
+
if (!cw) return chainLinesHtml(chain);
|
|
125
|
+
// Shortest window first (5h above 7d), whatever order the server ranks severity in; an
|
|
126
|
+
// unlabelled type sorts last, then by type name. A copy, so cw.windows keeps its order.
|
|
127
|
+
const hours = (w) => {
|
|
128
|
+
const m = /^(\d+)([hdw])$/.exec(windowLabel(w.type));
|
|
129
|
+
return m ? Number(m[1]) * (m[2] === "h" ? 1 : m[2] === "d" ? 24 : 168) : Infinity;
|
|
130
|
+
};
|
|
131
|
+
const shortestFirst = cw.windows.slice().sort((a, b) => (hours(a) === hours(b) ? (a.type < b.type ? -1 : a.type > b.type ? 1 : 0) : hours(a) < hours(b) ? -1 : 1));
|
|
132
|
+
return shortestFirst.map((w) => {
|
|
133
|
+
const line = "<span class='side-card-value window-line" + windowLineTone(w) + "'>" + esc(windowLine(w, cw, chain)) + "</span>";
|
|
97
134
|
if (typeof w.utilization !== "number") return line;
|
|
98
135
|
const pct = Math.min(100, Math.round(w.utilization * 100));
|
|
99
|
-
return line +
|
|
100
|
-
}).join("");
|
|
136
|
+
return line + meterBar(windowLineTone(w), pct);
|
|
137
|
+
}).join("") + chainLinesHtml(chain);
|
|
101
138
|
}
|
|
102
|
-
// The sidebar's spend card for a payload's budget block: a plain card for an all-free
|
|
139
|
+
// The sidebar's spend card for a payload's budget block: a plain card for an all-free team
|
|
103
140
|
// (no spend a cap could bind, so no editor), otherwise the button that opens the cap editor,
|
|
104
141
|
// with a meter while a cap is set.
|
|
105
|
-
function budgetCardHtml(b, cw) {
|
|
106
|
-
// A subscription-only
|
|
142
|
+
function budgetCardHtml(b, cw, chain) {
|
|
143
|
+
// A subscription-only team has no priced spend for a cap to bind, so the window card
|
|
107
144
|
// stands in place of the spend card and carries no editor.
|
|
108
145
|
if (cw && cw.only) {
|
|
109
|
-
return "<div class='side-card' title='Claude.ai subscription usage windows — a rejected window
|
|
110
|
-
"<span class='side-card-label'>" + icon("clock") + "Claude window</span>" + windowLinesHtml(cw) + "</div>";
|
|
146
|
+
return "<div class='side-card' title='Claude.ai subscription usage windows — a rejected window pauses claude model jobs until it resets'>" +
|
|
147
|
+
"<span class='side-card-label'>" + icon("clock") + "Claude window</span>" + windowLinesHtml(cw, chain) + "</div>";
|
|
111
148
|
}
|
|
112
149
|
if (b.free) {
|
|
113
|
-
return "<div class='side-card' title='Every model the
|
|
114
|
-
"<span class='side-card-label'>" + icon("dollar") + "Spend</span><span class='side-card-value'>Free models</span>" + windowLinesHtml(cw) + "</div>";
|
|
150
|
+
return "<div class='side-card' title='Every model the team can use is cost n/a, so there is nothing to cap'>" +
|
|
151
|
+
"<span class='side-card-label'>" + icon("dollar") + "Spend</span><span class='side-card-value'>Free models</span>" + windowLinesHtml(cw, chain) + "</div>";
|
|
115
152
|
}
|
|
116
153
|
const capped = b.capUsd > 0;
|
|
117
154
|
const pct = capped ? Math.min(100, (b.spentUsd / b.capUsd) * 100) : 0;
|
|
@@ -120,12 +157,12 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
120
157
|
esc(capped ? "Today's spend against the daily cap — click to change the cap" : "No daily cap — click to set one") + "'>" +
|
|
121
158
|
"<span class='side-card-label'>" + icon("dollar") + "Spent today<span class='edit'>" + (capped ? "Edit cap" : "Set a cap") + "</span></span>" +
|
|
122
159
|
"<span class='side-card-value'>" + esc(fmtUsd(b.spentUsd)) + " <small>" + esc(capped ? "of " + fmtCap(b.capUsd) : "no daily cap") + "</small></span>" +
|
|
123
|
-
(capped ?
|
|
160
|
+
(capped ? meterBar(tone, pct.toFixed(1)) : "") + windowLinesHtml(cw, chain) + "</button>";
|
|
124
161
|
}
|
|
125
162
|
function renderBudgetBadge(d) {
|
|
126
163
|
if (budgetEditing) return; // the open editor owns the slot until save or cancel
|
|
127
164
|
const b = d && d.budget;
|
|
128
|
-
paintPanel("budgetwrap", b ? budgetCardHtml(b, d && d.claudeWindow) : "");
|
|
165
|
+
paintPanel("budgetwrap", b ? budgetCardHtml(b, d && d.claudeWindow, d && d.chain) : "");
|
|
129
166
|
}
|
|
130
167
|
function openBudgetEditor() {
|
|
131
168
|
const b = lastStatus && lastStatus.budget;
|
|
@@ -137,7 +174,7 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
137
174
|
html: "<label for='budgetinput'>Daily spend cap</label>" +
|
|
138
175
|
"<div class='input-prefix'><span>$</span><input type='number' min='0' step='0.01' id='budgetinput' value='" +
|
|
139
176
|
esc(b.capUsd > 0 ? String(b.capUsd) : "") + "' placeholder='no cap'></div>" +
|
|
140
|
-
"<p class='hint'>" + esc("Spent today: " + fmtUsd(b.spentUsd) + ". At the cap the
|
|
177
|
+
"<p class='hint'>" + esc("Spent today: " + fmtUsd(b.spentUsd) + ". At the cap the roles " +
|
|
141
178
|
(b.fallback ? "switch to the free fallback model" : "pause") + " until midnight. Leave it empty for no cap.") + "</p>" +
|
|
142
179
|
"<div class='popover-actions'><button type='button' class='btn btn-sm' id='budgetcancel'>Cancel</button>" +
|
|
143
180
|
"<button type='button' class='btn btn-sm btn-primary' id='budgetset'>Save</button></div>",
|
|
@@ -211,20 +248,20 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
211
248
|
let pauseMenuOpen = false;
|
|
212
249
|
// The timed pauses the menu offers; 0 is a standing pause that lasts until Resume.
|
|
213
250
|
const PAUSE_CHOICES = [[1800, "For 30 minutes"], [3600, "For 1 hour"], [7200, "For 2 hours"], [28800, "For 8 hours"], [0, "Until I resume it"]];
|
|
214
|
-
// How long a timed
|
|
251
|
+
// How long a timed team pause has left (humanSeconds), or "" for a standing one.
|
|
215
252
|
function pauseLeft(d) {
|
|
216
253
|
return d.pausedUntil ? humanSeconds(Math.max(0, Math.round((d.pausedUntil - Date.now()) / 1000))) : "";
|
|
217
254
|
}
|
|
218
|
-
// The sidebar's pause control: Resume (with a timed pause's countdown) while the
|
|
255
|
+
// The sidebar's pause control: Resume (with a timed pause's countdown) while the team is
|
|
219
256
|
// paused, otherwise the button that opens the menu of timed pauses.
|
|
220
257
|
function pauseControlHtml(d) {
|
|
221
258
|
if (d.paused) {
|
|
222
259
|
const left = pauseLeft(d);
|
|
223
|
-
return "<button type='button' class='btn btn-warn' id='pausebadge' title='" + esc("The
|
|
224
|
-
icon("play") + "<span>Resume the
|
|
260
|
+
return "<button type='button' class='btn btn-warn' id='pausebadge' title='" + esc("The team is paused — click to resume it" + (d.pauseReason ? " (" + d.pauseReason + ")" : "")) + "'>" +
|
|
261
|
+
icon("play") + "<span>Resume the team</span><span class='btn-note' id='pausenote'>" + (left ? esc(left) + " left" : "") + "</span></button>";
|
|
225
262
|
}
|
|
226
|
-
return "<button type='button' class='btn' id='pausebadge' aria-haspopup='menu' title='Pause the
|
|
227
|
-
icon("pause") + "<span>Pause the
|
|
263
|
+
return "<button type='button' class='btn' id='pausebadge' aria-haspopup='menu' title='Pause the team — roles start no new runs'>" +
|
|
264
|
+
icon("pause") + "<span>Pause the team</span><span class='btn-note'>" + icon("chev") + "</span></button>";
|
|
228
265
|
}
|
|
229
266
|
function renderPauseBadge(d) {
|
|
230
267
|
if (pauseMenuOpen) return;
|
|
@@ -241,11 +278,11 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
241
278
|
const pop = openPopover("pausewrap", {
|
|
242
279
|
menu: true,
|
|
243
280
|
role: "menu",
|
|
244
|
-
html: "<div class='menu-title'>Pause the
|
|
281
|
+
html: "<div class='menu-title'>Pause the team</div>" +
|
|
245
282
|
"<input type='text' id='pausereason' class='menu-input' maxlength=${PAUSE_REASON_MAX} placeholder='Reason (optional)' aria-label='Pause reason' style='margin:2px 10px 4px;width:calc(100% - 20px)'>" +
|
|
246
283
|
PAUSE_CHOICES.map((c) => "<button type='button' class='menu-item' role='menuitem' data-pause='" + c[0] + "'>" +
|
|
247
284
|
icon(c[0] ? "clock" : "pause") + esc(c[1]) + "</button>").join("") +
|
|
248
|
-
"<p class='hint' style='margin:4px 10px 4px'>
|
|
285
|
+
"<p class='hint' style='margin:4px 10px 4px'>Current runs finish, and the director keeps running your messages.</p>",
|
|
249
286
|
onClosed: () => {
|
|
250
287
|
pauseMenuOpen = false;
|
|
251
288
|
renderPauseBadge(lastStatus);
|
|
@@ -255,20 +292,20 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
255
292
|
const first = pop.querySelector("button");
|
|
256
293
|
if (first) first.focus({ preventScroll: true });
|
|
257
294
|
}
|
|
258
|
-
async function
|
|
295
|
+
async function setTeamPause(paused, forSeconds, reason) {
|
|
259
296
|
const body = { paused: paused };
|
|
260
297
|
if (paused && forSeconds > 0) body.forSeconds = forSeconds;
|
|
261
298
|
const why = paused && reason ? String(reason).trim() : "";
|
|
262
299
|
if (why) body.reason = why;
|
|
263
300
|
// A failure changes nothing; the control still shows the real state.
|
|
264
301
|
await postAction("/api/pause", body, paused
|
|
265
|
-
? "
|
|
266
|
-
: "
|
|
302
|
+
? "Team paused" + (forSeconds > 0 ? " for " + humanSeconds(forSeconds) : "") + " — current runs finish"
|
|
303
|
+
: "Team resumed");
|
|
267
304
|
}
|
|
268
305
|
onClick((t, ev) => {
|
|
269
306
|
if (t.closest("#pausebadge")) {
|
|
270
307
|
ev.preventDefault();
|
|
271
|
-
if (lastStatus && lastStatus.paused)
|
|
308
|
+
if (lastStatus && lastStatus.paused) setTeamPause(false);
|
|
272
309
|
else if (pauseMenuOpen) closeMenus();
|
|
273
310
|
else openPauseMenu();
|
|
274
311
|
return;
|
|
@@ -276,14 +313,67 @@ export const GUI_CLIENT_OPERATOR_JS = String.raw ` // shared-operator:start
|
|
|
276
313
|
const choice = t.closest("[data-pause]");
|
|
277
314
|
if (choice) {
|
|
278
315
|
const input = $("pausereason");
|
|
279
|
-
|
|
316
|
+
setTeamPause(true, Number(choice.dataset.pause), input ? input.value : "");
|
|
280
317
|
}
|
|
281
318
|
});
|
|
282
319
|
// pause-control:end
|
|
283
320
|
|
|
321
|
+
// stop-control:start
|
|
322
|
+
let stopMenuOpen = false;
|
|
323
|
+
// The delays the menu offers for a scheduled graceful stop.
|
|
324
|
+
const STOP_CHOICES = [[3600, "In 1 hour"], [7200, "In 2 hours"], [14400, "In 4 hours"], [28800, "In 8 hours"]];
|
|
325
|
+
// The sidebar's scheduled-stop control: nothing while the team is stopped; Cancel while a
|
|
326
|
+
// stop is scheduled; otherwise the button that opens the menu of delays. No immediate stop:
|
|
327
|
+
// under run --gui it would take the page down mid-click.
|
|
328
|
+
function stopControlHtml(d) {
|
|
329
|
+
if (!d || !d.running) return "";
|
|
330
|
+
if (d.scheduledStopAt) {
|
|
331
|
+
return "<button type='button' class='btn btn-warn' id='stopbadge' title='A stop is scheduled — click to cancel it'>" +
|
|
332
|
+
icon("stop") + "<span>Cancel scheduled stop</span></button>";
|
|
333
|
+
}
|
|
334
|
+
return "<button type='button' class='btn' id='stopbadge' aria-haspopup='menu' title='Stop the team later — current runs finish, then it stops'>" +
|
|
335
|
+
icon("stop") + "<span>Stop the team later</span><span class='btn-note'>" + icon("chev") + "</span></button>";
|
|
336
|
+
}
|
|
337
|
+
function renderStopControl(d) {
|
|
338
|
+
if (stopMenuOpen) return;
|
|
339
|
+
paintPanel("stopwrap", stopControlHtml(d));
|
|
340
|
+
}
|
|
341
|
+
function openStopMenu() {
|
|
342
|
+
const pop = openPopover("stopwrap", {
|
|
343
|
+
menu: true,
|
|
344
|
+
role: "menu",
|
|
345
|
+
html: "<div class='menu-title'>Stop the team later</div>" +
|
|
346
|
+
STOP_CHOICES.map((c) => "<button type='button' class='menu-item' role='menuitem' data-stopin='" + c[0] + "'>" +
|
|
347
|
+
icon("clock") + esc(c[1]) + "</button>").join("") +
|
|
348
|
+
"<p class='hint' style='margin:4px 10px 4px'>Current runs finish, then the team stops.</p>",
|
|
349
|
+
onClosed: () => {
|
|
350
|
+
stopMenuOpen = false;
|
|
351
|
+
renderStopControl(lastStatus);
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
stopMenuOpen = true;
|
|
355
|
+
const first = pop.querySelector("button");
|
|
356
|
+
if (first) first.focus({ preventScroll: true });
|
|
357
|
+
}
|
|
358
|
+
function postStopSchedule(body) {
|
|
359
|
+
return postAction("/api/stop-schedule", body, (d) => d && typeof d.message === "string" ? d.message : "Stop schedule updated");
|
|
360
|
+
}
|
|
361
|
+
onClick((t, ev) => {
|
|
362
|
+
if (t.closest("#stopbadge")) {
|
|
363
|
+
ev.preventDefault();
|
|
364
|
+
if (lastStatus && lastStatus.scheduledStopAt) postStopSchedule({ cancel: true });
|
|
365
|
+
else if (stopMenuOpen) closeMenus();
|
|
366
|
+
else openStopMenu();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const choice = t.closest("[data-stopin]");
|
|
370
|
+
if (choice) postStopSchedule({ inSeconds: Number(choice.dataset.stopin) });
|
|
371
|
+
});
|
|
372
|
+
// stop-control:end
|
|
373
|
+
|
|
284
374
|
// sound-control:start
|
|
285
375
|
// The sidebar's sound control, beside the theme toggle: a speaker icon that flips between
|
|
286
|
-
// the unmuted and muted glyph. A per-browser preference (setSoundMuted stores it), not
|
|
376
|
+
// the unmuted and muted glyph. A per-browser preference (setSoundMuted stores it), not team
|
|
287
377
|
// state — no server call; the choice takes effect on the next cue.
|
|
288
378
|
function soundControlHtml() {
|
|
289
379
|
return "<button type='button' class='icon-btn' id='soundtoggle' title='" +
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** The dashboard's Pending view, browser-side: every loop's unmerged change as /api/diff
|
|
2
3
|
* serves it (the same document `tumwater diff` prints from the shared change collectors). The
|
|
3
4
|
* roster shows each role's branch, state, commits ahead of main with their subjects, and its
|
|
4
5
|
* uncommitted-file count; a row opens that loop's drawer, whose Pending change section fetches
|
|
5
6
|
* the full patch (`/api/diff?role=<id>`). It refetches when the view opens and when its
|
|
6
7
|
* Refresh button is pressed — never on a timer of its own. Spliced into gui-client.ts's
|
|
7
8
|
* script, reaching its helpers (esc, getJson, icon, errorPanel) through that concatenation. */
|
|
8
|
-
export const GUI_CLIENT_PENDING_JS = String.raw ` let
|
|
9
|
+
export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingTeam = null;
|
|
9
10
|
// pending-table:start
|
|
10
11
|
// One role's state cell: the collector's three states, with "ready" split into pending vs
|
|
11
12
|
// idle work. Never colors a degraded state as an active one.
|
|
@@ -13,10 +14,10 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingFleet = null;
|
|
|
13
14
|
if (r.state === "absent") return "<span class='muted'>no worktree</span>";
|
|
14
15
|
if (r.state === "no-base") return "<span class='muted'>no base</span>";
|
|
15
16
|
if (r.state !== "ready") return "<span class='muted'>unknown</span>";
|
|
16
|
-
if (r.ahead === 0 && (!r.dirtyFiles || r.dirtyFiles.length === 0)) return "<span class='muted'>no
|
|
17
|
+
if (r.ahead === 0 && (!r.dirtyFiles || r.dirtyFiles.length === 0)) return "<span class='muted'>no unmerged work</span>";
|
|
17
18
|
return "<span class='res t-indigo'>pending</span>";
|
|
18
19
|
}
|
|
19
|
-
// One role's ahead cell: the commit count plus each
|
|
20
|
+
// One role's ahead cell: the commit count plus each unmerged commit's sha and subject, or a
|
|
20
21
|
// dash for a degraded/empty role.
|
|
21
22
|
function pendingAheadHtml(r) {
|
|
22
23
|
if (r.state !== "ready" || !r.ahead) return "<span class='muted'>—</span>";
|
|
@@ -30,69 +31,72 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingFleet = null;
|
|
|
30
31
|
}
|
|
31
32
|
// The Pending roster: one clickable row per known role (the collector's config order), each
|
|
32
33
|
// opening that loop's drawer. Pure — the view paints what this returns.
|
|
33
|
-
function pendingTableHtml(
|
|
34
|
-
if (!
|
|
35
|
-
return "<div class='empty'><strong>No
|
|
36
|
-
if (
|
|
37
|
-
return "<div class='empty'><strong>Main branch " + esc(
|
|
38
|
-
const rows =
|
|
34
|
+
function pendingTableHtml(team) {
|
|
35
|
+
if (!team || !Array.isArray(team.roles) || team.roles.length === 0)
|
|
36
|
+
return "<div class='empty'><strong>No roles yet</strong>Roles appear here once the team is configured.</div>";
|
|
37
|
+
if (team.roles.every((r) => r.state === "no-base"))
|
|
38
|
+
return "<div class='empty'><strong>Main branch " + esc(team.mainBranch) + " does not exist</strong>There is no base to compare unmerged work against.</div>";
|
|
39
|
+
const rows = team.roles.map((r) =>
|
|
39
40
|
"<tr class='clickable' data-open='" + esc(r.role) + "'>" +
|
|
40
|
-
"<td class='mono'>" + esc(r.role) + "</td>" +
|
|
41
|
+
"<td class='mono'>" + esc(roleLabel(r.role)) + "</td>" +
|
|
41
42
|
"<td class='mono muted'>" + esc(r.branch) + "</td>" +
|
|
42
43
|
"<td>" + pendingStateHtml(r) + "</td>" +
|
|
43
44
|
"<td>" + pendingAheadHtml(r) + "</td>" +
|
|
44
45
|
"<td class='num'>" + pendingDirtyHtml(r) + "</td></tr>",
|
|
45
46
|
).join("");
|
|
46
|
-
return "<div class='table-wrap'><table class='table pending'><thead><tr><th class='c-loop'>
|
|
47
|
-
"<th>State</th><th>Ahead of " + esc(
|
|
47
|
+
return "<div class='table-wrap'><table class='table pending'><thead><tr><th class='c-loop'>Role</th><th>Branch</th>" + // vocabulary-ok: CSS class the stylesheet matches
|
|
48
|
+
"<th>State</th><th>Ahead of " + esc(team.mainBranch) + "</th><th class='c-dirty num'>Uncommitted</th></tr></thead><tbody>" + rows + "</tbody></table></div>";
|
|
48
49
|
}
|
|
49
|
-
// The
|
|
50
|
-
// line each mirroring tumwater queue's
|
|
50
|
+
// The Merge queue card's body: every queued merge in execution order (oldest first), one
|
|
51
|
+
// line each mirroring tumwater queue's renderMergeQueue — 1-based position, loop, tick,
|
|
51
52
|
// short sha, age, summary — so the page and the CLI cannot drift. Painted from the status
|
|
52
53
|
// payload the page already polls; depth counts the unreadable queue files the status reader
|
|
53
54
|
// skips, so a positive depth with no readable list states the count instead of inventing rows.
|
|
54
55
|
// Pure: the age comes from the passed now, and a malformed entry renders a stand-in rather
|
|
55
56
|
// than undefined/NaN. Null/absent/zero-depth reads as empty.
|
|
56
|
-
function pendingQueueHtml(
|
|
57
|
-
const depth =
|
|
58
|
-
const entries =
|
|
57
|
+
function pendingQueueHtml(mergeQueue, now, labels) {
|
|
58
|
+
const depth = mergeQueue && Number.isFinite(mergeQueue.depth) ? mergeQueue.depth : 0;
|
|
59
|
+
const entries = mergeQueue && Array.isArray(mergeQueue.entries) ? mergeQueue.entries : [];
|
|
59
60
|
let body;
|
|
60
61
|
if (depth === 0) {
|
|
61
|
-
body = "<div class='empty'>
|
|
62
|
+
body = "<div class='empty'>Merge queue empty</div>";
|
|
62
63
|
} else if (entries.length === 0) {
|
|
63
|
-
body = "<div class='empty'>
|
|
64
|
+
body = "<div class='empty'>merge queue: " + esc(depth) + " waiting</div>";
|
|
64
65
|
} else {
|
|
65
66
|
body = "<div style='padding:10px 16px 12px'>" + entries.map((e, i) => {
|
|
66
67
|
const sec = Number.isFinite(e.enqueuedAt) ? Math.max(0, Math.round((now - e.enqueuedAt) / 1000)) : 0;
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"
|
|
68
|
+
const run = Number.isFinite(e.run) ? e.run : "?";
|
|
69
|
+
const note = Number.isFinite(e.revisionRound) ? " · revision " + e.revisionRound + (Number.isFinite(e.priorObjections) && e.priorObjections > 0 ? " · " + plural(e.priorObjections, "prior objection") : "") : "";
|
|
70
|
+
return "<div class='mono clamp1' style='padding:3px 0'>" + (i + 1) + ". " + esc(e.role ? roleLabel(e.role) : "?") +
|
|
71
|
+
" run #" + esc(run) + " · " + esc(shortSha(e.sha)) + " · " + esc(humanSeconds(sec)) +
|
|
72
|
+
" ago" + (Array.isArray(labels) && labels[i] && typeof labels[i] === "string" ? " · [" + esc(labels[i]) + "]" : "") +
|
|
73
|
+
note +
|
|
74
|
+
" · " + esc(e.summary || "—") + "</div>";
|
|
71
75
|
}).join("") + "</div>";
|
|
72
76
|
}
|
|
73
|
-
return "<div class='card-head'><h2>
|
|
77
|
+
return "<div class='card-head'><h2>Merge queue</h2></div>" + body;
|
|
74
78
|
}
|
|
75
79
|
// pending-table:end
|
|
76
|
-
// Paint the
|
|
80
|
+
// Paint the Merge queue card from the polled status payload, at paint-time age; no fetch of
|
|
77
81
|
// its own (the entries ride /api/status, unlike the roster's /api/diff).
|
|
78
82
|
function renderPendingQueue(status) {
|
|
79
83
|
const card = $("pendingqueue");
|
|
80
84
|
if (!card) return;
|
|
81
|
-
card.innerHTML = pendingQueueHtml(status && status.
|
|
85
|
+
card.innerHTML = pendingQueueHtml(status && status.mergeQueue, Date.now(), status && status.mergeQueueLabels);
|
|
82
86
|
}
|
|
83
87
|
function buildPendingView() {
|
|
84
|
-
if (!buildOnce("pending", "<div class='view-head'><div><h1>Pending</h1><p>Every
|
|
88
|
+
if (!buildOnce("pending", "<div class='view-head'><div><h1>Pending</h1><p>Every role's unmerged change — its branch, commits ahead of main, and uncommitted files. Open a row for the full diff.</p></div>" +
|
|
85
89
|
"<div class='toolbar'><button type='button' class='btn btn-sm' id='pendingrefresh'>" + icon("refresh") + "Refresh</button></div></div>" +
|
|
86
90
|
"<div class='card' id='pendingqueue'><div class='empty'>Loading…</div></div>" +
|
|
87
91
|
"<div class='card' id='pendingbody'><div class='empty'>Loading…</div></div>")) return;
|
|
88
92
|
$("pendingrefresh").addEventListener("click", () => fetchPending());
|
|
89
93
|
}
|
|
90
|
-
// Load the
|
|
91
|
-
// no timer of its own, and the roster only changes when a loop
|
|
94
|
+
// Load the team-wide change roster. Called when the view opens and by the Refresh button;
|
|
95
|
+
// no timer of its own, and the roster only changes when a loop merges or edits its worktree.
|
|
92
96
|
async function fetchPending() {
|
|
93
97
|
buildPendingView();
|
|
94
98
|
try {
|
|
95
|
-
|
|
99
|
+
pendingTeam = await getJson("/api/diff");
|
|
96
100
|
} catch (err) {
|
|
97
101
|
const body = $("pendingbody");
|
|
98
102
|
if (body) body.innerHTML = errorPanel("Pending unavailable", err);
|
|
@@ -104,6 +108,6 @@ export const GUI_CLIENT_PENDING_JS = String.raw ` let pendingFleet = null;
|
|
|
104
108
|
function renderPending() {
|
|
105
109
|
const body = $("pendingbody");
|
|
106
110
|
if (!body) return;
|
|
107
|
-
body.innerHTML = pendingTableHtml(
|
|
111
|
+
body.innerHTML = pendingTableHtml(pendingTeam);
|
|
108
112
|
}
|
|
109
113
|
`;
|