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 History view, browser-side: every completed tick, newest first, as
|
|
2
3
|
* /api/history serves them (the rows `tumwater history` prints). A toolbar narrows it to one
|
|
3
4
|
* loop (server-side, so a quiet loop still gets its full count), searches the rendered row text
|
|
@@ -16,16 +17,20 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
|
|
|
16
17
|
let histSinceKey = recall("hist-since") || "";
|
|
17
18
|
let histNote = "";
|
|
18
19
|
let histFilter = "all";
|
|
20
|
+
let histMode = recall("hist-mode") === "summary" ? "summary" : "table";
|
|
19
21
|
let histRows = null;
|
|
22
|
+
let histSummary = null;
|
|
20
23
|
let histFetchedAt = 0;
|
|
21
|
-
const HIST_FILTERS = [["all", "All"], ["changes", "Changes"], ["none", "No change"], ["problems", "Problems"]];
|
|
24
|
+
const HIST_FILTERS = [["all", "All"], ["changes", "Changes"], ["none", "No change"], ["refused", "Refused"], ["problems", "Problems"]];
|
|
25
|
+
const HIST_MODES = [["table", "Table"], ["summary", "Summary"]];
|
|
22
26
|
function buildHistoryView() {
|
|
23
|
-
if (!buildOnce("history", "<div class='view-head'><div><h1>History</h1><p>Every completed
|
|
27
|
+
if (!buildOnce("history", "<div class='view-head'><div><h1>History</h1><p>Every completed run, newest first.</p></div>" +
|
|
24
28
|
"<div class='toolbar'>" +
|
|
25
|
-
"<label><span class='sr-only'>
|
|
29
|
+
"<label><span class='sr-only'>Role</span><select class='field' id='histrole'><option value=''>All roles</option></select></label>" +
|
|
26
30
|
"<div class='seg' id='histfilter'>" + HIST_FILTERS.map((f) => "<button type='button' data-hist='" + f[0] + "'>" + f[1] + "</button>").join("") + "</div>" +
|
|
27
|
-
"<
|
|
28
|
-
"<label><span class='sr-only'>
|
|
31
|
+
"<div class='seg' id='histsum'>" + HIST_MODES.map((m) => "<button type='button' data-histsum='" + m[0] + "'>" + m[1] + "</button>").join("") + "</div>" +
|
|
32
|
+
"<label><span class='sr-only'>Search</span><input class='field' id='histgrep' type='search' placeholder='Search runs…' autocomplete='off'></label>" +
|
|
33
|
+
"<label><span class='sr-only'>Runs to show</span><select class='field' id='histcount'>" +
|
|
29
34
|
HIST_COUNTS.map((n) => "<option value='" + n + "'>Last " + n + "</option>").join("") +
|
|
30
35
|
histWindowOptions() + "</select></label>" +
|
|
31
36
|
"<button type='button' class='btn btn-sm' id='histrefresh'>" + icon("refresh") + "Refresh</button>" +
|
|
@@ -60,6 +65,13 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
|
|
|
60
65
|
histFilter = b.dataset.hist;
|
|
61
66
|
renderHistory();
|
|
62
67
|
});
|
|
68
|
+
$("histsum").addEventListener("click", (ev) => {
|
|
69
|
+
const b = ev.target.closest("button[data-histsum]");
|
|
70
|
+
if (!b) return;
|
|
71
|
+
histMode = b.dataset.histsum;
|
|
72
|
+
store("hist-mode", histMode);
|
|
73
|
+
renderHistory();
|
|
74
|
+
});
|
|
63
75
|
$("histrefresh").addEventListener("click", () => fetchHistory());
|
|
64
76
|
// The drill-down's details toggle: the button alone expands or collapses (the row's own
|
|
65
77
|
// click still opens the drawer — gui-client-boot's data-open handler skips clicks on a
|
|
@@ -73,7 +85,7 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
|
|
|
73
85
|
// history-detail:start
|
|
74
86
|
// Tick drill-down state: one open card at a time, keyed "role#tick", with each key's cache
|
|
75
87
|
// entry — { state: "loading" } while its fetch is in flight, { state: "error", message }
|
|
76
|
-
// when one failed, { state: "ok", text } once /api/
|
|
88
|
+
// when one failed, { state: "ok", text } once /api/run answered. Cached until the view
|
|
77
89
|
// refetches (fetchHistory clears the map), so collapsing and re-expanding reuses the card.
|
|
78
90
|
const histDetails = new Map();
|
|
79
91
|
let histDetailOpen = "";
|
|
@@ -88,13 +100,14 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
|
|
|
88
100
|
// the tumwater tick rendering).
|
|
89
101
|
function histDetailCardHtml(entry) {
|
|
90
102
|
const state = entry && entry.state;
|
|
91
|
-
if (!state || state === "loading") return "<div class='empty'>Loading
|
|
92
|
-
if (state === "error") return "<div class='empty'><strong>" + esc(entry.message || "
|
|
103
|
+
if (!state || state === "loading") return "<div class='empty'>Loading run detail…</div>";
|
|
104
|
+
if (state === "error") return "<div class='empty'><strong>" + esc(entry.message || "Run detail unavailable") + "</strong></div>";
|
|
93
105
|
const lines = String(entry.text || "").split("\n");
|
|
94
106
|
const head = lines.shift() || "";
|
|
95
107
|
const trail = lines.join("\n");
|
|
96
108
|
return "<div class='hist-detail'><div class='hist-detail-head mono'>" + esc(head) + "</div>" +
|
|
97
|
-
(trail ? "<pre class='mono'>" + esc(trail) + "</pre>" : "<div class='empty'>No events in this
|
|
109
|
+
(trail ? "<pre class='mono'>" + esc(trail) + "</pre>" : "<div class='empty'>No events in this run's block.</div>") +
|
|
110
|
+
(entry.stat ? "<pre class='mono hist-detail-stat'>" + esc(entry.stat) + "</pre>" : "") +
|
|
98
111
|
"</div>";
|
|
99
112
|
}
|
|
100
113
|
// Expand or collapse one row's card. Opening fetches the trail on first expand only — an
|
|
@@ -116,27 +129,27 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
|
|
|
116
129
|
// (and a re-expand during flight sees it and lets the first fetch finish).
|
|
117
130
|
histDetails.set(key, { state: "loading" });
|
|
118
131
|
try {
|
|
119
|
-
const d = await getJson("/api/
|
|
132
|
+
const d = await getJson("/api/run?role=" + encodeURIComponent(role) + "&run=" + encodeURIComponent(String(tick)));
|
|
120
133
|
// Name the endpoint, the tick it answered for, and the likely cause: a 200 without
|
|
121
134
|
// text is a server too old (or too new) for this dashboard's client, not a lost tick —
|
|
122
135
|
// a 404 already answers that shape with the CLI's own not-found wording.
|
|
123
136
|
if (!d || typeof d.text !== "string")
|
|
124
137
|
throw new Error(
|
|
125
|
-
"/api/
|
|
138
|
+
"/api/run answered " + role + " run " + tick + " without a text field — the running tumwater may not match this dashboard",
|
|
126
139
|
);
|
|
127
|
-
histDetails.set(key, { state: "ok", text: d.text });
|
|
140
|
+
histDetails.set(key, { state: "ok", text: d.text, stat: typeof d.stat === "string" ? d.stat : "" });
|
|
128
141
|
} catch (err) {
|
|
129
|
-
histDetails.set(key, { state: "error", message: err && err.message ? err.message : "
|
|
142
|
+
histDetails.set(key, { state: "error", message: err && err.message ? err.message : "Run detail unavailable" });
|
|
130
143
|
}
|
|
131
144
|
renderHistory();
|
|
132
145
|
}
|
|
133
146
|
// history-detail:end
|
|
134
|
-
// The loop picker's options follow the
|
|
135
|
-
// poll with no roster change repaints nothing. A role the
|
|
147
|
+
// The loop picker's options follow the team's role list (custom loops included), keyed so a
|
|
148
|
+
// poll with no roster change repaints nothing. A role the team dropped falls back to All
|
|
136
149
|
// loops and persists, exactly as syncRolePicker does for the report and Activity pickers.
|
|
137
150
|
function syncHistoryRoles(d) {
|
|
138
151
|
if (!d) return;
|
|
139
|
-
histRole = syncRolePicker("histrole", (d.
|
|
152
|
+
histRole = syncRolePicker("histrole", (d.roles || []).map((l) => l.role), histRole, "hist-role");
|
|
140
153
|
}
|
|
141
154
|
async function fetchHistory(throttled) {
|
|
142
155
|
if (throttled && Date.now() - histFetchedAt < 4000) return;
|
|
@@ -146,6 +159,10 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
|
|
|
146
159
|
try {
|
|
147
160
|
const d = await getJson(historyQuery(histCount, histRole, histSinceKey, histGrep));
|
|
148
161
|
histRows = d && Array.isArray(d.rows) ? d.rows : [];
|
|
162
|
+
// The summary rides the same fetch (the endpoint folds the exact rows it served into
|
|
163
|
+
// its additive summary), so toggling to Summary never refetches; a server too old to
|
|
164
|
+
// include it leaves histSummary null and the toggle falls back to the table.
|
|
165
|
+
histSummary = d && d.summary && Array.isArray(d.summary.roles) ? d.summary : null;
|
|
149
166
|
// The trailing-window shape carries the server's coverage; the count shape has no note.
|
|
150
167
|
histNote = d && typeof d.note === "string" ? d.note : "";
|
|
151
168
|
// A refetch invalidates the drill-down cache: cached trails could disagree with the new
|
|
@@ -166,10 +183,12 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
|
|
|
166
183
|
// names the window in the empty state.
|
|
167
184
|
const HIST_WINDOWS = { "1h": [3_600_000, "the last hour"], "6h": [21_600_000, "the last 6 hours"], "1d": [86_400_000, "the last day"], "7d": [604_800_000, "the last 7 days"] };
|
|
168
185
|
// The /api/history URL for the current toolbar: the trailing-window shape when a window key
|
|
169
|
-
// is set, the count shape otherwise, role and grep appended only when chosen.
|
|
186
|
+
// is set, the count shape otherwise, role and grep appended only when chosen. summary=1 rides
|
|
187
|
+
// every ask — the endpoint's additive summary folds the exact served rows, so the Summary
|
|
188
|
+
// mode renders from already-fetched data and the toggle swaps only the body.
|
|
170
189
|
function historyQuery(count, role, sinceKey, grep) {
|
|
171
190
|
const w = HIST_WINDOWS[sinceKey];
|
|
172
|
-
return (w ? "/api/history?sinceMs=" + w[0] : "/api/history?n=" + count) + (role ? "&role=" + encodeURIComponent(role) : "") + (grep ? "&grep=" + encodeURIComponent(grep) : "");
|
|
191
|
+
return (w ? "/api/history?sinceMs=" + w[0] : "/api/history?n=" + count) + (role ? "&role=" + encodeURIComponent(role) : "") + (grep ? "&grep=" + encodeURIComponent(grep) : "") + "&summary=1";
|
|
173
192
|
}
|
|
174
193
|
// The window options, the phrase capitalized for the select ("The last day").
|
|
175
194
|
function histWindowOptions() {
|
|
@@ -186,58 +205,97 @@ export const GUI_CLIENT_HISTORY_JS = String.raw ` // The counts the "Ticks to s
|
|
|
186
205
|
function historyClass(result) {
|
|
187
206
|
if (result === "changed" || result === "queued") return "changes";
|
|
188
207
|
if (result === "no_change" || result === "skipped" || result === "user_aborted") return "none";
|
|
208
|
+
if (result === "refused") return "refused";
|
|
189
209
|
return "problems";
|
|
190
210
|
}
|
|
191
211
|
// The History table for rows (/api/history's, newest first) under a grep (already applied
|
|
192
212
|
// server-side) and an outcome filter; known is
|
|
193
|
-
// the set of the
|
|
213
|
+
// the set of the team's loop ids, whose rows open that loop's drawer. Pure — the view's
|
|
194
214
|
// render paints what this returns.
|
|
195
215
|
function historyTableHtml(rows, filter, known, grep, sinceKey) {
|
|
196
216
|
const shown = (rows || []).filter((r) => filter === "all" || historyClass(r.result) === filter);
|
|
197
217
|
if (!shown.length) {
|
|
198
218
|
const hasRows = Boolean(rows && rows.length);
|
|
199
219
|
if (hasRows)
|
|
200
|
-
return "<div class='empty'><strong>No
|
|
220
|
+
return "<div class='empty'><strong>No runs match this filter</strong>Try another outcome or role.</div>";
|
|
201
221
|
if (grep)
|
|
202
|
-
return "<div class='empty'><strong>No
|
|
222
|
+
return "<div class='empty'><strong>No runs match this search</strong>Try another search.</div>";
|
|
203
223
|
const w = HIST_WINDOWS[sinceKey];
|
|
204
224
|
return w
|
|
205
|
-
? "<div class='empty'><strong>No
|
|
206
|
-
: "<div class='empty'><strong>No
|
|
225
|
+
? "<div class='empty'><strong>No runs in " + w[1] + "</strong>The window may have had nothing to do, or its older events may have rotated out.</div>"
|
|
226
|
+
: "<div class='empty'><strong>No runs yet</strong>Completed runs show up here as the roles work.</div>";
|
|
207
227
|
}
|
|
208
|
-
return "<div class='table-wrap'><table class='table history'><thead><tr><th class='c-x'></th><th>When</th><th>
|
|
209
|
-
"<th>Result</th><th class='c-detail'>Detail</th><th class='num c-dur'>Took</th><th class='num c-usage'>Usage</th></tr></thead><tbody>" +
|
|
228
|
+
return "<div class='table-wrap'><table class='table history'><thead><tr><th class='c-x'></th><th>When</th><th>Role</th><th class='c-tick num'>Run</th>" + // vocabulary-ok: the c-tick class stays
|
|
229
|
+
"<th>Result</th><th class='c-model'>Model</th><th class='c-detail'>Detail</th><th class='num c-dur'>Took</th><th class='num c-usage'>Usage</th></tr></thead><tbody>" +
|
|
210
230
|
shown.map((r) => {
|
|
211
231
|
const res = resultInfo(r.result);
|
|
212
232
|
// The drill-down toggle rides the drawer's button style and lives in its own leading
|
|
213
|
-
// cell; only a
|
|
214
|
-
// /api/
|
|
233
|
+
// cell; only a team loop's row carries one, the same rows that open the drawer (the
|
|
234
|
+
// /api/run endpoint validates the role, so an id the config no longer knows would
|
|
215
235
|
// only ever answer 400).
|
|
216
|
-
const key = histDetailKey(r.
|
|
236
|
+
const key = histDetailKey(r.role, r.run);
|
|
217
237
|
const open = histDetailOpen === key;
|
|
218
|
-
const toggle = known.has(r.
|
|
219
|
-
? "<button type='button' class='btn btn-sm hist-x' data-tickdetail='1' data-role='" + esc(r.
|
|
220
|
-
"' data-tick='" + esc(r.
|
|
238
|
+
const toggle = known.has(r.role)
|
|
239
|
+
? "<button type='button' class='btn btn-sm hist-x' data-tickdetail='1' data-role='" + esc(r.role) +
|
|
240
|
+
"' data-tick='" + esc(r.run) + "' aria-expanded='" + open + "' title='" + (open ? "Hide run detail" : "Show run detail") + "'>" + icon("chev") + "</button>" // vocabulary-ok: the data-tick name stays
|
|
221
241
|
: "";
|
|
222
|
-
const row = "<tr" + (known.has(r.
|
|
242
|
+
const row = "<tr" + (known.has(r.role) ? " class='clickable' data-open='" + esc(r.role) + "'" : "") + ">" +
|
|
223
243
|
"<td class='c-x'>" + toggle + "</td>" +
|
|
224
244
|
"<td title='" + esc(r.time) + "'><span class='clamp1'>" + esc(fmtAgo(r.ts)) + "</span></td>" +
|
|
225
|
-
"<td class='mono'>" + esc(r.
|
|
226
|
-
"<td class='c-tick num muted'>#" + esc(r.
|
|
245
|
+
"<td class='mono'>" + esc(roleLabel(r.role)) + "</td>" +
|
|
246
|
+
"<td class='c-tick num muted'>#" + esc(r.run) + "</td>" + // vocabulary-ok: the c-tick class stays
|
|
227
247
|
"<td><span class='res t-" + res.tone + "'>" + esc(res.label) + "</span></td>" +
|
|
248
|
+
"<td class='c-model'><span class='clamp1' title='" + esc(r.model || "") + "'>" + (r.model ? esc(r.model) : "<span class='muted'>—</span>") + "</span></td>" +
|
|
228
249
|
"<td class='c-detail'><span class='clamp2' title='" + esc(r.detail) + "'>" + (r.detail ? esc(r.detail) : "<span class='muted'>—</span>") + "</span></td>" +
|
|
229
|
-
"<td class='num c-dur'>" + (r.durationMs
|
|
250
|
+
"<td class='num c-dur'>" + durCell(r.durationMs) + "</td>" +
|
|
230
251
|
"<td class='num c-usage muted'>" + esc(r.usage || "—") + "</td></tr>";
|
|
231
252
|
// The expanded card rides directly beneath its row, full width.
|
|
232
|
-
return open ? row + "<tr class='detail-row'><td colspan='
|
|
253
|
+
return open ? row + "<tr class='detail-row'><td colspan='9'>" + histDetailCardHtml(histDetails.get(key)) + "</td></tr>" : row;
|
|
233
254
|
}).join("") + "</tbody></table></div>";
|
|
234
255
|
}
|
|
235
256
|
// history-table:end
|
|
257
|
+
// history-summary:start
|
|
258
|
+
// The Summary table: one row per loop plus the total, in the same column set the CLI's
|
|
259
|
+
// history --summary prints (ticks, changes, none, refused, problems, tokens, cost, time,
|
|
260
|
+
// last change), so the dashboard and the CLI cannot drift column for column. Every number
|
|
261
|
+
// comes from the
|
|
262
|
+
// server's summary (built on the shared summarizeTickRows) — nothing here re-derives a
|
|
263
|
+
// tally — and the cost/token cells reuse the page's report formatters. The time cell is the
|
|
264
|
+
// sum of the loop's run spans via fmtSpan, a dash when none of its rows carried a span. The
|
|
265
|
+
// last-change cell is the newest changes-class tick's age via fmtAgo, a dash when the loop had
|
|
266
|
+
// none. Pure — the view's render paints what this returns.
|
|
267
|
+
function histSummaryHtml(summary) {
|
|
268
|
+
const all = summary.roles.concat(summary.total ? [summary.total] : []);
|
|
269
|
+
const cell = (r, key, fmt) => "<td class='num'>" + esc(fmt ? fmt(r[key]) : r[key]) + "</td>";
|
|
270
|
+
const rowsHtml = all.map((r) =>
|
|
271
|
+
"<tr" + (r.role === "total" ? " class='hist-total'" : "") + ">" +
|
|
272
|
+
"<td class='mono'>" + esc(r.role === "total" ? r.role : roleLabel(r.role)) + "</td>" +
|
|
273
|
+
cell(r, "runs") + cell(r, "changes") + cell(r, "none") + cell(r, "refused") + cell(r, "problems") +
|
|
274
|
+
cell(r, "tokens", (n) => fmtTokens(Number(n))) + cell(r, "costUsd", (n) => fmtUsd(Number(n))) +
|
|
275
|
+
"<td class='num'>" + durCell(r.durationMs) + "</td>" +
|
|
276
|
+
"<td>" + esc(r.lastChangeTs === null || r.lastChangeTs === undefined ? "—" : fmtAgo(Number(r.lastChangeTs))) + "</td></tr>").join("");
|
|
277
|
+
return "<div class='table-wrap'><table class='table history'><thead><tr><th>Role</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>" +
|
|
278
|
+
rowsHtml + "</tbody></table></div>";
|
|
279
|
+
}
|
|
280
|
+
// history-summary:end
|
|
236
281
|
function renderHistory() {
|
|
237
282
|
const body = $("histbody");
|
|
238
283
|
if (!body) return;
|
|
239
284
|
markActive($("histfilter"), "hist", histFilter);
|
|
240
|
-
|
|
241
|
-
|
|
285
|
+
const sum = $("histsum");
|
|
286
|
+
if (sum) sum.style.display = "";
|
|
287
|
+
markActive(sum, "histsum", histMode);
|
|
288
|
+
// The outcome filter is meaningless in Summary mode (the summary already folds every class),
|
|
289
|
+
// so it is hidden there; only the body changes, keeping the same fetch path.
|
|
290
|
+
const filterEl = $("histfilter");
|
|
291
|
+
if (filterEl) filterEl.style.display = histMode === "summary" ? "none" : "";
|
|
292
|
+
// Summary mode shows the per-loop table when the fetch carried a summary over actual rows;
|
|
293
|
+
// otherwise (no rows, or a too-old server that answered without a summary) it falls back to
|
|
294
|
+
// the same empty state the table shows.
|
|
295
|
+
const showSummary = histMode === "summary" && histSummary && histRows && histRows.length > 0;
|
|
296
|
+
const bodyHtml = showSummary
|
|
297
|
+
? histSummaryHtml(histSummary)
|
|
298
|
+
: historyTableHtml(histRows, histFilter, new Set(((lastStatus && lastStatus.roles) || []).map((l) => l.role)), histGrep, histSinceKey);
|
|
299
|
+
body.innerHTML = historyNoteHtml(histNote) + bodyHtml;
|
|
242
300
|
}
|
|
243
301
|
`;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** The dashboard's browser-side twins of roleLabel and eventTypeLabel. The script is
|
|
3
|
+
* String.raw text and cannot import server modules, so the data is interpolated at module build
|
|
4
|
+
* and the few lines of logic are hand-kept copies, pinned against their TypeScript originals by
|
|
5
|
+
* test/gui-client-labels.test.ts. Spliced into gui-client.ts's script right after the format
|
|
6
|
+
* helpers, so every later client module can call the two functions. */
|
|
7
|
+
import { EVENT_TYPE_LABELS, RUN_RESULT_LABELS } from "../../events/event-labels.js";
|
|
8
|
+
import { INSTANCE_ID_SOURCE } from "../../roles/loop-ids.js";
|
|
9
|
+
export const GUI_CLIENT_LABELS_JS = String.raw ` // labels:start
|
|
10
|
+
const INSTANCE_ID = new RegExp(${JSON.stringify(INSTANCE_ID_SOURCE)});
|
|
11
|
+
const EVENT_TYPE_LABELS = ${JSON.stringify(EVENT_TYPE_LABELS)};
|
|
12
|
+
const RUN_RESULT_LABELS = ${JSON.stringify(RUN_RESULT_LABELS)};
|
|
13
|
+
const hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
14
|
+
function roleLabel(id) {
|
|
15
|
+
const m = INSTANCE_ID.exec(id);
|
|
16
|
+
return m ? m[1] + " #" + Number(m[2]) : id;
|
|
17
|
+
}
|
|
18
|
+
function eventTypeLabel(code) {
|
|
19
|
+
if (hasOwn(EVENT_TYPE_LABELS, code)) return EVENT_TYPE_LABELS[code];
|
|
20
|
+
if (hasOwn(RUN_RESULT_LABELS, code)) return RUN_RESULT_LABELS[code];
|
|
21
|
+
return String(code).replace(/_/g, " ");
|
|
22
|
+
}
|
|
23
|
+
// labels:end
|
|
24
|
+
`;
|
|
@@ -1,15 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** The dashboard's loops table, browser-side — the team's rows grouped by what the loops are
|
|
3
|
+
* doing (working, merge, needs-attention, paused, idle), each with its status pill, current
|
|
3
4
|
* work, spend, and per-row operator controls (prompt, wake, pause/resume, and the two-click
|
|
4
5
|
* abort), patched cell by cell so a ticking row keeps focus and hover. renderLoops repaints it
|
|
5
|
-
* from each /api/status poll;
|
|
6
|
+
* from each /api/status poll; renderTeam (gui-client-team.ts) calls it. Spliced into
|
|
6
7
|
* gui-client.ts's script, reaching its helpers (esc, icon, phaseInfo, sortLoops, postAction,
|
|
7
8
|
* setTarget, focusComposer, rerender, …) through that concatenation. */
|
|
8
9
|
export const GUI_CLIENT_LOOPS_JS = String.raw `
|
|
9
10
|
// ---- loops ----
|
|
10
11
|
// row-actions:start
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
// A two-click confirm, the confirmSlot shape the drawer's note-clear/entry-cancel also use:
|
|
13
|
+
// the first click stores a { id, title?, until } record and repaints; armed() fires a second
|
|
14
|
+
// click inside the 5 s window; a 5100 ms auto-disarm drops an unconfirmed record past its
|
|
15
|
+
// expiry. Defined here (the earliest consumer splice) so the loops' abort in this file, an
|
|
16
|
+
// earlier splice, and the drawer's notebook-clear/entry-cancel in a later splice all share
|
|
17
|
+
// one arm, guard, and disarm instead of three copies.
|
|
18
|
+
function confirmSlot(repaint) {
|
|
19
|
+
let confirm = null; // { id, title?, until }
|
|
20
|
+
return {
|
|
21
|
+
armed: (id, title) => Boolean(confirm && confirm.id === id && (title === undefined || confirm.title === title) && confirm.until > Date.now()),
|
|
22
|
+
disarm: () => { confirm = null; },
|
|
23
|
+
arm(id, title) {
|
|
24
|
+
confirm = { id, title, until: Date.now() + 5000 };
|
|
25
|
+
repaint();
|
|
26
|
+
setTimeout(() => { if (confirm && confirm.id === id && confirm.until <= Date.now()) { confirm = null; repaint(); } }, 5100);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// The abort slot: keyed by role, so arming is per loop — a second click on the same loop's
|
|
31
|
+
// abort fires, while a first click on another loop's abort re-arms instead of posting.
|
|
32
|
+
const confirmAbortSlot = confirmSlot(rerender);
|
|
33
|
+
const abortConfirming = (role) => confirmAbortSlot.armed(role);
|
|
34
|
+
// The drawer's Disable slot: a second click on the same role's Disable fires, a first click
|
|
35
|
+
// on another role's re-arms — the same per-role keying as the abort slot it sits beside.
|
|
36
|
+
const confirmDisableSlot = confirmSlot(rerender);
|
|
37
|
+
const disableConfirming = (role) => confirmDisableSlot.armed(role);
|
|
13
38
|
let roleChooser = null; // { role, action, until }: the one open duration chooser ("pause-for" or "wake-in")
|
|
14
39
|
const roleChooserOpen = (role, action) => Boolean(roleChooser && roleChooser.role === role && roleChooser.action === action && roleChooser.until > Date.now());
|
|
15
40
|
const rolePauseMenuOpen = (role) => roleChooserOpen(role, "pause-for");
|
|
@@ -26,9 +51,11 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
|
|
|
26
51
|
return;
|
|
27
52
|
}
|
|
28
53
|
if (action === "abort" && !abortConfirming(role)) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
54
|
+
confirmAbortSlot.arm(role);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (action === "disable" && !disableConfirming(role)) {
|
|
58
|
+
confirmDisableSlot.arm(role);
|
|
32
59
|
return;
|
|
33
60
|
}
|
|
34
61
|
if ((action === "pause-for" || action === "wake-in") && forSeconds === undefined) {
|
|
@@ -36,20 +63,27 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
|
|
|
36
63
|
rerender();
|
|
37
64
|
return;
|
|
38
65
|
}
|
|
39
|
-
|
|
66
|
+
confirmAbortSlot.disarm();
|
|
67
|
+
confirmDisableSlot.disarm();
|
|
40
68
|
roleChooser = null;
|
|
41
69
|
const pausing = action === "pause" || action === "pause-for";
|
|
42
70
|
const timed = action === "pause-for" && forSeconds > 0;
|
|
43
|
-
const path = action === "abort" ? "/api/abort" : action === "reset-counters" ? "/api/reset-counters" : action === "wake-cancel" ? "/api/wake-cancel" : pausing || action === "resume" ? "/api/pause-role" : "/api/wake";
|
|
71
|
+
const path = action === "abort" ? "/api/abort" : action === "disable" ? "/api/role-disable" : action === "reset-counters" ? "/api/reset-counters" : action === "wake-cancel" ? "/api/wake-cancel" : pausing || action === "resume" ? "/api/pause-role" : "/api/wake";
|
|
44
72
|
const wakeIn = action === "wake-in";
|
|
45
73
|
const body = pausing || action === "resume" ? { role, paused: pausing, ...(timed ? { forSeconds } : {}) } : wakeIn ? { role, inSeconds: forSeconds } : { role };
|
|
46
74
|
await postAction(path, body, (d) => {
|
|
75
|
+
if (action === "disable") {
|
|
76
|
+
// The role drops out of d.roles on the next poll, so without closing it the open
|
|
77
|
+
// drawer would fall to its "no longer part of the team" head.
|
|
78
|
+
closeDrawer();
|
|
79
|
+
return d && typeof d.message === "string" ? d.message : role + ": done";
|
|
80
|
+
}
|
|
47
81
|
let msg = d && typeof d.message === "string" ? d.message : action === "wake-cancel" ? role + ": scheduled wake cancelled" : role + ": done";
|
|
48
82
|
if (wakeIn && !(d && typeof d.message === "string")) msg = role + " wakes in " + humanSeconds(forSeconds);
|
|
49
83
|
else if (timed) msg = role + " paused for " + humanSeconds(forSeconds);
|
|
50
84
|
else if (pausing || action === "resume") {
|
|
51
85
|
msg = role + (d && d.changed
|
|
52
|
-
? (pausing ? " paused — it starts no new
|
|
86
|
+
? (pausing ? " paused — it starts no new runs" : " resumed")
|
|
53
87
|
: (pausing ? " was already paused" : " was not paused"));
|
|
54
88
|
}
|
|
55
89
|
return msg;
|
|
@@ -72,77 +106,85 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
|
|
|
72
106
|
const detail = phaseDetailBase(l, info, d);
|
|
73
107
|
const w = scheduledWakeFor(l, d);
|
|
74
108
|
if (!w) return detail;
|
|
75
|
-
return (detail ? detail + " · " : "") + "
|
|
109
|
+
return (detail ? detail + " · " : "") + "your wake in " + humanSeconds(Math.max(0, Math.round((w.atMs - Date.now()) / 1000)));
|
|
76
110
|
}
|
|
77
111
|
function phaseDetailBase(l, info, d) {
|
|
78
112
|
const raw = fmtNextRun(l, d.running);
|
|
79
113
|
const slowed = / ×(\d+)$/.exec(raw);
|
|
80
114
|
const next = slowed ? raw.slice(0, slowed.index) : raw;
|
|
81
|
-
const backoff = next.startsWith("
|
|
82
|
-
// A yield-scaled clock: the loop's recent ticks
|
|
83
|
-
const note = slowed ? " · slowed ×" + slowed[1] + ", nothing
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
const backoff = next.startsWith("sleeping ") ? "sleeping — next check in " + next.slice(9) : "";
|
|
116
|
+
// A yield-scaled clock: the loop's recent ticks merged nothing, so it ticks less often.
|
|
117
|
+
const note = slowed ? " · slowed ×" + slowed[1] + ", nothing merged lately" : "";
|
|
118
|
+
// The consecutive no-change streak (status-payload noChangeStreak, mirrored from
|
|
119
|
+
// status-model loopPhase's · no change ×N suffix): how many completed ticks in a row
|
|
120
|
+
// returned nothing to do. Shown only on a resting idle cell (sleeping/queued/idle/
|
|
121
|
+
// deferred) — never on live work, a pause, a failure, or main-red, which the status
|
|
122
|
+
// label likewise leaves bare. Observational only: a legitimately idle maintenance loop
|
|
123
|
+
// can hold a long streak.
|
|
124
|
+
const noChange = ["sleeping", "queued", "idle", "deferred"].includes(info.key) && (l.noChangeStreak || 0) >= 2
|
|
125
|
+
? " · no change ×" + l.noChangeStreak : "";
|
|
126
|
+
if (info.key === "sleeping") return (backoff || (next === "-" ? "" : next === "now" ? "wakes now" : "wakes in " + next)) + note + noChange;
|
|
127
|
+
if (info.key === "queued") return (backoff || info.detail) + note + noChange;
|
|
128
|
+
if (info.key === "paused" && d.pausedRolesUntil && !d.paused && (d.pausedLoops || []).includes(l.role)) return info.detail + " · resumes in " + humanSeconds(Math.max(0, Math.round((d.pausedRolesUntil - Date.now()) / 1000)));
|
|
129
|
+
return info.detail + noChange;
|
|
88
130
|
}
|
|
89
131
|
function loopCells(l, d) {
|
|
90
132
|
const info = phaseInfo(l.phase);
|
|
91
|
-
const paused = (d.
|
|
133
|
+
const paused = (d.pausedLoops || []).includes(l.role);
|
|
92
134
|
const queued = (d.roleInbox || {})[l.role] || 0;
|
|
93
135
|
const detail = phaseDetail(l, info, d);
|
|
94
136
|
const stalled = info.live && STALL.test(l.phase);
|
|
95
|
-
const name = "<div class='loop-name'><button type='button' class='linkish role' data-open='" + esc(l.role) + "' title='Open " + esc(l.role) + "'>" + esc(l.role) + "</button>" +
|
|
96
|
-
(l.custom ? "<span class='tag' title='A
|
|
97
|
-
(l.modelTier ? "<span class='tag' title='The
|
|
137
|
+
const name = "<div class='loop-name'><button type='button' class='linkish role' data-open='" + esc(l.role) + "' title='Open " + esc(roleLabel(l.role)) + "'>" + esc(roleLabel(l.role)) + "</button>" + // vocabulary-ok: CSS class the stylesheet and tests match
|
|
138
|
+
(l.custom ? "<span class='tag' title='A custom role from tumwater.json'>custom</span>" : "") +
|
|
139
|
+
(l.modelTier ? "<span class='tag' title='The model tier this role resolves at'>" + esc(l.modelTier) + "</span>" : "") +
|
|
98
140
|
(l.fallback ? "<span class='tag t-amber' title='Running on the model fallback while its primary keeps failing provider-class errors'>fallback</span>" : "") +
|
|
99
|
-
(queued ? "<span class='tag t-indigo' title='" + esc(plural(queued, "
|
|
100
|
-
"</div><div class='sub'>" + esc(plural(l.commits, "commit") + " · " + plural(l.
|
|
141
|
+
(queued ? "<span class='tag t-indigo' title='" + esc(plural(queued, "message") + " in this role's inbox") + "'>" + icon("chat") + queued + "</span>" : "") +
|
|
142
|
+
"</div><div class='sub'>" + esc(plural(l.commits, "commit") + " · " + plural(l.runs, "run") + " · merged " + (l.lastMergedAt ? fmtAgo(l.lastMergedAt) : "never")) + (l.model ? " · " + esc(l.model) : "") +
|
|
143
|
+
(l.modelFromPi ? " <span class='tag' title='pi's own default model, from its settings.json: tumwater.json names no model'>(pi default)</span>" : "") + "</div>";
|
|
101
144
|
const status = pill(info) + (detail ? "<div class='sub" + (stalled ? " t-red" : "") + "' title='" + esc(l.phase) + "'>" + esc(detail) + "</div>" : "");
|
|
102
145
|
let activity;
|
|
103
|
-
if (info.live || info.key === "vetted") {
|
|
104
|
-
// Parallel work instances, part 6/7: while idle or
|
|
105
|
-
// claimed before the
|
|
106
|
-
const text = l.currentWork || l.claim ||
|
|
107
|
-
const pending = info.key === "working" ? "Starting up…" : info.key === "reviewing" ? "Reviewing the change…" : "
|
|
146
|
+
if (info.live || info.key === "vetted") { // vocabulary-ok: internal phase key
|
|
147
|
+
// Parallel work instances, part 6/7: while idle or merge, show the entry the instance
|
|
148
|
+
// claimed before the merge summary placeholder.
|
|
149
|
+
const text = l.currentWork || l.claim || mergeSummary(d, l.role);
|
|
150
|
+
const pending = info.key === "working" ? "Starting up…" : info.key === "reviewing" ? "Reviewing the change…" : "Merging the change…";
|
|
108
151
|
activity = text ? "<div class='now clamp2' title='" + esc(text) + "'>" + esc(text) + "</div>" : "<div class='muted'>" + pending + "</div>";
|
|
109
|
-
} else if (l.claim
|
|
110
|
-
// An idle instance holding a claim names its entry;
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
const text = l.claim || ("idle — no unclaimed " + (l.instanceOf === "bugfix" ? "bugs" : "plans"));
|
|
152
|
+
} else if (l.claim) {
|
|
153
|
+
// An idle instance holding a claim names its entry; the pill detail carries why an
|
|
154
|
+
// idle loop with none is idle.
|
|
155
|
+
const text = l.claim;
|
|
114
156
|
activity = "<div class='now clamp2' title='" + esc(text) + "'>" + esc(text) + "</div>";
|
|
115
157
|
} else if (l.lastResult) {
|
|
116
158
|
const r = resultInfo(l.lastResult);
|
|
117
159
|
const why = resultWhy(l);
|
|
118
160
|
activity = "<div class='clamp2' title='" + esc(why || r.label) + "'><span class='res t-" + r.tone + "'>" + esc(r.label) + "</span>" +
|
|
119
|
-
(why ? " <span class='now'>" + esc(why) + "</span>" : "") + "</div><div class='sub'>" + esc(fmtAgo(l.
|
|
120
|
-
} else activity = "<div class='muted'>No
|
|
161
|
+
(why ? " <span class='now'>" + esc(why) + "</span>" : "") + "</div><div class='sub'>" + esc(fmtAgo(l.lastRunEndedAt)) + "</div>";
|
|
162
|
+
} else activity = "<div class='muted'>No runs yet</div>";
|
|
121
163
|
const spent = "<div>" + esc(fmtUsd(l.todayUsd)) + "</div><div class='sub'>" + esc(fmtUsd(l.costUsd)) + " total</div>";
|
|
122
164
|
const b = (action, ic, title, cls) => "<button type='button' class='icon-btn rowaction" + (cls ? " " + cls : "") + "' data-action='" + action +
|
|
123
165
|
"' data-role='" + esc(l.role) + "' title='" + esc(title) + "' aria-label='" + esc(title) + "'>" + icon(ic) + "</button>";
|
|
124
|
-
let actions = b("prompt", "chat", "
|
|
166
|
+
let actions = b("prompt", "chat", "Message " + roleLabel(l.role));
|
|
125
167
|
// The running set (info.live), not the permit-holder inFlight: the exempt running director
|
|
126
168
|
// has an abortable tick without holding a maxConcurrent permit (BUGS.md 2026-10-06).
|
|
127
169
|
if (info.live) {
|
|
128
170
|
actions += abortConfirming(l.role)
|
|
129
|
-
? "<button type='button' class='icon-btn rowaction confirming' data-action='abort' data-role='" + esc(l.role) + "' title='Click again to
|
|
130
|
-
: b("abort", "stop", "
|
|
131
|
-
} else actions += b("wake", "bolt", "Wake " + l.role + " now");
|
|
132
|
-
if (paused) actions += b("resume", "play", "Resume " + l.role);
|
|
171
|
+
? "<button type='button' class='icon-btn rowaction confirming' data-action='abort' data-role='" + esc(l.role) + "' title='Click again to stop this run'>" + icon("stop") + "Stop?</button>"
|
|
172
|
+
: b("abort", "stop", "Stop " + roleLabel(l.role) + "'s run", "danger");
|
|
173
|
+
} else actions += b("wake", "bolt", "Wake " + roleLabel(l.role) + " now");
|
|
174
|
+
if (paused) actions += b("resume", "play", "Resume " + roleLabel(l.role));
|
|
133
175
|
else if (rolePauseMenuOpen(l.role)) {
|
|
134
176
|
actions += ROLE_PAUSE_CHOICES.map((c) => "<button type='button' class='icon-btn rowaction' data-action='pause-for' data-for='" + c[0] +
|
|
135
|
-
"' data-role='" + esc(l.role) + "' title='Pause " + esc(l.role) + " " + (c[0] ? "for " + c[1] : "until resumed") + "'>" + esc(c[1]) + "</button>").join("");
|
|
136
|
-
} else actions += b("pause", "pause", "Pause " + l.role) + b("pause-for", "clock", "Pause " + l.role + " for a duration");
|
|
177
|
+
"' data-role='" + esc(l.role) + "' title='Pause " + esc(roleLabel(l.role)) + " " + (c[0] ? "for " + c[1] : "until resumed") + "'>" + esc(c[1]) + "</button>").join("");
|
|
178
|
+
} else actions += b("pause", "pause", "Pause " + roleLabel(l.role)) + b("pause-for", "clock", "Pause " + roleLabel(l.role) + " for a duration");
|
|
137
179
|
return { info, cells: [name, status, activity, spent, "<div class='actions'>" + actions + "</div>"] };
|
|
138
180
|
}
|
|
139
181
|
// loops-table:end
|
|
140
|
-
const LOOP_TDS = ["c-loop", "c-status", "c-activity", "c-today num", "c-actions"];
|
|
182
|
+
const LOOP_TDS = ["c-loop", "c-status", "c-activity", "c-today num", "c-actions"]; // vocabulary-ok: CSS class the stylesheet and tests match
|
|
141
183
|
let loopKeys = "";
|
|
142
184
|
// Rows are patched cell by cell: a working loop's elapsed time changes every second, and
|
|
143
185
|
// rewriting only that cell keeps focus and hover on the row's buttons.
|
|
144
186
|
function patchLoops(rows) {
|
|
145
|
-
const tbody = $("
|
|
187
|
+
const tbody = $("rolerows");
|
|
146
188
|
const keys = rows.map((r) => r.key).join("|");
|
|
147
189
|
if (keys !== loopKeys) {
|
|
148
190
|
loopKeys = keys;
|
|
@@ -163,7 +205,7 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
|
|
|
163
205
|
function renderLoops(d) {
|
|
164
206
|
$("loopstable").classList.toggle("free", Boolean(d.budget && d.budget.free));
|
|
165
207
|
const byGroup = LOOP_GROUPS.map(() => []);
|
|
166
|
-
for (const l of sortLoops(d.
|
|
208
|
+
for (const l of sortLoops(d.roles || [])) {
|
|
167
209
|
const rank = loopRank(l.phase);
|
|
168
210
|
byGroup[LOOP_GROUPS.findIndex((g) => g.slice(1).includes(rank))].push(l);
|
|
169
211
|
}
|
|
@@ -174,13 +216,13 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
|
|
|
174
216
|
rows.push({ key: "g" + gi, cls: "group", group: true, cells: [esc(g[0]) + "<span class='badge'>" + byGroup[gi].length + "</span>"] });
|
|
175
217
|
for (const l of byGroup[gi]) {
|
|
176
218
|
const { info, cells } = loopCells(l, d);
|
|
177
|
-
rows.push({ key: "l:" + l.role, role: l.role, cls: "loop clickable" + (info.live ? " live" : "") + (selected === l.role ? " selected" : ""), cells });
|
|
219
|
+
rows.push({ key: "l:" + l.role, role: l.role, cls: "loop clickable" + (info.live ? " live" : "") + (selected === l.role ? " selected" : ""), cells }); // vocabulary-ok: CSS class the stylesheet and tests match
|
|
178
220
|
}
|
|
179
221
|
});
|
|
180
|
-
if (!rows.length) rows.push({ key: "empty", cls: "group", group: true, cells: ["No
|
|
222
|
+
if (!rows.length) rows.push({ key: "empty", cls: "group", group: true, cells: ["No roles are enabled in tumwater.json"] });
|
|
181
223
|
patchLoops(rows);
|
|
182
224
|
}
|
|
183
|
-
$("
|
|
225
|
+
$("rolerows").addEventListener("click", (ev) => {
|
|
184
226
|
const b = clickClosest(ev, "button.rowaction");
|
|
185
227
|
if (b) { ev.preventDefault(); rowAction(b.dataset.action, b.dataset.role, b.dataset.for === undefined ? undefined : Number(b.dataset.for)); return; }
|
|
186
228
|
const tr = clickClosest(ev, "tr[data-role]");
|
|
@@ -189,14 +231,14 @@ export const GUI_CLIENT_LOOPS_JS = String.raw `
|
|
|
189
231
|
$("resetall").addEventListener("click", () =>
|
|
190
232
|
postAction("/api/reset-counters", {}, (d) => (d && typeof d.message === "string" ? d.message : "counters reset")));
|
|
191
233
|
$("wakeall").addEventListener("click", () =>
|
|
192
|
-
postAction("/api/wake", {}, (d) => (d && typeof d.message === "string" ? d.message : "every
|
|
234
|
+
postAction("/api/wake", {}, (d) => (d && typeof d.message === "string" ? d.message : "every role woken")));
|
|
193
235
|
// wake-later:start
|
|
194
236
|
const WAKE_LATER_CHOICES = [[1800, "In 30 minutes"], [3600, "In 1 hour"], [7200, "In 2 hours"], [28800, "In 8 hours"]];
|
|
195
237
|
function openWakeLaterMenu() {
|
|
196
238
|
openPopover("wakelaterwrap", {
|
|
197
239
|
menu: true,
|
|
198
240
|
role: "menu",
|
|
199
|
-
html: "<div class='menu-title'>Wake every
|
|
241
|
+
html: "<div class='menu-title'>Wake every role</div>" +
|
|
200
242
|
WAKE_LATER_CHOICES.map((c) => "<button type='button' class='menu-item' role='menuitem' data-wakein='" + c[0] + "'>" +
|
|
201
243
|
icon("clock") + esc(c[1]) + "</button>").join("") +
|
|
202
244
|
"<button type='button' class='menu-item' role='menuitem' data-wakecancel='1'>" + icon("x") + "Cancel scheduled wakes</button>",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** A small, safe Markdown renderer for the dashboard, browser-side: the failure digest and the
|
|
2
3
|
* backlog entries (PLANS/BUGS/QUESTIONS.md bodies) are Markdown the loops write, and reading
|
|
3
4
|
* them as rendered headings, lists, and tables beats a pre-formatted text blob. It covers what
|