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,6 +1,7 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The dashboard's detail drawer, browser-side: a sheet on the right that opens over the page
|
|
2
3
|
* without losing it. For a loop it shows the state, the loop's controls (prompt, wake now or later,
|
|
3
|
-
* pause/resume, abort), what it is working on, its last result, the metrics the
|
|
4
|
+
* pause/resume, abort), what it is working on, its last result, the metrics the team table
|
|
4
5
|
* leaves out (tokens, context, cost, timings), its recent ticks (/api/history filtered to the
|
|
5
6
|
* loop), and its live transcript (/api/transcript, re-fetched on the 1 s poll and kept pinned
|
|
6
7
|
* to the bottom while the reader is there). For a backlog entry it shows the entry in full
|
|
@@ -12,10 +13,19 @@
|
|
|
12
13
|
export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind: "loop", role } | { kind: "entry", file, index } while open
|
|
13
14
|
let drawerTicks = null; // the open loop's recent history rows (null until fetched)
|
|
14
15
|
let drawerTicksAt = 0;
|
|
15
|
-
let drawerChange = null; // the open loop's
|
|
16
|
+
let drawerChange = null; // the open loop's unmerged change: { role, state: "loading"|"ok"|"error", view, message }
|
|
16
17
|
let drawerRole = null; // the open loop's /api/role payload: { role, state: "loading"|"ok"|"error", view, message }
|
|
17
18
|
const openEntryKey = () => (drawer && drawer.kind === "entry" ? drawer.file + ":" + drawer.index : null);
|
|
18
|
-
const openLoopRole = () => (drawer && drawer.kind === "loop" ? drawer.role : null);
|
|
19
|
+
const openLoopRole = () => (drawer && drawer.kind === "loop" ? drawer.role : null); // vocabulary-ok: the drawer kind stays "loop"
|
|
20
|
+
// One key/value row builder shared by the loop-metrics table (renderLoopDrawer) and the
|
|
21
|
+
// Configuration section (drawerConfigHtml): identical markup, formerly copy-pasted into two
|
|
22
|
+
// local consts (kv and row) so the row shape could drift between the two sections.
|
|
23
|
+
const kvRow = (k, v, title) => "<div><dt>" + esc(k) + "</dt><dd title='" + esc(title || v) + "'>" + esc(v) + "</dd></div>";
|
|
24
|
+
// A diff block in the Change section (the committed change and the dirty-tree uncommitted
|
|
25
|
+
// diff, drawerChangeHtml's two arms): trim the trailing newline the diff carriage-returned,
|
|
26
|
+
// escape the rest, and wrap it in the shared .diff pre. Formerly copy-pasted into both arms
|
|
27
|
+
// so the trim/escape could drift between them.
|
|
28
|
+
const diffPre = (text) => "<pre class='mono diff'>" + esc(String(text).replace(/\n+$/, "")) + "</pre>";
|
|
19
29
|
|
|
20
30
|
function showDrawer(bodyHtml) {
|
|
21
31
|
for (const id of ["drawerhead", "draweractions", "drawermeta", "drawerrole", "transcript", "entrybody"]) delete lastPaint[id];
|
|
@@ -29,8 +39,14 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
29
39
|
drawer = null;
|
|
30
40
|
entryEditing = null;
|
|
31
41
|
noteEditing = null;
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
capEditing = null;
|
|
43
|
+
instructionsEditing = null;
|
|
44
|
+
quietEditing = null;
|
|
45
|
+
intervalEditing = null;
|
|
46
|
+
instancesEditing = null;
|
|
47
|
+
modelEditing = null;
|
|
48
|
+
disarmNoteClear();
|
|
49
|
+
if (wasLoop && (location.hash.startsWith("#role/") || location.hash.startsWith("#loop/"))) history.replaceState(null, "", "#" + activeView); // vocabulary-ok: legacy dashboard hash, still routed
|
|
34
50
|
drawerTicks = null;
|
|
35
51
|
drawerChange = null;
|
|
36
52
|
drawerRole = null;
|
|
@@ -39,12 +55,12 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
39
55
|
$("drawerhead").innerHTML = "";
|
|
40
56
|
$("drawerbody").innerHTML = "";
|
|
41
57
|
for (const id of ["drawerhead", "draweractions", "drawermeta", "drawerrole", "transcript", "entrybody"]) delete lastPaint[id];
|
|
42
|
-
if (lastStatus)
|
|
58
|
+
if (lastStatus) renderTeam(lastStatus); // drop the row highlight
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
function openLoop(role) {
|
|
46
|
-
drawer = { kind: "loop", role };
|
|
47
|
-
const hash = "#
|
|
62
|
+
drawer = { kind: "loop", role }; // vocabulary-ok: the drawer kind stays "loop"
|
|
63
|
+
const hash = "#role/" + encodeURIComponent(role);
|
|
48
64
|
if (location.hash !== hash) history.replaceState(null, "", hash);
|
|
49
65
|
drawerTicks = null;
|
|
50
66
|
drawerTicksAt = 0;
|
|
@@ -55,7 +71,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
55
71
|
"<pre id='transcript' class='transcript'><span class='muted'>Loading…</span></pre></section>");
|
|
56
72
|
if (lastStatus) {
|
|
57
73
|
renderLoopDrawer(lastStatus);
|
|
58
|
-
|
|
74
|
+
renderTeam(lastStatus);
|
|
59
75
|
}
|
|
60
76
|
loadLoopTicks(true);
|
|
61
77
|
loadDrawerChange(role);
|
|
@@ -70,16 +86,16 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
70
86
|
drawer = { kind: "entry", file, index: Number(index) };
|
|
71
87
|
entryEditing = null;
|
|
72
88
|
showDrawer("<div id='entrybody'><div class='empty'>Loading…</div></div>");
|
|
73
|
-
if (lastStatus)
|
|
89
|
+
if (lastStatus) renderTeam(lastStatus);
|
|
74
90
|
refreshEntry();
|
|
75
91
|
}
|
|
76
92
|
function toggleEntry(file, index) {
|
|
77
93
|
if (openEntryKey() === file + ":" + index) closeDrawer();
|
|
78
94
|
else openEntry(file, index);
|
|
79
95
|
}
|
|
80
|
-
// The change the
|
|
81
|
-
function
|
|
82
|
-
const f = d.
|
|
96
|
+
// The change the merge slot holds for a role — what a merge (or approved) row is about.
|
|
97
|
+
function mergeSummary(d, role) {
|
|
98
|
+
const f = d.mergeQueue && d.mergeQueue.inFlight;
|
|
83
99
|
if (!f) return "";
|
|
84
100
|
const change = (Array.isArray(f.changes) ? f.changes : [f]).find((c) => c.role === role);
|
|
85
101
|
return change && change.summary ? change.summary : "";
|
|
@@ -88,35 +104,36 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
88
104
|
function renderLoopDrawer(d) {
|
|
89
105
|
const role = openLoopRole();
|
|
90
106
|
if (!role) return;
|
|
91
|
-
const l = (d.
|
|
107
|
+
const l = (d.roles || []).find((x) => x.role === role);
|
|
92
108
|
if (!l) {
|
|
93
|
-
paintPanel("drawerhead", "<div class='kicker'>
|
|
94
|
-
"</h2><span class='muted'>This
|
|
109
|
+
paintPanel("drawerhead", "<div class='kicker'>Role</div><h2 id='drawertitle' class='mono'>" + esc(roleLabel(role)) +
|
|
110
|
+
"</h2><span class='muted'>This role is no longer part of the team.</span>");
|
|
95
111
|
paintPanel("draweractions", "");
|
|
96
112
|
paintPanel("drawermeta", drawerChangeSection());
|
|
97
113
|
paintPanel("drawerrole", drawerRoleSection());
|
|
98
114
|
return;
|
|
99
115
|
}
|
|
100
116
|
const info = phaseInfo(l.phase);
|
|
101
|
-
const paused = (d.
|
|
117
|
+
const paused = (d.pausedLoops || []).includes(l.role);
|
|
102
118
|
const detail = phaseDetail(l, info, d);
|
|
103
|
-
paintPanel("drawerhead", "<div class='kicker'>
|
|
104
|
-
"<h2 id='drawertitle' class='mono'>" + esc(l.role) + "</h2><div class='chips'>" + pill(info) +
|
|
119
|
+
paintPanel("drawerhead", "<div class='kicker'>Role" + (l.custom ? " · user-defined" : "") + "</div>" +
|
|
120
|
+
"<h2 id='drawertitle' class='mono'>" + esc(roleLabel(l.role)) + "</h2><div class='chips'>" + pill(info) +
|
|
105
121
|
(detail ? "<span class='muted'>" + esc(detail) + "</span>" : "") + "</div>");
|
|
106
122
|
|
|
107
123
|
const btn = (action, ic, label, cls) => "<button type='button' class='btn btn-sm rowaction" + (cls ? " " + cls : "") + "' data-action='" + action +
|
|
108
124
|
"' data-role='" + esc(l.role) + "'>" + icon(ic) + esc(label) + "</button>";
|
|
109
|
-
let actions = btn("prompt", "chat", "
|
|
125
|
+
let actions = btn("prompt", "chat", "Message this role");
|
|
110
126
|
// The running set (info.live), not the permit-holder inFlight: the exempt running director
|
|
111
127
|
// has an abortable tick without holding a maxConcurrent permit (BUGS.md 2026-10-06).
|
|
112
|
-
if (info.live) actions += abortConfirming(l.role) ? btn("abort", "stop", "Click again to
|
|
128
|
+
if (info.live) actions += abortConfirming(l.role) ? btn("abort", "stop", "Click again to stop", "btn-danger confirming") : btn("abort", "stop", "Stop this run", "btn-danger");
|
|
113
129
|
else {
|
|
114
130
|
actions += btn("wake", "bolt", "Wake now");
|
|
115
131
|
if (roleWakeMenuOpen(l.role)) {
|
|
116
132
|
actions += ROLE_WAKE_CHOICES.map((c) => "<button type='button' class='btn btn-sm rowaction' data-action='wake-in' data-for='" + c[0] +
|
|
117
133
|
"' data-role='" + esc(l.role) + "'>in " + esc(c[1]) + "</button>").join("");
|
|
118
134
|
} else actions += btn("wake-in", "clock", "Wake later…");
|
|
119
|
-
// Exact id only: the cancel matches ids exactly, so an instance named via its base role
|
|
135
|
+
// Exact id only: the cancel matches ids exactly, so an instance named via its base role
|
|
136
|
+
// would cancel nothing.
|
|
120
137
|
if (scheduledWakesFor(l, d).some((w) => w.roles.includes(l.role))) actions += btn("wake-cancel", "x", "Cancel scheduled wake");
|
|
121
138
|
}
|
|
122
139
|
if (paused) actions += btn("resume", "play", "Resume");
|
|
@@ -125,44 +142,47 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
125
142
|
"' data-role='" + esc(l.role) + "'>" + esc(c[1]) + "</button>").join("");
|
|
126
143
|
} else actions += btn("pause", "pause", "Pause") + btn("pause-for", "clock", "Pause for…");
|
|
127
144
|
actions += btn("reset-counters", "refresh", "Reset counters");
|
|
145
|
+
// The two-click Disable, the drawer half of the enable/disable pair (the Settings card's
|
|
146
|
+
// Enable re-enables). The director cannot be disabled, so it shows no Disable button.
|
|
147
|
+
if (l.role !== "director") actions += disableConfirming(l.role) ? btn("disable", "x", "Click again to disable", "btn-danger confirming") : btn("disable", "x", "Disable", "btn-danger");
|
|
128
148
|
|
|
129
|
-
const working = info.live || info.key === "vetted";
|
|
149
|
+
const working = info.live || info.key === "vetted"; // vocabulary-ok: the persisted phase key
|
|
130
150
|
const now = working
|
|
131
|
-
? "<section><div class='sec-head'><h3>" + (info.key === "working" ? "Working on" : info.key === "reviewing" ? "Under review" : "
|
|
132
|
-
"</h3></div><div class='note'>" + esc(l.currentWork ||
|
|
151
|
+
? "<section><div class='sec-head'><h3>" + (info.key === "working" ? "Working on" : info.key === "reviewing" ? "Under review" : "Merging") +
|
|
152
|
+
"</h3></div><div class='note'>" + esc(l.currentWork || mergeSummary(d, l.role) || (info.key === "working" ? "Starting up…" : "Waiting for its first words…")) + "</div></section>"
|
|
133
153
|
: "";
|
|
134
154
|
const r = l.lastResult ? resultInfo(l.lastResult) : null;
|
|
135
155
|
const why = resultWhy(l);
|
|
136
|
-
const last = "<section><div class='sec-head'><h3>Last result</h3>" + (l.
|
|
137
|
-
"</div><div class='note'>" + (r ? "<span class='res t-" + r.tone + "'>" + esc(r.label) + "</span>" + (why ? " — " + esc(why) : "") : "<span class='muted'>No completed
|
|
138
|
-
const kv = (k, v, title) => "<div><dt>" + esc(k) + "</dt><dd title='" + esc(title || v) + "'>" + esc(v) + "</dd></div>";
|
|
156
|
+
const last = "<section><div class='sec-head'><h3>Last result</h3>" + (l.lastRunEndedAt ? "<span class='muted' style='font-size:13px'>" + esc(fmtAgo(l.lastRunEndedAt)) + "</span>" : "") +
|
|
157
|
+
"</div><div class='note'>" + (r ? "<span class='res t-" + r.tone + "'>" + esc(r.label) + "</span>" + (why ? " — " + esc(why) : "") : "<span class='muted'>No completed runs yet.</span>") + "</div></section>";
|
|
139
158
|
const next = fmtNextRun(l, d.running);
|
|
140
159
|
// fmtNextRun's shapes: "-", "now" (possibly with a " ×N" yield suffix), and a bare or
|
|
141
160
|
// "backoff "-prefixed countdown. A backoff countdown keeps the label but as a suffix;
|
|
142
161
|
// a suffixed "now" passes through as-is — it is due now, not a future time.
|
|
143
162
|
const nextNote = next === "-" ? "—"
|
|
144
163
|
: next === "now" || next.startsWith("now ") ? next
|
|
145
|
-
: next.startsWith("
|
|
164
|
+
: next.startsWith("sleeping ") ? "in " + next.slice(9) + " (sleeping)"
|
|
146
165
|
: "in " + next;
|
|
147
|
-
const tickWord = info.live ? "this
|
|
148
|
-
const stats = "<dl class='kv'>" +
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
const tickWord = info.live ? "this run" : "last run";
|
|
167
|
+
const stats = "<dl class='kv'>" + kvRow("Commits", String(l.commits)) + kvRow("Runs", String(l.runs)) +
|
|
168
|
+
kvRow("Tokens, " + tickWord, fmtTokens(l.generated)) + kvRow("Peak context, " + tickWord, fmtTokens(l.peakCtx)) +
|
|
169
|
+
kvRow("Spent today", fmtUsd(l.todayUsd)) + kvRow("Spent in total", fmtUsd(l.costUsd)) +
|
|
170
|
+
kvRow("Last run ended", l.lastRunEndedAt ? fmtAgo(l.lastRunEndedAt) : "never", fmtLastTick(l.lastRunEndedAt)) +
|
|
171
|
+
kvRow("Last merged", l.lastMergedAt ? fmtAgo(l.lastMergedAt) : "never") +
|
|
172
|
+
kvRow("Next run", nextNote) + "</dl>";
|
|
153
173
|
const ticks = drawerTicks === null
|
|
154
174
|
? "<p class='muted'>Loading…</p>"
|
|
155
175
|
: drawerTicks.length
|
|
156
|
-
? "<ul class='ticks'>" + drawerTicks.map((t) => {
|
|
176
|
+
? "<ul class='ticks'>" + drawerTicks.map((t) => { // vocabulary-ok: the ticks class stays
|
|
157
177
|
const res = resultInfo(t.result);
|
|
158
|
-
return "<li><span class='when' title='" + esc(t.time) + "'>#" + esc(t.
|
|
178
|
+
return "<li><span class='when' title='" + esc(t.time) + "'>#" + esc(t.run) + " · " + esc(fmtAgo(t.ts)) + "</span>" +
|
|
159
179
|
"<span class='clamp1' title='" + esc(t.detail) + "'><span class='res t-" + res.tone + "'>" + esc(res.label) + "</span> " + esc(t.detail) + "</span>" +
|
|
160
180
|
"<span class='when'>" + (t.durationMs === null ? "" : esc(fmtSpan(Number(t.durationMs)))) + "</span></li>";
|
|
161
181
|
}).join("") + "</ul>"
|
|
162
|
-
: "<p class='muted'>No completed
|
|
182
|
+
: "<p class='muted'>No completed runs in the log yet.</p>";
|
|
163
183
|
paintPanel("draweractions", actions);
|
|
164
184
|
paintPanel("drawermeta", now + last + stats +
|
|
165
|
-
"<section><div class='sec-head'><h3>Recent
|
|
185
|
+
"<section><div class='sec-head'><h3>Recent runs</h3></div>" + ticks + "</section>" + drawerChangeSection());
|
|
166
186
|
// The notebook/prompt live in their own panel, outside drawermeta: the poll rewrites
|
|
167
187
|
// drawermeta whenever its countdown or tick-age text changes, and an innerHTML rewrite
|
|
168
188
|
// would collapse an opened Next tick prompt. This panel's markup is stable while its
|
|
@@ -170,7 +190,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
170
190
|
paintPanel("drawerrole", drawerRoleSection());
|
|
171
191
|
}
|
|
172
192
|
|
|
173
|
-
// The open loop's
|
|
193
|
+
// The open loop's unmerged change, as /api/diff?role serves it: the full patch the CLI's
|
|
174
194
|
// tumwater diff --role prints. Degrades like the collector — a missing worktree, a missing
|
|
175
195
|
// base branch, or no work each get a plain statement, and an unreadable payload says so
|
|
176
196
|
// rather than rendering a half-view. Pure of side effects; the view paints what it returns.
|
|
@@ -178,17 +198,17 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
178
198
|
if (!view || typeof view !== "object") return "<p class='muted'>Pending change unavailable.</p>";
|
|
179
199
|
const commits = Array.isArray(view.commits) ? view.commits : [];
|
|
180
200
|
const dirty = Array.isArray(view.dirtyFiles) ? view.dirtyFiles : [];
|
|
181
|
-
if (view.state === "absent") return "<p class='muted'>No
|
|
201
|
+
if (view.state === "absent") return "<p class='muted'>No workspace yet — this role has not run.</p>";
|
|
182
202
|
if (view.state === "no-base") return "<p class='muted'>Main branch " + esc(view.mainBranch) + " does not exist.</p>";
|
|
183
203
|
if (view.state !== "ready" || (view.ahead === 0 && dirty.length === 0)) return "<p class='muted'>No unlanded work.</p>";
|
|
184
204
|
let html = "<p class='note'>" + esc(view.branch) + " · " + esc(plural(view.ahead, "commit")) +
|
|
185
205
|
" ahead of " + esc(view.mainBranch) +
|
|
186
206
|
(dirty.length ? " · " + esc(plural(dirty.length, "uncommitted file")) : "") + "</p>";
|
|
187
207
|
html += commitRowsHtml(commits);
|
|
188
|
-
if (view.diff) html +=
|
|
208
|
+
if (view.diff) html += diffPre(view.diff);
|
|
189
209
|
if (dirty.length) {
|
|
190
210
|
html += "<p class='note'>Uncommitted: " + esc(dirty.join(", ")) + "</p>";
|
|
191
|
-
if (view.uncommittedDiff) html +=
|
|
211
|
+
if (view.uncommittedDiff) html += diffPre(view.uncommittedDiff);
|
|
192
212
|
}
|
|
193
213
|
return html;
|
|
194
214
|
}
|
|
@@ -218,7 +238,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
218
238
|
}
|
|
219
239
|
if (lastStatus) renderLoopDrawer(lastStatus);
|
|
220
240
|
}
|
|
221
|
-
// The open loop's
|
|
241
|
+
// The open loop's unmerged change, as /api/diff?role serves it.
|
|
222
242
|
function loadDrawerChange(role) {
|
|
223
243
|
return loadDrawerSection(role, "/api/diff?role=", "Pending change unavailable", (v) => { drawerChange = v; });
|
|
224
244
|
}
|
|
@@ -235,11 +255,12 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
235
255
|
const r = drawerRole;
|
|
236
256
|
let body;
|
|
237
257
|
if (!r || r.state === "loading") body = "<p class='muted'>Loading…</p>";
|
|
238
|
-
else if (r.state === "error") body = "<p class='muted'>" + esc(r.message || "
|
|
258
|
+
else if (r.state === "error") body = "<p class='muted'>" + esc(r.message || "Notes and prompt unavailable") + "</p>";
|
|
239
259
|
else {
|
|
240
260
|
const view = r.view && typeof r.view === "object" ? r.view : {};
|
|
241
|
-
const note = typeof view.
|
|
261
|
+
const note = typeof view.notes === "string" && view.notes.trim() !== "" ? view.notes : null;
|
|
242
262
|
const nextPrompt = typeof view.nextPrompt === "string" && view.nextPrompt.trim() !== "" ? view.nextPrompt : null;
|
|
263
|
+
const outcomes = Array.isArray(view.recentOutcomes) ? view.recentOutcomes : [];
|
|
243
264
|
const canEdit = r.role !== "director";
|
|
244
265
|
const editing = canEdit && noteEditing && noteEditing.role === r.role;
|
|
245
266
|
const buttons = !canEdit ? "" : "<span style='margin-left:auto'><button type='button' class='btn btn-sm' data-act='note-edit' data-arg='" + esc(r.role) + "'>Edit</button>" +
|
|
@@ -248,31 +269,267 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
248
269
|
? "<textarea id='noteedittext' rows='12' class='mono' style='width:100%'>" + esc(noteEditing.text) + "</textarea>" +
|
|
249
270
|
"<div><button type='button' class='btn btn-sm btn-primary' data-act='note-save' data-arg='" + esc(r.role) + "'>Save</button> " +
|
|
250
271
|
"<button type='button' class='btn btn-sm' data-act='note-discard' data-arg='" + esc(r.role) + "'>Discard</button></div>"
|
|
251
|
-
: note ? "<pre class='mono'>" + esc(note) + "</pre>" : "<p class='muted'>No
|
|
252
|
-
body = "<div class='sec-head'><h3>
|
|
253
|
-
"<details><summary>Next
|
|
272
|
+
: note ? "<pre class='mono'>" + esc(note) + "</pre>" : "<p class='muted'>No notes yet.</p>";
|
|
273
|
+
body = "<div class='sec-head'><h3>Notes</h3>" + (editing ? "" : buttons) + "</div>" + noteBody +
|
|
274
|
+
"<details><summary>Next run's prompt</summary>" + (nextPrompt ? "<pre class='mono'>" + esc(nextPrompt) + "</pre>" : "<p class='muted'>Nothing to run (empty inbox).</p>") + "</details>" +
|
|
275
|
+
// The loop's recent tick outcomes (rolePayload's recentOutcomes, read from the event
|
|
276
|
+
// log), rendered like the Recent ticks list above: each with its relative age, result
|
|
277
|
+
// label, summary, and duration. Mirrors the Recent tick outcomes section of
|
|
278
|
+
// tumwater role <id> — no backticks allowed here (this whole script is one big
|
|
279
|
+
// String.raw template), so the page and the CLI read one loop's outcomes identically.
|
|
280
|
+
(outcomes.length ? "<section><div class='sec-head'><h3>Recent outcomes" +
|
|
281
|
+
"<span class='muted' style='font-size:13px'>· " + outcomes.length + "</span></h3></div>" +
|
|
282
|
+
"<ul class='ticks'>" + outcomes.map((o) => { // vocabulary-ok: the ticks class stays
|
|
283
|
+
const info = resultInfo(o.result);
|
|
284
|
+
const detail = typeof o.summary === "string" && o.summary.trim() !== "" ? o.summary : "";
|
|
285
|
+
return "<li><span class='when' title='" + esc(new Date(Number(o.ts)).toLocaleString()) + "'>" + esc(fmtAgo(Number(o.ts))) + "</span>" +
|
|
286
|
+
"<span class='clamp1' title='" + esc(detail) + "'><span class='res t-" + info.tone + "'>" + esc(info.label) + "</span>" + (detail ? " " + esc(detail) : "") + "</span>" +
|
|
287
|
+
"<span class='when'>" + (o.durationMs == null ? "" : esc(fmtSpan(Number(o.durationMs)))) + "</span></li>";
|
|
288
|
+
}).join("") + "</ul></section>" : "") +
|
|
289
|
+
drawerConfigHtml(view, r.role);
|
|
254
290
|
}
|
|
255
291
|
return "<section>" + body + "</section>";
|
|
256
292
|
}
|
|
293
|
+
// A Configuration section built from the same /api/role payload the CLI's tumwater role
|
|
294
|
+
// <id> renders (rolePayload), so the drawer's and the terminal's rows read one document and
|
|
295
|
+
// cannot drift: what the loop RUNS ON (model/tier, provider cap, image reading, the
|
|
296
|
+
// fallback pairs, billing, min tick interval) and CARRIES (queued prompts, the instructions
|
|
297
|
+
// override, find text). Each row is guarded by a present/own check so an absent field
|
|
298
|
+
// renders no row — a cap-less team shows no Provider row, no fallback shows no fallback
|
|
299
|
+
// rows, and a notes-only payload adds an empty (omitted) section. Labels and wording mirror
|
|
300
|
+
// role-render.ts's family; no backticks allowed here (this whole script is one big
|
|
301
|
+
// String.raw template).
|
|
302
|
+
function drawerConfigHtml(view, role) {
|
|
303
|
+
const pair = (p, m) => [p, m].filter(Boolean).join("/");
|
|
304
|
+
const singleLine = (s) => String(s).replace(/\s+/g, " ").trim();
|
|
305
|
+
// A verbatim value's single-line clamped rendering (80 chars + ellipsis) with the full
|
|
306
|
+
// text riding the title tooltip — the same clamp+tooltip idiom the tick summaries use.
|
|
307
|
+
const clamp = (t) => { const one = singleLine(t); return one.length > 80 ? one.slice(0, 80) + "…" : one; };
|
|
308
|
+
const rows = [];
|
|
309
|
+
// A role that runs on pi's own saved default carries no provider/model but a piDefault
|
|
310
|
+
// selector (rolePayload's resolvedModelFields leaves both undefined), and the CLI's
|
|
311
|
+
// Model line names that default (<agent> default (<selector>), agentDefault). Mirror
|
|
312
|
+
// that branch so the drawer names the model a default pi team actually runs on.
|
|
313
|
+
const defaultLine = typeof view.piDefault === "string"
|
|
314
|
+
? (view.backend && view.backend !== "pi" ? view.backend : "pi") + " default (" + view.piDefault + ")"
|
|
315
|
+
: "";
|
|
316
|
+
const modelLine = defaultLine !== "" ? defaultLine : pair(view.provider, view.model);
|
|
317
|
+
if (modelLine !== "" || view.modelTier !== undefined) {
|
|
318
|
+
rows.push(kvRow("Model", (modelLine !== "" ? modelLine + " " : "") + (view.modelTier !== undefined ? "(" + view.modelTier + " tier)" : "").trim()));
|
|
319
|
+
}
|
|
320
|
+
// The per-role model triple (roles.<base>.provider/.model/.thinking, the file's own raw
|
|
321
|
+
// overrides rolePayload's providerOverride/modelOverride/thinkingOverride carry) — the
|
|
322
|
+
// one per-role write the Model row above still renders read-only, the dashboard half of
|
|
323
|
+
// the CLI's --provider/--model/--thinking. An always-shown row (every role, the director
|
|
324
|
+
// included, since the CLI writes its triple too) whose body names the payload's raw
|
|
325
|
+
// overrides, or no overrides, and whose control is a model-edit button (Edit/Set model);
|
|
326
|
+
// while the editor is open for this role, three text inputs prefilled with the raw
|
|
327
|
+
// overrides (empty when unset) plus Save/Discard replace the body, mirroring the interval
|
|
328
|
+
// editor's markup.
|
|
329
|
+
const rawTriple = [];
|
|
330
|
+
if (typeof view.providerOverride === "string" && view.providerOverride !== "") rawTriple.push("provider: " + view.providerOverride);
|
|
331
|
+
if (typeof view.modelOverride === "string" && view.modelOverride !== "") rawTriple.push("model: " + view.modelOverride);
|
|
332
|
+
if (typeof view.thinkingOverride === "string" && view.thinkingOverride !== "") rawTriple.push("thinking: " + view.thinkingOverride);
|
|
333
|
+
const modelBody = rawTriple.length ? rawTriple.join(" · ") : "no overrides";
|
|
334
|
+
const editModel = modelEditing && modelEditing.role === role;
|
|
335
|
+
const modelControl = editModel
|
|
336
|
+
? "<input id='modeleditprovider' type='text' placeholder='provider' value='" + esc(modelEditing.provider) + "'>" +
|
|
337
|
+
"<input id='modeleditmodel' type='text' placeholder='model' value='" + esc(modelEditing.model) + "'>" +
|
|
338
|
+
"<input id='modeleditthinking' type='text' placeholder='thinking' value='" + esc(modelEditing.thinking) + "'>" +
|
|
339
|
+
"<div><button type='button' class='btn btn-sm btn-primary' data-act='model-save' data-arg='" + esc(role) + "'>Save</button> " +
|
|
340
|
+
"<button type='button' class='btn btn-sm' data-act='model-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
|
|
341
|
+
: "<button type='button' class='btn btn-sm' data-act='model-edit' data-arg='" + esc(role) + "'>" + (rawTriple.length ? "Edit" : "Set model") + "</button>";
|
|
342
|
+
rows.push("<div><dt>Model overrides</dt><dd title='" + esc(modelBody) + "'>" + esc(modelBody) + modelControl + "</dd></div>");
|
|
343
|
+
// Context window: the resolved primary seam's declared window - the same seam '- Next run:'
|
|
344
|
+
// previews. Guard optional to keep an older payload that lacks the field rendering as before.
|
|
345
|
+
if (view.contextWindow !== undefined) {
|
|
346
|
+
rows.push(kvRow("Context window", view.contextWindow === null ? "unknown" : fmtTokens(view.contextWindow) + " tokens"));
|
|
347
|
+
}
|
|
348
|
+
if (view.providerKey !== undefined && view.providerCap !== undefined) {
|
|
349
|
+
rows.push(kvRow("Provider", view.providerKey + " (cap " + view.providerCap + ")"));
|
|
350
|
+
}
|
|
351
|
+
if (view.imageInput !== undefined) {
|
|
352
|
+
const images = view.imageInput === true ? "yes"
|
|
353
|
+
: view.imageInput === false ? (view.imageReroute ? "no — reruns on " + view.imageReroute : "no")
|
|
354
|
+
: "unknown";
|
|
355
|
+
rows.push(kvRow("Images", images));
|
|
356
|
+
}
|
|
357
|
+
if (view.modelFallback && typeof view.modelFallback === "object" && view.modelFallback.model !== undefined) {
|
|
358
|
+
const fb = view.modelFallback;
|
|
359
|
+
const when = typeof fb.since === "number" ? " since " + fmtAgo(fb.since) : "";
|
|
360
|
+
rows.push(kvRow("Model fallback", pair(fb.provider, fb.model) + " — on fallback" + when + " (primary failing: " + (fb.reason ?? "") + ")"));
|
|
361
|
+
}
|
|
362
|
+
if (view.fallback && typeof view.fallback === "object" && view.fallback.model !== undefined) {
|
|
363
|
+
rows.push(kvRow("Budget fallback", pair(view.fallback.provider, view.fallback.model) + " (" + (view.fallbackFree ? "free" : "priced") + ")"));
|
|
364
|
+
}
|
|
365
|
+
if (view.billing !== undefined) {
|
|
366
|
+
const window = view.billing === "subscription" && view.billingWindow ? " (" + view.billingWindow + " window)" : "";
|
|
367
|
+
rows.push(kvRow("Billing", (view.billing === "api" ? "API" : view.billing) + window));
|
|
368
|
+
}
|
|
369
|
+
// The min run interval (rolePayload's minRunIntervalSeconds), worded like role-render.ts's
|
|
370
|
+
// Min run interval line, plus a control to set/clear the per-role override — the last
|
|
371
|
+
// per-role hold the drawer edits. Shown whenever the payload names an interval (always —
|
|
372
|
+
// the effective value is the role's override or the team default); the body tags whether
|
|
373
|
+
// the shown value is the role's own override or the team default. While the editor is open
|
|
374
|
+
// for this role, an input prefilled with the override (empty when none) and Save/Discard
|
|
375
|
+
// replace the sentence, mirroring the daily-cap editor's markup; a role with no override
|
|
376
|
+
// still seeds an empty input, so an empty buffer on Save clears.
|
|
377
|
+
if (typeof view.minRunIntervalSeconds === "number") {
|
|
378
|
+
const editing = intervalEditing && intervalEditing.role === role;
|
|
379
|
+
const intervalBody = String(view.minRunIntervalSeconds) + "s" +
|
|
380
|
+
(typeof view.minRunIntervalOverride === "number" ? " (override)" : " (team default)");
|
|
381
|
+
const seed = intervalEditing ? intervalEditing.value : null;
|
|
382
|
+
const intervalControl = editing
|
|
383
|
+
? "<input id='intervaleditinput' type='number' min='0' step='1' value='" + (seed === "" || seed == null ? "" : String(seed)) + "' placeholder='team default'>" +
|
|
384
|
+
"<div><button type='button' class='btn btn-sm btn-primary' data-act='interval-save' data-arg='" + esc(role) + "'>Save</button> " +
|
|
385
|
+
"<button type='button' class='btn btn-sm' data-act='interval-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
|
|
386
|
+
: "<button type='button' class='btn btn-sm' data-act='interval-edit' data-arg='" + esc(role) + "'>" + (typeof view.minRunIntervalOverride === "number" ? "Edit" : "Set interval") + "</button>";
|
|
387
|
+
rows.push("<div><dt>Min run interval</dt><dd title='" + esc(intervalBody) + "'>" + esc(intervalBody) + intervalControl + "</dd></div>");
|
|
388
|
+
}
|
|
389
|
+
// The parallel-runner count (roles.<base>.instances), worded like role-render.ts's
|
|
390
|
+
// Instances line — the last per-role hold the drawer edits. Rendered only for a role that
|
|
391
|
+
// may split (its base, an instance strip, is feature or bugfix): the count is meaningless
|
|
392
|
+
// for any other role, which role-view never carries. The body reads the payload's numeric
|
|
393
|
+
// count (roles.<base>.instances) tagged (set) or falls back to 1 tagged (default) when the
|
|
394
|
+
// file leaves it unset — role-view carries instances only when the file sets it. While the
|
|
395
|
+
// editor is open for this role, an input prefilled with the count (empty when none) and
|
|
396
|
+
// Save/Discard
|
|
397
|
+
// replace the sentence, mirroring the interval editor's markup; a role with no count still
|
|
398
|
+
// seeds an empty input, so an empty buffer on Save clears.
|
|
399
|
+
const splitBase = role.replace(/-\d+$/, "");
|
|
400
|
+
if (splitBase === "feature" || splitBase === "bugfix") {
|
|
401
|
+
const hasInstances = typeof view.instances === "number";
|
|
402
|
+
const instCount = hasInstances ? view.instances : 1;
|
|
403
|
+
const instBody = String(instCount) + (hasInstances ? " (set)" : " (default)");
|
|
404
|
+
const editingInst = instancesEditing && instancesEditing.role === role;
|
|
405
|
+
const seed = instancesEditing ? instancesEditing.value : null;
|
|
406
|
+
const instControl = editingInst
|
|
407
|
+
? "<input id='instanceseditinput' type='number' min='1' max='8' step='1' value='" + (seed === "" || seed == null ? "" : String(seed)) + "' placeholder='default 1'>" +
|
|
408
|
+
"<div><button type='button' class='btn btn-sm btn-primary' data-act='instances-save' data-arg='" + esc(role) + "'>Save</button> " +
|
|
409
|
+
"<button type='button' class='btn btn-sm' data-act='instances-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
|
|
410
|
+
: "<button type='button' class='btn btn-sm' data-act='instances-edit' data-arg='" + esc(role) + "'>" + (hasInstances ? "Edit" : "Set instances") + "</button>";
|
|
411
|
+
rows.push("<div><dt>Instances</dt><dd title='" + esc(instBody) + "'>" + esc(instBody) + instControl + "</dd></div>");
|
|
412
|
+
}
|
|
413
|
+
// The windows governing the loop, worded like role-render.ts's Quiet hours line; the end time
|
|
414
|
+
// mirrors quietWindowEnd (browser code cannot import it). A non-director role's own window
|
|
415
|
+
// is an always-shown editable row (the Daily cap row's rationale: the control sits where
|
|
416
|
+
// the hold is shown) — including when the payload carries no role-scope window ("no own
|
|
417
|
+
// window"); any other window stays a read-only row worded exactly as before ("team window").
|
|
418
|
+
const own = Array.isArray(view.quietHours) ? view.quietHours.find(function (w) { return w && w.scope === "role"; }) : undefined;
|
|
419
|
+
const ownWindow = own && typeof own.window === "string" ? own.window : "";
|
|
420
|
+
if (role !== "director") {
|
|
421
|
+
const ownEnd = String(ownWindow).split("-")[1];
|
|
422
|
+
const ownState = own && own.inWindow
|
|
423
|
+
? "holding now — quiet until " + (ownEnd ? ownEnd.trim() : "")
|
|
424
|
+
: "not holding now";
|
|
425
|
+
const editQuiet = quietEditing && quietEditing.role === role;
|
|
426
|
+
const quietBody = ownWindow !== "" ? ownWindow + " (own window; " + ownState + ")" : "no own window";
|
|
427
|
+
const quietControl = editQuiet
|
|
428
|
+
? "<input id='quieteditinput' type='text' placeholder='23:00-07:00' value='" + esc(quietEditing.window) + "'>" +
|
|
429
|
+
"<div><button type='button' class='btn btn-sm btn-primary' data-act='quiet-save' data-arg='" + esc(role) + "'>Save</button> " +
|
|
430
|
+
"<button type='button' class='btn btn-sm' data-act='quiet-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
|
|
431
|
+
: "<button type='button' class='btn btn-sm' data-act='quiet-edit' data-arg='" + esc(role) + "'>" + (ownWindow !== "" ? "Edit" : "Set quiet hours") + "</button>";
|
|
432
|
+
rows.push("<div><dt>Quiet hours</dt><dd title='" + esc(quietBody) + "'>" + esc(quietBody) + quietControl + "</dd></div>");
|
|
433
|
+
}
|
|
434
|
+
if (Array.isArray(view.quietHours)) view.quietHours.forEach(function (w) {
|
|
435
|
+
if (!w || w.scope === "role" || typeof w.window !== "string") return;
|
|
436
|
+
const end = String(w.window).split("-")[1];
|
|
437
|
+
const state = w.inWindow ? "holding now — quiet until " + (end ? end.trim() : "") : "not holding now";
|
|
438
|
+
rows.push(kvRow("Quiet hours", w.window + " (team window; " + state + ")"));
|
|
439
|
+
});
|
|
440
|
+
if (typeof view.inboxCount === "number") rows.push(kvRow("Queued prompts", String(view.inboxCount)));
|
|
441
|
+
// The loop's own spend over its retained event window (rolePayload's usage, summed from the
|
|
442
|
+
// same run_end events recentOutcomes reads): how much the loop burned on tokens and cost,
|
|
443
|
+
// with a notional suffix only when a claude-subscription seam burned non-zero notional spend
|
|
444
|
+
// — mirroring role-render.ts's Spend line, so the drawer and tumwater role <id> read one
|
|
445
|
+
// loop's spend identically. Each numeric field is coerced (foreign shapes read as 0) and an
|
|
446
|
+
// all-zero usage is absent on the payload, so this row never renders for a never-costly loop.
|
|
447
|
+
if (view.usage && typeof view.usage === "object") {
|
|
448
|
+
const u = view.usage;
|
|
449
|
+
const real = Number(u.costUsd) || 0;
|
|
450
|
+
const tokens = Number(u.tokens) || 0;
|
|
451
|
+
const notional = Number(u.notionalCostUsd) || 0;
|
|
452
|
+
let spend = fmtUsd(real) + " · " + fmtTokens(tokens) + " tokens";
|
|
453
|
+
if (notional !== 0) spend += " · " + fmtUsd(notional) + " notional";
|
|
454
|
+
rows.push(kvRow("Spend", spend));
|
|
455
|
+
}
|
|
456
|
+
// The per-role daily cap (rolePayload's dailyCap), worded like role-render.ts's Daily cap line.
|
|
457
|
+
// The per-role daily cap (rolePayload's dailyCap), worded like role-render.ts's Daily cap
|
|
458
|
+
// line. Shown for every non-director role — always, cap or not — because the "Set a cap"/
|
|
459
|
+
// "Edit cap" control is the end of the amber role-cap alert's path: the alert's remedy
|
|
460
|
+
// leads here. A director shows no row (role-view.ts's guard exempts it from a cap). While
|
|
461
|
+
// the cap editor is open for this role, an input prefilled with the current cap (empty for
|
|
462
|
+
// none) and Save/Discard replace the sentence, mirroring the notebook editor's markup.
|
|
463
|
+
if (role !== "director") {
|
|
464
|
+
const dc = view.dailyCap && typeof view.dailyCap === "object" ? view.dailyCap : null;
|
|
465
|
+
const editing = capEditing && capEditing.role === role;
|
|
466
|
+
const capBody = dc
|
|
467
|
+
? fmtUsd(Number(dc.spentUsd) || 0) + " of " + fmtUsd(Number(dc.capUsd) || 0) + " spent today (" + (dc.over ? "holding now — no new runs until local midnight" : "not holding") + ")" +
|
|
468
|
+
(Array.isArray(dc.roleIds) && dc.roleIds.length > 1 ? " · shared by " + dc.roleIds.join(", ") : "")
|
|
469
|
+
: "no cap";
|
|
470
|
+
const capControl = editing
|
|
471
|
+
? "<input id='capeditinput' type='number' min='0' step='0.01' value='" + (capEditing.value ? String(capEditing.value) : "") + "' placeholder='no cap'>" +
|
|
472
|
+
"<div><button type='button' class='btn btn-sm btn-primary' data-act='cap-save' data-arg='" + esc(role) + "'>Save</button> " +
|
|
473
|
+
"<button type='button' class='btn btn-sm' data-act='cap-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
|
|
474
|
+
: "<button type='button' class='btn btn-sm' data-act='cap-edit' data-arg='" + esc(role) + "'>" + (dc ? "Edit cap" : "Set a cap") + "</button>";
|
|
475
|
+
rows.push("<div><dt>Daily cap</dt><dd title='" + esc(capBody) + "'>" + esc(capBody) + capControl + "</dd></div>");
|
|
476
|
+
}
|
|
477
|
+
// The standing instructions (roles.<base>.instructions, resolved through the base role),
|
|
478
|
+
// worded like role-render.ts's Instructions override. Shown for every role — always,
|
|
479
|
+
// override or not — because the Edit/Save control is how the director's and instance
|
|
480
|
+
// runs' instructions get written from the dashboard: the same always-shown rationale as
|
|
481
|
+
// the Daily cap row above. While the editor is open for this role, a textarea prefilled
|
|
482
|
+
// with the current text and Save/Discard replace the sentence, mirroring the notebook
|
|
483
|
+
// editor's markup; a role with no override still seeds an empty textarea, so an empty
|
|
484
|
+
// buffer on Save clears.
|
|
485
|
+
const instEditing = instructionsEditing && instructionsEditing.role === role;
|
|
486
|
+
const instText = typeof view.instructions === "string" ? view.instructions : "";
|
|
487
|
+
const instBody = instText.trim() !== "" ? clamp(instText) : "No standing instructions.";
|
|
488
|
+
const instControl = instEditing
|
|
489
|
+
? "<textarea id='instructionsedittext' rows='10' class='mono' style='width:100%'>" + esc(instructionsEditing.text) + "</textarea>" +
|
|
490
|
+
"<div><button type='button' class='btn btn-sm btn-primary' data-act='instructions-save' data-arg='" + esc(role) + "'>Save</button> " +
|
|
491
|
+
"<button type='button' class='btn btn-sm' data-act='instructions-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
|
|
492
|
+
: "<button type='button' class='btn btn-sm' data-act='instructions-edit' data-arg='" + esc(role) + "'>Edit</button>";
|
|
493
|
+
rows.push("<div><dt>Instructions override</dt><dd title='" + esc(instText) + "'>" + esc(instBody) + instControl + "</dd></div>");
|
|
494
|
+
if (typeof view.find === "string" && view.find.trim() !== "") rows.push(kvRow("Find text", clamp(view.find), view.find));
|
|
495
|
+
return rows.length ? "<section><div class='sec-head'><h3>Configuration</h3></div><dl class='kv'>" + rows.join("") + "</dl></section>" : "";
|
|
496
|
+
}
|
|
257
497
|
// An open notebook edit holds the draft's starting text, not the live payload, so a poll
|
|
258
498
|
// refetch neither wipes the textarea nor changes the painted panel string. Clear waits for a
|
|
259
499
|
// confirming click like entry cancel: the first click arms it for 5 s, the second posts.
|
|
260
500
|
let noteEditing = null; // { role, text }
|
|
261
|
-
let
|
|
501
|
+
let capEditing = null; // { role, value }
|
|
502
|
+
let instructionsEditing = null; // { role, text }
|
|
503
|
+
let quietEditing = null; // { role, window }
|
|
504
|
+
let intervalEditing = null; // { role, value } — value is the seeded override (number) or "" when none
|
|
505
|
+
let instancesEditing = null; // { role, value } — value is the seeded count (number) or "" when none
|
|
506
|
+
let modelEditing = null; // { role, provider, model, thinking } — the seeded raw overrides ("" when unset)
|
|
262
507
|
function repaintDrawerRole() { if (lastStatus) renderLoopDrawer(lastStatus); }
|
|
263
|
-
|
|
508
|
+
// The notebook clear and the backlog entry cancel each build a two-click confirm slot from
|
|
509
|
+
// the shared confirmSlot factory (defined in the loops splice, an earlier one), so the arm,
|
|
510
|
+
// guard, and disarm logic lives once.
|
|
511
|
+
const noteClear = confirmSlot(repaintDrawerRole);
|
|
512
|
+
const noteClearArmed = (role) => noteClear.armed(role);
|
|
513
|
+
function armNoteClear(role) { noteClear.arm(role); }
|
|
514
|
+
function disarmNoteClear() { noteClear.disarm(); }
|
|
515
|
+
function startNoteEdit(role, text) { noteEditing = { role, text }; disarmNoteClear(); repaintDrawerRole(); }
|
|
264
516
|
function stopNoteEdit() { noteEditing = null; repaintDrawerRole(); }
|
|
265
|
-
|
|
266
|
-
function
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
517
|
+
function startCapEdit(role, capUsd) { capEditing = { role, value: capUsd ? Number(capUsd) || 0 : 0 }; repaintDrawerRole(); }
|
|
518
|
+
function stopCapEdit() { capEditing = null; repaintDrawerRole(); }
|
|
519
|
+
function startInstructionsEdit(role, text) { instructionsEditing = { role, text: String(text ?? "") }; repaintDrawerRole(); }
|
|
520
|
+
function stopInstructionsEdit() { instructionsEditing = null; repaintDrawerRole(); }
|
|
521
|
+
function startIntervalEdit(role, value) { intervalEditing = { role, value: value === "" || value == null ? "" : Number(value) }; repaintDrawerRole(); }
|
|
522
|
+
function stopIntervalEdit() { intervalEditing = null; repaintDrawerRole(); }
|
|
523
|
+
function startInstancesEdit(role, value) { instancesEditing = { role, value: value === "" || value == null ? "" : Number(value) }; repaintDrawerRole(); }
|
|
524
|
+
function stopInstancesEdit() { instancesEditing = null; repaintDrawerRole(); }
|
|
525
|
+
function startModelEdit(role, seed) { modelEditing = { role, provider: String((seed && seed.provider) ?? ""), model: String((seed && seed.model) ?? ""), thinking: String((seed && seed.thinking) ?? "") }; repaintDrawerRole(); }
|
|
526
|
+
function stopModelEdit() { modelEditing = null; repaintDrawerRole(); }
|
|
527
|
+
function startQuietEdit(role, window) { quietEditing = { role, window: String(window ?? "") }; repaintDrawerRole(); }
|
|
528
|
+
function stopQuietEdit() { quietEditing = null; repaintDrawerRole(); }
|
|
272
529
|
// The notebook fetch shares loadDrawerSection; closing the drawer clears drawerRole, so
|
|
273
530
|
// reopening refetches.
|
|
274
531
|
function loadDrawerRole(role) {
|
|
275
|
-
return loadDrawerSection(role, "/api/role?role=", "
|
|
532
|
+
return loadDrawerSection(role, "/api/role?role=", "Notes and prompt unavailable", (v) => { drawerRole = v; });
|
|
276
533
|
}
|
|
277
534
|
|
|
278
535
|
async function loadLoopTicks(force) {
|
|
@@ -309,7 +566,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
309
566
|
const pre = $("transcript");
|
|
310
567
|
if (openLoopRole() !== role || !pre) return;
|
|
311
568
|
const lines = Array.isArray(d.lines) ? d.lines : [];
|
|
312
|
-
const html = lines.length ? transcriptHtml(lines) : "<span class='muted'>No transcript yet — it fills in once this
|
|
569
|
+
const html = lines.length ? transcriptHtml(lines) : "<span class='muted'>No transcript yet — it fills in once this role's next run starts.</span>";
|
|
313
570
|
// Stay pinned to the newest line while the reader is at the bottom; leave them be if they
|
|
314
571
|
// scrolled up to read.
|
|
315
572
|
const atBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 12;
|
|
@@ -317,17 +574,13 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
317
574
|
}
|
|
318
575
|
|
|
319
576
|
// A plan/bug cancel waits for its confirming click, the confirmAbort shape: the first click
|
|
320
|
-
// arms it for 5 s, a second inside the window fires, and a timeout disarms.
|
|
321
|
-
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
const entryCancelArmed = (key, title) =>
|
|
325
|
-
function disarmEntryCancel() {
|
|
326
|
-
function armEntryCancel(key, title) {
|
|
327
|
-
confirmEntryCancel = { key, title, until: Date.now() + 5000 };
|
|
328
|
-
refreshEntry();
|
|
329
|
-
setTimeout(() => { if (confirmEntryCancel && confirmEntryCancel.key === key && confirmEntryCancel.until <= Date.now()) { confirmEntryCancel = null; refreshEntry(); } }, 5100);
|
|
330
|
-
}
|
|
577
|
+
// arms it for 5 s, a second inside the window fires, and a timeout disarms. The slot records
|
|
578
|
+
// the title too: a list that shifted between the clicks must re-arm, not cancel whatever
|
|
579
|
+
// entry now sits at that position.
|
|
580
|
+
const entryCancel = confirmSlot(refreshEntry);
|
|
581
|
+
const entryCancelArmed = (key, title) => entryCancel.armed(key, title);
|
|
582
|
+
function disarmEntryCancel() { entryCancel.disarm(); }
|
|
583
|
+
function armEntryCancel(key, title) { entryCancel.arm(key, title); }
|
|
331
584
|
// An open edit form: painted once, so a poll never overwrites the operator's draft; the
|
|
332
585
|
// server's title check catches an entry that changed underneath it at save time.
|
|
333
586
|
let entryEditing = null; // { key, title, painted }
|
|
@@ -336,7 +589,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
336
589
|
refreshEntry();
|
|
337
590
|
}
|
|
338
591
|
function stopEntryEdit() { entryEditing = null; }
|
|
339
|
-
const ENTRY_KINDS = { plans: "Planned feature", bugs: "Open bug", questions: "Open question" };
|
|
592
|
+
const ENTRY_KINDS = { plans: "Planned feature", bugs: "Open bug", questions: "Open question", done: "Done plan", fixed: "Fixed bug", answered: "Answered question" };
|
|
340
593
|
async function refreshEntry() {
|
|
341
594
|
if (!drawer || drawer.kind !== "entry") return;
|
|
342
595
|
const file = drawer.file;
|
|
@@ -363,21 +616,23 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
|
|
|
363
616
|
"<textarea id='entryeditbody' rows='14'>" + esc(d.body || "") + "</textarea>");
|
|
364
617
|
return;
|
|
365
618
|
}
|
|
366
|
-
const action = file === "questions"
|
|
619
|
+
const action = file === "done" || file === "fixed" || file === "answered" ? "" : file === "questions"
|
|
367
620
|
? "<textarea id='answertext' rows='3' placeholder='Your answer — recorded in QUESTIONS.md'></textarea>" +
|
|
368
621
|
"<button type='button' class='btn btn-sm btn-primary' data-act='answer-record' data-arg='" + index + "'>" + icon("chat") + "Record answer</button>" +
|
|
369
622
|
"<button type='button' class='btn btn-sm' data-act='answer' data-arg='" + index + "'>Ask the director instead</button>"
|
|
370
623
|
: "<button type='button' class='btn btn-sm' data-act='mention' data-arg='" + esc(file + ":" + index) + "'>" + icon("chat") + "Ask the director about this</button>" +
|
|
624
|
+
(d.refused || d.waiting ? "<button type='button' class='btn btn-sm btn-primary' data-act='entry-release' data-arg='" + esc(file + ":" + index) + "'>Release this entry</button>" : "") +
|
|
625
|
+
(index > 0 ? "<button type='button' class='btn btn-sm' data-act='entry-top' data-arg='" + esc(file + ":" + index) + "'>Move to top</button>" : "") +
|
|
371
626
|
"<button type='button' class='btn btn-sm' data-act='entry-edit' data-arg='" + esc(file + ":" + index) + "'>Edit this entry</button>" +
|
|
372
627
|
"<button type='button' class='btn btn-sm btn-danger' data-act='entry-cancel' data-arg='" + esc(file + ":" + index) + "'>" + (entryCancelArmed(file + ":" + index) ? "Click again to cancel" : "Cancel this entry") + "</button>";
|
|
373
628
|
// The body is model-written Markdown: renderMarkdown escapes every piece of it.
|
|
374
|
-
paintPanel("entrybody", "<div class='drawer-actions'>" + action + "</div
|
|
629
|
+
paintPanel("entrybody", (action ? "<div class='drawer-actions'>" + action + "</div>" : "") + "<div class='md'>" +
|
|
375
630
|
(d.body ? renderMarkdown(d.body) : "<p class='muted'>No details for this entry.</p>") + "</div>");
|
|
376
631
|
}
|
|
377
632
|
|
|
378
633
|
async function refreshDrawer() {
|
|
379
634
|
if (!drawer) return;
|
|
380
|
-
if (drawer.kind === "loop") {
|
|
635
|
+
if (drawer.kind === "loop") { // vocabulary-ok: the drawer kind stays "loop"
|
|
381
636
|
if (lastStatus) renderLoopDrawer(lastStatus);
|
|
382
637
|
await refreshTranscript(false);
|
|
383
638
|
} else {
|