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,5 +1,6 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The dashboard's boot, browser-side: view routing over #fragment links (Back/Forward and
|
|
2
|
-
* bookmarks work, re-clicking the open tab refetches), #
|
|
3
|
+
* bookmarks work, re-clicking the open tab refetches), #role/<role> opening that loop's
|
|
3
4
|
* drawer with the address bar following it, the global action dispatcher behind the
|
|
4
5
|
* data-act/data-open attributes (alert and tile buttons, loop names anywhere, questions),
|
|
5
6
|
* the keyboard shortcuts (Escape closes the drawer, "/" focuses the composer), the theme
|
|
@@ -7,11 +8,19 @@
|
|
|
7
8
|
* when the serving build moves. Spliced into gui-client.ts's script as its last section,
|
|
8
9
|
* reaching the shared state and the render helpers of every other section through that
|
|
9
10
|
* concatenation. */
|
|
10
|
-
export const GUI_CLIENT_BOOT_JS = String.raw ` //
|
|
11
|
+
export const GUI_CLIENT_BOOT_JS = String.raw ` // One tail for the "…written to the file but the worktree was left dirty" toasts: the
|
|
12
|
+
// cancel/edit/file backlog actions share the same committed===false copy, kept in one place
|
|
13
|
+
// so the operator-facing wording about an uncommitted edit cannot drift between them. The
|
|
14
|
+
// question-answer toast names QUESTIONS.md explicitly, so it stays inline.
|
|
15
|
+
function uncommittedTail(d) {
|
|
16
|
+
return d.committed === false ? ", but NOT committed to main (uncommitted in the checkout)" : "";
|
|
17
|
+
}
|
|
18
|
+
// ---- views ----
|
|
11
19
|
// view-routing:start
|
|
12
|
-
const VIEWS = {
|
|
20
|
+
const VIEWS = { team: "team-view", history: "history", usage: "report", failures: "failures", pending: "pending", settings: "settings-view" }; // vocabulary-ok: view ids
|
|
13
21
|
function switchView(v) {
|
|
14
|
-
if (
|
|
22
|
+
if (v === "fleet") v = "team"; // vocabulary-ok: legacy dashboard hash, still routed
|
|
23
|
+
if (!VIEWS[v]) v = "team";
|
|
15
24
|
activeView = v;
|
|
16
25
|
for (const k of Object.keys(VIEWS)) {
|
|
17
26
|
$(VIEWS[k]).hidden = k !== v;
|
|
@@ -20,15 +29,15 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
20
29
|
if (k === v) tab.setAttribute("aria-current", "page");
|
|
21
30
|
else tab.removeAttribute("aria-current");
|
|
22
31
|
}
|
|
23
|
-
if (v === "
|
|
32
|
+
if (v === "team" && lastStatus) renderTeam(lastStatus);
|
|
24
33
|
if (v === "history") fetchHistory();
|
|
25
34
|
if (v === "usage") fetchReport();
|
|
26
35
|
if (v === "failures") fetchFailures();
|
|
27
36
|
if (v === "pending") fetchPending();
|
|
28
|
-
if (v === "settings") { fetchSettings(); fetchDoctor(); }
|
|
37
|
+
if (v === "settings") { fetchSettings(); fetchDoctor(); fetchRetirable(); fetchBrief(); }
|
|
29
38
|
}
|
|
30
39
|
// Tabs are plain #fragment links, so Back/Forward and bookmarks work; re-clicking the open
|
|
31
|
-
// tab refetches its data. #
|
|
40
|
+
// tab refetches its data. #role/<role> opens that loop's drawer (over Team on a fresh load),
|
|
32
41
|
// and the address bar follows the open drawer, so a loop's live view can be linked to.
|
|
33
42
|
let routed = false;
|
|
34
43
|
function route() {
|
|
@@ -38,9 +47,9 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
38
47
|
// script and the dashboard never polls again — a frozen page from a hand-edited URL.
|
|
39
48
|
let h = location.hash.slice(1);
|
|
40
49
|
try { h = decodeURIComponent(h); } catch { /* malformed escape: route on the raw fragment */ }
|
|
41
|
-
if (h.startsWith("loop/")) {
|
|
42
|
-
if (!routed) switchView("
|
|
43
|
-
if (openLoopRole() !== h.slice(
|
|
50
|
+
if (h.startsWith("role/") || h.startsWith("loop/")) { // vocabulary-ok: legacy dashboard hash, still routed
|
|
51
|
+
if (!routed) switchView("team");
|
|
52
|
+
if (openLoopRole() !== h.slice(h.indexOf("/") + 1)) openLoop(h.slice(h.indexOf("/") + 1));
|
|
44
53
|
} else switchView(h);
|
|
45
54
|
routed = true;
|
|
46
55
|
}
|
|
@@ -58,6 +67,12 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
58
67
|
const form = ev.target instanceof Element ? ev.target.closest("form.settings-row") : null;
|
|
59
68
|
if (form) { ev.preventDefault(); saveSetting(form); }
|
|
60
69
|
});
|
|
70
|
+
// The Initial prompt card's Save: its own form class, so the settings listener above never
|
|
71
|
+
// routes it to saveSetting.
|
|
72
|
+
document.addEventListener("submit", (ev) => {
|
|
73
|
+
const form = ev.target instanceof Element ? ev.target.closest("form.brief-form") : null;
|
|
74
|
+
if (form) { ev.preventDefault(); saveBrief(form); }
|
|
75
|
+
});
|
|
61
76
|
// The Settings view's Clear buttons: one delegated click listener resolves the row's Clear
|
|
62
77
|
// through clickClosest and calls clearSetting (gui-client-settings). No per-row wiring, so
|
|
63
78
|
// the panel repaints freely.
|
|
@@ -69,26 +84,43 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
69
84
|
// The Health check card's Re-run button: runs the doctor checks again.
|
|
70
85
|
document.addEventListener("click", (ev) => {
|
|
71
86
|
if (clickClosest(ev, "button[data-action='doctor-rerun']")) { ev.preventDefault(); fetchDoctor(); }
|
|
87
|
+
if (clickClosest(ev, "button[data-action='notify-test']")) { ev.preventDefault(); testNotify(); }
|
|
88
|
+
const retire = clickClosest(ev, "button[data-action='retire']");
|
|
89
|
+
if (retire) { ev.preventDefault(); confirmRetire(retire.dataset.role); }
|
|
90
|
+
const enable = clickClosest(ev, "button[data-action='enable']");
|
|
91
|
+
if (enable) { ev.preventDefault(); enableRole(enable.dataset.role); }
|
|
92
|
+
});
|
|
93
|
+
// The Health card's Problems only checkbox: repaint the cached report with only its warn
|
|
94
|
+
// and fail rows (toggleDoctorProblems, gui-client-settings), without rerunning the checks.
|
|
95
|
+
document.addEventListener("change", (ev) => {
|
|
96
|
+
const box = clickClosest(ev, "input[data-action='doctor-problems']");
|
|
97
|
+
if (box) toggleDoctorProblems(box.checked);
|
|
72
98
|
});
|
|
73
99
|
// ---- global actions: alert and tile buttons, loop names anywhere, questions ----
|
|
74
100
|
// run-act:start
|
|
101
|
+
// The backlog drawer rows resolve a list entry's title from the last polled status by its
|
|
102
|
+
// backlog file and 0-based position (entry-cancel, entry-edit, and the mention action). The
|
|
103
|
+
// call sites keep their arg's "file:index" split because that shape is theirs alone.
|
|
104
|
+
function entryTitleAt(file, index) {
|
|
105
|
+
return ((lastStatus && lastStatus[file]) || [])[Number(index)];
|
|
106
|
+
}
|
|
75
107
|
function runAct(act, arg) {
|
|
76
|
-
if (act === "
|
|
108
|
+
if (act === "role") openLoop(arg);
|
|
77
109
|
else if (act === "close") closeDrawer();
|
|
78
110
|
else if (act === "view") location.hash = arg;
|
|
79
111
|
else if (act === "budget") openBudgetEditor();
|
|
80
|
-
else if (act === "resume")
|
|
112
|
+
else if (act === "resume") setTeamPause(false);
|
|
81
113
|
else if (act === "copy") copyText(arg);
|
|
82
114
|
else if (act === "restart")
|
|
83
115
|
postAction("/api/restart", {}, (d) =>
|
|
84
116
|
d && typeof d.message === "string"
|
|
85
117
|
? d.message
|
|
86
|
-
: "Restart requested —
|
|
118
|
+
: "Restart requested — tumwater applies it within one poll");
|
|
87
119
|
else if (act === "reclaim")
|
|
88
120
|
postAction("/api/reclaim", {}, (d) =>
|
|
89
121
|
d && typeof d.message === "string" ? d.message : "Reclaim requested");
|
|
90
122
|
else if (act === "questions" || act === "queued" || act === "backlog") {
|
|
91
|
-
if (activeView !== "
|
|
123
|
+
if (activeView !== "team") location.hash = "team";
|
|
92
124
|
if (act !== "backlog") setBacklogTab(act);
|
|
93
125
|
$("backlogcard").scrollIntoView({ behavior: "smooth", block: "start" });
|
|
94
126
|
} else if (act === "answer") {
|
|
@@ -103,24 +135,50 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
103
135
|
postAction("/api/question-answer", { position: Number(arg) + 1, title: q, decision }, (d) => {
|
|
104
136
|
closeDrawer();
|
|
105
137
|
return d && d.status === "answered"
|
|
106
|
-
? "Answered “" + splitTitle(q).title + "” — moved to ## Answered"
|
|
138
|
+
? "Answered “" + splitTitle(q).title + "” — moved to ## Answered" + (d.committed === false ? ", but NOT committed to main (QUESTIONS.md is uncommitted in the checkout)" : "")
|
|
107
139
|
: "That question was already answered or moved — refreshed";
|
|
108
140
|
});
|
|
109
141
|
} else if (act === "entry-cancel") {
|
|
110
142
|
const [file, index] = arg.split(":");
|
|
111
|
-
const title = (
|
|
143
|
+
const title = entryTitleAt(file, index);
|
|
112
144
|
if (title === undefined) return;
|
|
113
145
|
if (!entryCancelArmed(arg, title)) { armEntryCancel(arg, title); return; }
|
|
114
146
|
disarmEntryCancel();
|
|
115
147
|
postAction("/api/backlog-cancel", { kind: file === "bugs" ? "bug" : "plan", position: Number(index) + 1, title }, (d) => {
|
|
116
148
|
closeDrawer();
|
|
117
149
|
return d && d.status === "cancelled"
|
|
118
|
-
? "Cancelled “" + splitTitle(title).title + "” from " + (file === "bugs" ? "BUGS.md" : "PLANS.md")
|
|
150
|
+
? "Cancelled “" + splitTitle(title).title + "” from " + (file === "bugs" ? "BUGS.md" : "PLANS.md") + uncommittedTail(d)
|
|
119
151
|
: "That entry was already removed or moved — refreshed";
|
|
120
152
|
});
|
|
153
|
+
} else if (act === "entry-release") {
|
|
154
|
+
const [file, index] = arg.split(":");
|
|
155
|
+
const title = entryTitleAt(file, index);
|
|
156
|
+
if (title === undefined) return;
|
|
157
|
+
postAction("/api/backlog-release", { kind: file === "bugs" ? "bug" : "plan", position: Number(index) + 1, title }, (d) => {
|
|
158
|
+
closeDrawer();
|
|
159
|
+
if (d && d.status === "released") {
|
|
160
|
+
return "Released “" + splitTitle(title).title + "” in " + (file === "bugs" ? "BUGS.md" : "PLANS.md") + " — " + (file === "bugs" ? "bugfix" : "feature") + " will pick it up" + uncommittedTail(d);
|
|
161
|
+
}
|
|
162
|
+
return d && d.status === "not_refused"
|
|
163
|
+
? "That entry carries no Refused or Waiting note — refreshed"
|
|
164
|
+
: "That entry was removed or moved — refreshed";
|
|
165
|
+
});
|
|
166
|
+
} else if (act === "entry-top") {
|
|
167
|
+
const [file, index] = arg.split(":");
|
|
168
|
+
const title = entryTitleAt(file, index);
|
|
169
|
+
if (title === undefined) return;
|
|
170
|
+
postAction("/api/backlog-top", { kind: file === "bugs" ? "bug" : "plan", position: Number(index) + 1, title }, (d) => {
|
|
171
|
+
closeDrawer();
|
|
172
|
+
if (d && d.status === "moved_to_top") {
|
|
173
|
+
return "Moved “" + splitTitle(title).title + "” to the top of " + (file === "bugs" ? "BUGS.md" : "PLANS.md") + " — " + (file === "bugs" ? "bugfix" : "feature") + " takes it next" + uncommittedTail(d);
|
|
174
|
+
}
|
|
175
|
+
return d && d.status === "already_first"
|
|
176
|
+
? "That entry is already first — refreshed"
|
|
177
|
+
: "That entry was removed or moved — refreshed";
|
|
178
|
+
});
|
|
121
179
|
} else if (act === "entry-edit") {
|
|
122
180
|
const [file, index] = arg.split(":");
|
|
123
|
-
const title = (
|
|
181
|
+
const title = entryTitleAt(file, index);
|
|
124
182
|
if (title !== undefined) startEntryEdit(arg, title);
|
|
125
183
|
} else if (act === "entry-edit-discard") {
|
|
126
184
|
stopEntryEdit();
|
|
@@ -139,11 +197,11 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
139
197
|
stopEntryEdit();
|
|
140
198
|
closeDrawer();
|
|
141
199
|
return d && d.status === "edited"
|
|
142
|
-
? "Edited “" + splitTitle(d.title).title + "” in " + (file === "bugs" ? "BUGS.md" : "PLANS.md")
|
|
200
|
+
? "Edited “" + splitTitle(d.title).title + "” in " + (file === "bugs" ? "BUGS.md" : "PLANS.md") + uncommittedTail(d)
|
|
143
201
|
: "That entry was removed or moved — refreshed";
|
|
144
202
|
});
|
|
145
203
|
} else if (act === "note-edit") {
|
|
146
|
-
const note = drawerRole && drawerRole.view && typeof drawerRole.view.
|
|
204
|
+
const note = drawerRole && drawerRole.view && typeof drawerRole.view.notes === "string" ? drawerRole.view.notes : "";
|
|
147
205
|
startNoteEdit(arg, note);
|
|
148
206
|
} else if (act === "note-discard") {
|
|
149
207
|
stopNoteEdit();
|
|
@@ -159,11 +217,129 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
159
217
|
postAction("/api/role-notes", { role: arg, text }, (d) => {
|
|
160
218
|
noteEditing = null;
|
|
161
219
|
loadDrawerRole(arg);
|
|
162
|
-
return d && typeof d.message === "string" ? d.message : "
|
|
220
|
+
return d && typeof d.message === "string" ? d.message : "Notes updated";
|
|
221
|
+
});
|
|
222
|
+
} else if (act === "cap-edit") {
|
|
223
|
+
startCapEdit(arg, drawerRole?.view?.dailyCap?.capUsd ?? 0);
|
|
224
|
+
} else if (act === "cap-discard") {
|
|
225
|
+
stopCapEdit();
|
|
226
|
+
} else if (act === "cap-save") {
|
|
227
|
+
const input = $("capeditinput");
|
|
228
|
+
if (!input) return;
|
|
229
|
+
// A type=number input reports "" both for a field cleared on purpose (no cap) and for
|
|
230
|
+
// text the browser rejected ("$2"); validity.badInput tells them apart, so a typo is an
|
|
231
|
+
// error instead of silently removing the cap — the team editor's guard, in scope from
|
|
232
|
+
// the operator blob concatenated earlier.
|
|
233
|
+
if (input.validity && input.validity.badInput) {
|
|
234
|
+
showFlash("error: the cap must be a number of 0 or more — leave it empty for no cap");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const parsed = parseBudgetField(input.value);
|
|
238
|
+
if (!parsed.ok) {
|
|
239
|
+
showFlash("error: " + parsed.error);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const value = parsed.value;
|
|
243
|
+
postAction("/api/role-cap", { role: arg, maxDailyCostUsd: value }, (d) => {
|
|
244
|
+
capEditing = null;
|
|
245
|
+
loadDrawerRole(arg);
|
|
246
|
+
return value > 0 ? "Daily cap set to " + fmtCap(value) : "Daily cap removed";
|
|
247
|
+
});
|
|
248
|
+
} else if (act === "interval-edit") {
|
|
249
|
+
startIntervalEdit(arg, drawerRole?.view?.minRunIntervalOverride ?? "");
|
|
250
|
+
} else if (act === "interval-discard") {
|
|
251
|
+
stopIntervalEdit();
|
|
252
|
+
} else if (act === "interval-save") {
|
|
253
|
+
const input = $("intervaleditinput");
|
|
254
|
+
if (!input) return;
|
|
255
|
+
// A type=number input reports "" both for a field cleared on purpose (the team
|
|
256
|
+
// default) and for text the browser rejected ("$2"); validity.badInput tells them
|
|
257
|
+
// apart, so a typo is an error instead of silently clearing the override — the cap
|
|
258
|
+
// arm's guard, in scope from the operator blob concatenated earlier.
|
|
259
|
+
if (input.validity && input.validity.badInput) {
|
|
260
|
+
showFlash("error: the interval must be a number of 0 or more — leave it empty for the team default");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const raw = input.value.trim();
|
|
264
|
+
const value = raw === "" ? null : Number(raw);
|
|
265
|
+
postAction("/api/role-interval", { role: arg, minRunIntervalSeconds: value }, (d) => {
|
|
266
|
+
intervalEditing = null;
|
|
267
|
+
loadDrawerRole(arg);
|
|
268
|
+
return d && typeof d.message === "string" ? d.message : "Min run interval updated";
|
|
269
|
+
});
|
|
270
|
+
} else if (act === "instances-edit") {
|
|
271
|
+
startInstancesEdit(arg, drawerRole?.view?.instances ?? "");
|
|
272
|
+
} else if (act === "instances-discard") {
|
|
273
|
+
stopInstancesEdit();
|
|
274
|
+
} else if (act === "instances-save") {
|
|
275
|
+
const input = $("instanceseditinput");
|
|
276
|
+
if (!input) return;
|
|
277
|
+
// A type=number input reports "" both for a field cleared on purpose (the default 1)
|
|
278
|
+
// and for text the browser rejected ("$2"); validity.badInput tells them apart, so a
|
|
279
|
+
// typo is an error instead of silently clearing the count — the interval arm's guard,
|
|
280
|
+
// in scope from the operator blob concatenated earlier. (A rejected "2.5" is a real
|
|
281
|
+
// number the browser accepts, so it posts and validateConfig's 1–8 integer rule 400s
|
|
282
|
+
// it, flashed through postAction's error path.)
|
|
283
|
+
if (input.validity && input.validity.badInput) {
|
|
284
|
+
showFlash("error: the instances must be an integer from 1 to 8 — leave it empty for the default 1");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const raw = input.value.trim();
|
|
288
|
+
const value = raw === "" ? null : Number(raw);
|
|
289
|
+
postAction("/api/role-instances", { role: arg, instances: value }, (d) => {
|
|
290
|
+
instancesEditing = null;
|
|
291
|
+
loadDrawerRole(arg);
|
|
292
|
+
return d && typeof d.message === "string" ? d.message : "Instances updated";
|
|
293
|
+
});
|
|
294
|
+
} else if (act === "instructions-edit") {
|
|
295
|
+
startInstructionsEdit(arg, drawerRole?.view?.instructions ?? "");
|
|
296
|
+
} else if (act === "instructions-discard") {
|
|
297
|
+
stopInstructionsEdit();
|
|
298
|
+
} else if (act === "instructions-save") {
|
|
299
|
+
const box = $("instructionsedittext");
|
|
300
|
+
const text = box ? box.value : "";
|
|
301
|
+
postAction("/api/role-instructions", { role: arg, text }, (d) => {
|
|
302
|
+
instructionsEditing = null;
|
|
303
|
+
loadDrawerRole(arg);
|
|
304
|
+
return d && typeof d.message === "string" ? d.message : "Instructions updated";
|
|
305
|
+
});
|
|
306
|
+
} else if (act === "quiet-edit") {
|
|
307
|
+
startQuietEdit(arg, drawerRole?.view?.quietHours?.find((w) => w && w.scope === "role")?.window ?? "");
|
|
308
|
+
} else if (act === "quiet-discard") {
|
|
309
|
+
stopQuietEdit();
|
|
310
|
+
} else if (act === "quiet-save") {
|
|
311
|
+
const input = $("quieteditinput");
|
|
312
|
+
const window = input ? input.value : "";
|
|
313
|
+
// A text input has no badInput guard; an empty value reaches the server's clear branch,
|
|
314
|
+
// and a malformed window is refused by checkQuietHours, flashed through postAction's
|
|
315
|
+
// error path.
|
|
316
|
+
postAction("/api/role-quiet-hours", { role: arg, window }, (d) => {
|
|
317
|
+
quietEditing = null;
|
|
318
|
+
loadDrawerRole(arg);
|
|
319
|
+
return d && typeof d.message === "string" ? d.message : "Quiet hours updated";
|
|
320
|
+
});
|
|
321
|
+
} else if (act === "model-edit") {
|
|
322
|
+
startModelEdit(arg, { provider: drawerRole?.view?.providerOverride ?? "", model: drawerRole?.view?.modelOverride ?? "", thinking: drawerRole?.view?.thinkingOverride ?? "" });
|
|
323
|
+
} else if (act === "model-discard") {
|
|
324
|
+
stopModelEdit();
|
|
325
|
+
} else if (act === "model-save") {
|
|
326
|
+
// Every field is a text input, so no badInput guard; an empty value reaches the
|
|
327
|
+
// server's clear branch for that field. All three are sent (each trimmed) — the server
|
|
328
|
+
// ignores whatever it does not recognize and requires at least one present.
|
|
329
|
+
const providerBox = $("modeleditprovider");
|
|
330
|
+
const modelBox = $("modeleditmodel");
|
|
331
|
+
const thinkingBox = $("modeleditthinking");
|
|
332
|
+
const provider = providerBox ? providerBox.value.trim() : "";
|
|
333
|
+
const model = modelBox ? modelBox.value.trim() : "";
|
|
334
|
+
const thinking = thinkingBox ? thinkingBox.value.trim() : "";
|
|
335
|
+
postAction("/api/role-model", { role: arg, provider, model, thinking }, (d) => {
|
|
336
|
+
modelEditing = null;
|
|
337
|
+
loadDrawerRole(arg);
|
|
338
|
+
return d && typeof d.message === "string" ? d.message : "Model overrides updated";
|
|
163
339
|
});
|
|
164
340
|
} else if (act === "mention") {
|
|
165
341
|
const [file, index] = arg.split(":");
|
|
166
|
-
const title = (
|
|
342
|
+
const title = entryTitleAt(file, index);
|
|
167
343
|
if (title !== undefined) draftForDirector("About “" + splitTitle(title).title + "”: ");
|
|
168
344
|
}
|
|
169
345
|
}
|
|
@@ -175,7 +351,10 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
175
351
|
// A history row's drill-down button sits inside the row's data-open cell area but must not
|
|
176
352
|
// open the drawer — expansion is the button's alone (gui-client-history's own delegated
|
|
177
353
|
// handler toggles the card).
|
|
178
|
-
if (open && !t.closest("#
|
|
354
|
+
if (open && !t.closest("#rolerows") && !t.closest("[data-tickdetail]")) { // vocabulary-ok: element selectors
|
|
355
|
+
ev.preventDefault();
|
|
356
|
+
toggleLoop(open.dataset.open);
|
|
357
|
+
}
|
|
179
358
|
});
|
|
180
359
|
const typing = (el) => el instanceof Element && el.closest("input, textarea, select, [contenteditable]") !== null;
|
|
181
360
|
document.addEventListener("keydown", (ev) => {
|
|
@@ -204,12 +383,12 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
204
383
|
// ---- the poll ----
|
|
205
384
|
function rerender() {
|
|
206
385
|
if (!lastStatus) return;
|
|
207
|
-
if (activeView === "
|
|
386
|
+
if (activeView === "team") renderTeam(lastStatus);
|
|
208
387
|
if (activeView === "pending") renderPendingQueue(lastStatus);
|
|
209
|
-
if (drawer && drawer.kind === "loop") renderLoopDrawer(lastStatus);
|
|
388
|
+
if (drawer && drawer.kind === "loop") renderLoopDrawer(lastStatus); // vocabulary-ok: drawer kind
|
|
210
389
|
}
|
|
211
390
|
let lastEventKey = null;
|
|
212
|
-
// New events are what move the numbers the other panels show (today's
|
|
391
|
+
// New events are what move the numbers the other panels show (today's merges, the History
|
|
213
392
|
// rows, the open loop's recent ticks), so they refetch on that signal, each throttled.
|
|
214
393
|
function onNewEvents() {
|
|
215
394
|
refreshToday(false);
|
|
@@ -221,7 +400,7 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
221
400
|
let d;
|
|
222
401
|
try {
|
|
223
402
|
// The guarded getJson: the server's 500 sends a JSON {error} body, which must never be
|
|
224
|
-
// taken for
|
|
403
|
+
// taken for team state — a failed poll keeps the last good frame and says so. The
|
|
225
404
|
// bounded wait covers the other failure a bare fetch never reports: a connection the
|
|
226
405
|
// server accepted but never answers (pollSignal).
|
|
227
406
|
d = await getJson("/api/status", pollSignal());
|
|
@@ -245,14 +424,14 @@ export const GUI_CLIENT_BOOT_JS = String.raw ` // ---- views ----
|
|
|
245
424
|
renderSidebar(d);
|
|
246
425
|
renderAlerts(d);
|
|
247
426
|
renderComposer(d);
|
|
248
|
-
if (activeView === "
|
|
427
|
+
if (activeView === "team") renderTeam(d);
|
|
249
428
|
if (activeView === "pending") renderPendingQueue(d);
|
|
250
429
|
if (activeView === "history") syncHistoryRoles(d);
|
|
251
|
-
// Keep the Usage/Failures Loop pickers in step with the
|
|
430
|
+
// Keep the Usage/Failures Loop pickers in step with the team while either is open.
|
|
252
431
|
if (activeView === "usage" || activeView === "failures") syncReportRoles(d);
|
|
253
432
|
const items = d.eventItems || [];
|
|
254
433
|
const last = items[items.length - 1];
|
|
255
|
-
const key = last ? last.ts + ":" + last.type + ":" + last.
|
|
434
|
+
const key = last ? last.ts + ":" + last.type + ":" + last.source : "";
|
|
256
435
|
if (lastEventKey !== null && key !== lastEventKey) onNewEvents();
|
|
257
436
|
lastEventKey = key;
|
|
258
437
|
} catch (err) {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** The dashboard's composer, browser-side — the one input box that steers the director or any
|
|
2
3
|
* single loop: the target selector with its per-target unsent drafts, the character counter,
|
|
3
4
|
* the image attachments (dropped or pasted onto the box, rendered as removable chips), and
|
|
4
5
|
* the submit that reads the bytes and queues the prompt through the same endpoints the CLI's
|
|
5
|
-
* "prompt" and "prompt --role" use. Split out of gui-client-
|
|
6
|
-
* read-only status rendering the composer sits on top of.
|
|
6
|
+
* "prompt" and "prompt --role" use. Split out of gui-client-team.ts, which keeps the
|
|
7
|
+
* read-only status rendering the composer sits on top of. renderTeam calls renderComposer
|
|
7
8
|
* each poll; gui-client-boot.ts's question and backlog actions and gui-client-loops.ts's row
|
|
8
9
|
* actions call draftForDirector and focusComposer. Spliced into gui-client.ts's script right
|
|
9
|
-
* after the
|
|
10
|
-
* and the
|
|
10
|
+
* after the team view, reaching its helpers ($, esc, postJson, plural, showFlash, refresh)
|
|
11
|
+
* and the team view's activeView/drawer state through that concatenation. */
|
|
11
12
|
import { DIRECTOR_PROMPT_MAX_CHARS } from "../../inbox/inbox-submit.js";
|
|
12
13
|
import { PROMPT_IMAGE_EXTENSIONS, PROMPT_IMAGE_MAX_BYTES, PROMPT_IMAGES_MAX_COUNT } from "../../inbox/inbox-attachments.js";
|
|
13
14
|
export const GUI_CLIENT_COMPOSER_JS = String.raw ` // ---- composer: one box for the director or any single loop ----
|
|
@@ -17,39 +18,73 @@ export const GUI_CLIENT_COMPOSER_JS = String.raw ` // ---- composer: one box fo
|
|
|
17
18
|
let promptTarget = "director";
|
|
18
19
|
let targetKey = "";
|
|
19
20
|
const drafts = {}; // each target keeps its own unsent draft
|
|
21
|
+
// Recall of recently sent prompts, the composer's mirror of the TUI prompt line's
|
|
22
|
+
// tui-prompt-history.ts: Up steps back through the prompts you have sent, Down steps
|
|
23
|
+
// forward and restores the live draft you were typing. promptHistory holds the text of
|
|
24
|
+
// each successfully sent prompt, oldest first; recallIndex is null while the live draft
|
|
25
|
+
// is shown, an index while browsing history; recallDraft saves the live draft when the
|
|
26
|
+
// first Up leaves it, so Down past the newest entry hands it back untouched.
|
|
27
|
+
const promptHistory = [];
|
|
28
|
+
let recallIndex = null;
|
|
29
|
+
let recallDraft = "";
|
|
30
|
+
const PROMPT_HISTORY_MAX = 50;
|
|
31
|
+
// The last status payload, kept so setTarget can re-render the image note between polls.
|
|
32
|
+
let composerData = null;
|
|
33
|
+
// The image-input notice under the image chips (plans/image-input.md "Surfaces", part 5/5):
|
|
34
|
+
// when the selected non-director loop's model can't read images but reroutes a blind read
|
|
35
|
+
// to another tier, say so — read from the status payload (imageInput/imageReroute), the same
|
|
36
|
+
// facts the images alert and the submit gate use, so the composer never promises a run the
|
|
37
|
+
// gate would refuse.
|
|
38
|
+
function renderPromptImageNote() {
|
|
39
|
+
const loop = (composerData && composerData.roles || []).find((l) => l.role === promptTarget);
|
|
40
|
+
const note = $("promptimagenote");
|
|
41
|
+
const text = loop && loop.imageInput === false && loop.imageReroute
|
|
42
|
+
? promptTarget + "'s model can't read images; image prompts run on " + loop.imageReroute
|
|
43
|
+
: "";
|
|
44
|
+
if (note.textContent !== text) note.textContent = text;
|
|
45
|
+
note.hidden = text === "";
|
|
46
|
+
}
|
|
20
47
|
function renderComposer(d) {
|
|
21
|
-
const roles = (d.
|
|
48
|
+
const roles = (d.roles || []).map((l) => l.role).filter((r) => r !== "director");
|
|
22
49
|
const key = roles.join(",");
|
|
23
50
|
if (key !== targetKey) {
|
|
24
51
|
targetKey = key;
|
|
25
|
-
targetSelect.innerHTML = "<option value='director'>Director</option>" + (roles.length ? "<optgroup label='One
|
|
26
|
-
roles.map((r) => "<option value='" + esc(r) + "'>" + esc(r) + "</option>").join("") + "</optgroup>" : "");
|
|
52
|
+
targetSelect.innerHTML = "<option value='director'>Director</option>" + (roles.length ? "<optgroup label='One role, at its next run'>" +
|
|
53
|
+
roles.map((r) => "<option value='" + esc(r) + "'>" + esc(roleLabel(r)) + "</option>").join("") + "</optgroup>" : "");
|
|
27
54
|
if (promptTarget !== "director" && !roles.includes(promptTarget)) setTarget("director");
|
|
28
55
|
targetSelect.value = promptTarget;
|
|
29
56
|
}
|
|
30
57
|
const n = (d.inbox || 0) + Object.values(d.roleInbox || {}).reduce((a, b) => a + b, 0);
|
|
31
58
|
const link = $("queuelink");
|
|
32
59
|
link.hidden = n === 0;
|
|
33
|
-
link.dataset.act = "queued";
|
|
34
|
-
const text = plural(n, "
|
|
60
|
+
link.dataset.act = "queued"; // vocabulary-ok: data-act routing key the click handler matches
|
|
61
|
+
const text = plural(n, "message") + " in the inbox";
|
|
35
62
|
if (link.textContent !== text) link.textContent = text;
|
|
63
|
+
composerData = d;
|
|
64
|
+
renderPromptImageNote();
|
|
36
65
|
}
|
|
37
66
|
function composerHint() {
|
|
38
67
|
$("prompthint").innerHTML = esc(promptTarget === "director"
|
|
39
|
-
? "The director runs this next, ahead of every
|
|
40
|
-
: "
|
|
68
|
+
? "The director runs this next, ahead of every role."
|
|
69
|
+
: "Goes to the " + roleLabel(promptTarget) + " inbox for its next run; the role wakes right away.") +
|
|
41
70
|
" <kbd>Enter</kbd> sends · <kbd>Shift</kbd>+<kbd>Enter</kbd> adds a line · <kbd>/</kbd> jumps here";
|
|
42
|
-
promptInput.placeholder = promptTarget === "director" ? "Tell the
|
|
71
|
+
promptInput.placeholder = promptTarget === "director" ? "Tell the team what to do next…" : "Tell the " + roleLabel(promptTarget) + " role what to do on its next run…";
|
|
43
72
|
}
|
|
44
73
|
function setTarget(t) {
|
|
45
74
|
if (t === promptTarget) return;
|
|
46
|
-
|
|
75
|
+
// Mid-recall the box holds a recalled entry, not the draft; hand the saved draft to the
|
|
76
|
+
// old target's slot (the TUI's settlePromptRecall swaps the same way) so no unsent draft
|
|
77
|
+
// is lost, then end recall.
|
|
78
|
+
drafts[promptTarget] = recallIndex !== null ? recallDraft : promptInput.value;
|
|
47
79
|
promptTarget = t;
|
|
80
|
+
recallIndex = null;
|
|
81
|
+
recallDraft = "";
|
|
48
82
|
targetSelect.value = t;
|
|
49
83
|
promptInput.value = drafts[t] || "";
|
|
50
84
|
composerHint();
|
|
51
85
|
autosize();
|
|
52
86
|
updateCount();
|
|
87
|
+
renderPromptImageNote();
|
|
53
88
|
}
|
|
54
89
|
function autosize() {
|
|
55
90
|
promptInput.style.height = "auto";
|
|
@@ -65,25 +100,66 @@ export const GUI_CLIENT_COMPOSER_JS = String.raw ` // ---- composer: one box fo
|
|
|
65
100
|
// about a backlog entry), leaving the cursor at the end.
|
|
66
101
|
function draftForDirector(text) {
|
|
67
102
|
setTarget("director");
|
|
103
|
+
recallIndex = null;
|
|
104
|
+
recallDraft = "";
|
|
68
105
|
promptInput.value = text;
|
|
69
106
|
autosize();
|
|
70
107
|
updateCount();
|
|
71
108
|
focusComposer();
|
|
72
109
|
promptInput.setSelectionRange(text.length, text.length);
|
|
73
110
|
}
|
|
74
|
-
// The composer sits on the
|
|
111
|
+
// The composer sits on the Team view; bring it into sight from anywhere and focus it.
|
|
75
112
|
function focusComposer() {
|
|
76
|
-
if (activeView !== "
|
|
113
|
+
if (activeView !== "team") location.hash = "team";
|
|
77
114
|
if (window.innerWidth <= 1180 && drawer) closeDrawer();
|
|
78
115
|
$("promptform").scrollIntoView({ behavior: "smooth", block: "center" });
|
|
79
116
|
promptInput.focus({ preventScroll: true });
|
|
80
117
|
}
|
|
81
118
|
targetSelect.addEventListener("change", () => setTarget(targetSelect.value));
|
|
82
119
|
promptInput.addEventListener("input", () => { autosize(); updateCount(); });
|
|
120
|
+
// Recall one recently sent prompt: step is -1 for Up (older) and 1 for Down (newer).
|
|
121
|
+
// Returns true when the box changed (so the caller can stop the browser's own Up/Down
|
|
122
|
+
// cursor move). Up from the live draft saves it and moves to the newest entry; Up steps
|
|
123
|
+
// one older while it can and holds the oldest end. Down steps newer; past the newest it
|
|
124
|
+
// restores the saved live draft and ends recall. Down while already live is a no-op. On a
|
|
125
|
+
// change the recalled text is set ready to send, sized, counted, and the cursor put at the
|
|
126
|
+
// end. The composer's mirror of the TUI's recallPromptHistory.
|
|
127
|
+
function recallSentPrompt(step) {
|
|
128
|
+
if (step === -1) {
|
|
129
|
+
if (promptHistory.length === 0) return false;
|
|
130
|
+
if (recallIndex === null) {
|
|
131
|
+
recallDraft = promptInput.value;
|
|
132
|
+
recallIndex = promptHistory.length - 1;
|
|
133
|
+
} else if (recallIndex === 0) {
|
|
134
|
+
return false; // already at the oldest end
|
|
135
|
+
} else {
|
|
136
|
+
recallIndex--;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
if (recallIndex === null) return false; // already on the live draft
|
|
140
|
+
if (recallIndex >= promptHistory.length - 1) {
|
|
141
|
+
promptInput.value = recallDraft; // past the newest: the live draft returns
|
|
142
|
+
recallDraft = "";
|
|
143
|
+
recallIndex = null;
|
|
144
|
+
autosize();
|
|
145
|
+
updateCount();
|
|
146
|
+
promptInput.setSelectionRange(promptInput.value.length, promptInput.value.length);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
recallIndex++;
|
|
150
|
+
}
|
|
151
|
+
promptInput.value = promptHistory[recallIndex];
|
|
152
|
+
autosize();
|
|
153
|
+
updateCount();
|
|
154
|
+
promptInput.setSelectionRange(promptInput.value.length, promptInput.value.length);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
83
157
|
promptInput.addEventListener("keydown", (ev) => {
|
|
84
158
|
if (ev.key === "Enter" && !ev.shiftKey && !ev.isComposing) {
|
|
85
159
|
ev.preventDefault();
|
|
86
160
|
$("promptform").requestSubmit();
|
|
161
|
+
} else if ((ev.key === "ArrowUp" || ev.key === "ArrowDown") && !ev.isComposing && (recallIndex !== null || !promptInput.value.includes("\n"))) {
|
|
162
|
+
if (recallSentPrompt(ev.key === "ArrowUp" ? -1 : 1)) ev.preventDefault();
|
|
87
163
|
} else if (ev.key === "Escape") promptInput.blur();
|
|
88
164
|
});
|
|
89
165
|
// composer-images:start
|
|
@@ -185,17 +261,15 @@ export const GUI_CLIENT_COMPOSER_JS = String.raw ` // ---- composer: one box fo
|
|
|
185
261
|
// "prompt" and "prompt --role" use. Resolves to the confirmation to show; rejects when the
|
|
186
262
|
// server did not take it (the caller keeps the text so it can be fixed and resent). Pending
|
|
187
263
|
// images ride along as { name, dataBase64 } and are named in the confirmation.
|
|
188
|
-
async function sendPrompt(target, text, images
|
|
264
|
+
async function sendPrompt(target, text, images) {
|
|
189
265
|
const attached = images && images.length ? " with " + plural(images.length, "image") : "";
|
|
190
|
-
const deferred = defer ? " — delivers in " + defer : "";
|
|
191
266
|
const body = images && images.length ? { text, images } : { text };
|
|
192
|
-
if (defer) body.defer = defer;
|
|
193
267
|
if (target === "director") {
|
|
194
268
|
await postJson("/api/prompt", body);
|
|
195
|
-
return "
|
|
269
|
+
return "Added to the director's inbox — it runs next" + attached;
|
|
196
270
|
}
|
|
197
271
|
await postJson("/api/prompt-role", { role: target, ...body });
|
|
198
|
-
return "
|
|
272
|
+
return "Added to the " + roleLabel(target) + " inbox for its next run — it wakes now" + attached;
|
|
199
273
|
}
|
|
200
274
|
// composer-send:end
|
|
201
275
|
let sending = false;
|
|
@@ -203,14 +277,13 @@ export const GUI_CLIENT_COMPOSER_JS = String.raw ` // ---- composer: one box fo
|
|
|
203
277
|
ev.preventDefault();
|
|
204
278
|
const text = promptInput.value.trim();
|
|
205
279
|
if (!text || sending) return;
|
|
206
|
-
const defer = $("promptdefer").value.trim();
|
|
207
280
|
const target = promptTarget;
|
|
208
281
|
sending = true;
|
|
209
282
|
$("promptsend").disabled = true;
|
|
210
283
|
const images = [];
|
|
211
284
|
try {
|
|
212
285
|
for (const img of promptImages) images.push({ name: img.name, dataBase64: await readImageAsBase64(img.file) });
|
|
213
|
-
showFlash(await sendPrompt(target, text, images
|
|
286
|
+
showFlash(await sendPrompt(target, text, images));
|
|
214
287
|
} catch (err) {
|
|
215
288
|
// The prompt was not accepted: keep the text and the images so they can be fixed and resent.
|
|
216
289
|
showFlash("error: " + err.message);
|
|
@@ -222,8 +295,14 @@ export const GUI_CLIENT_COMPOSER_JS = String.raw ` // ---- composer: one box fo
|
|
|
222
295
|
promptImages.length = 0;
|
|
223
296
|
renderPromptImages();
|
|
224
297
|
promptInput.value = "";
|
|
225
|
-
$("promptdefer").value = "";
|
|
226
298
|
drafts[target] = "";
|
|
299
|
+
// Record the sent prompt for recall: consecutive duplicates add one entry (re-sending the
|
|
300
|
+
// same nudge does not fill the history with copies), the oldest drop past the cap, and the
|
|
301
|
+
// recall position returns to the live draft for the next browse.
|
|
302
|
+
if (text && promptHistory[promptHistory.length - 1] !== text) promptHistory.push(text);
|
|
303
|
+
if (promptHistory.length > PROMPT_HISTORY_MAX) promptHistory.shift();
|
|
304
|
+
recallIndex = null;
|
|
305
|
+
recallDraft = "";
|
|
227
306
|
if (target !== "director") setTarget("director");
|
|
228
307
|
autosize();
|
|
229
308
|
updateCount();
|