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,238 +1,41 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { readTickDetail } from "../../tick/tick-detail-data.js";
|
|
9
|
-
import { renderTickDetail } from "../../tick/tick-detail.js";
|
|
10
|
-
import { collectFleetChanges, collectRoleChange } from "../../change/change-data.js";
|
|
11
|
-
import { renderFleetChange, renderRoleChange } from "../../change/change-render.js";
|
|
12
|
-
import { readLandingQueue } from "../../landing/landing-queue.js";
|
|
13
|
-
import { renderLandingQueue } from "../../landing/landing-queue-render.js";
|
|
14
|
-
import { promptLengthProblem, submitPrompt } from "../../inbox/inbox-submit.js";
|
|
15
|
-
import { listWideScope, queuedListRows, renderQueuedList } from "../../inbox/inbox.js";
|
|
16
|
-
import { fileEntryWithBody, OPERATOR_FILE_KINDS, tryCancelEntry, tryEditEntry } from "../../backlog/backlog-write.js";
|
|
17
|
-
import { stripEntryStamp } from "../../backlog/backlog-structure.js";
|
|
18
|
-
import { cancelRolePrompt } from "../../inbox/inbox-cancel.js";
|
|
19
|
-
import { durationLabel } from "../../cli/cli-args.js";
|
|
20
|
-
import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../../cli/question-commands.js";
|
|
21
|
-
import { editQueuedFile, readQueuedPromptBody } from "../../inbox/inbox-edit.js";
|
|
22
|
-
import { setDailyBudgetUsd } from "../../config/config-write.js";
|
|
23
|
-
import { isFleetPaused, pausedRoles, pauseFleet, pauseRole, resumeFleet, resumeRole, } from "../../fleet/fleet-state.js";
|
|
24
|
-
import { requestAbort, requestWake, cancelWakes, rolePauseMessage, roleResumeMessage, submitRolePromptAndWake, } from "../../operator/operator-intent.js";
|
|
25
|
-
import { collapseWhitespace, errorMessage } from "../../text/text.js";
|
|
26
|
-
import { saveRoleNote } from "../../agent/role-notes-write.js";
|
|
27
|
-
import { roleNotesPath } from "../../paths.js";
|
|
28
|
-
import { fleetPausePhrase, fleetResumePhrase } from "../../text/phrases.js";
|
|
29
|
-
import { usdCap } from "../../text/format.js";
|
|
1
|
+
import { tickDetailStat } from "../../tick/tick-detail.js";
|
|
2
|
+
import { collectTeamChanges, collectRoleChange } from "../../change/change-data.js";
|
|
3
|
+
import { runDoctor } from "../../doctor/doctor.js";
|
|
4
|
+
import { commitOperatorBacklogEdit } from "../../backlog/backlog-commit.js";
|
|
5
|
+
import { stampedSubject } from "../../git/commit-message.js";
|
|
6
|
+
import { requestAbort } from "../../operator/operator-intent.js";
|
|
7
|
+
import { errorMessage } from "../../text/text.js";
|
|
30
8
|
import { DIRECTOR_ROLE } from "../../roles/roles.js";
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import { activityEvents, roleInspectMarkdown } from "./tui-pane.js";
|
|
36
|
-
import { arrowDir, pageDir } from "./tui-keymap.js";
|
|
9
|
+
import { browsableBacklogEntries } from "./tui-backlog.js";
|
|
10
|
+
import { arrowDir } from "./tui-keymap.js";
|
|
11
|
+
import { createTuiEditors } from "./tui-editors.js";
|
|
12
|
+
import { createTuiPanes } from "./tui-panes.js";
|
|
37
13
|
/** How long a TUI flash notice stays visible (ms). */
|
|
38
14
|
const FLASH_MS = 3000;
|
|
39
|
-
/** Every Ctrl+<key> cycle walks its own ReadonlyArray<[key, label]> table one slot at a time,
|
|
40
|
-
* wrapping past the last back to the first. Each handler holds a key the table carries (a null
|
|
41
|
-
* default slot included), so findIndex always hits. The null fallback in cycleLabel keeps a
|
|
42
|
-
* would-be stale key from rendering "undefined". */
|
|
43
|
-
function nextInCycle(table, current) {
|
|
44
|
-
const i = table.findIndex(([key]) => key === current);
|
|
45
|
-
return table[(i + 1) % table.length][0];
|
|
46
|
-
}
|
|
47
|
-
function cycleLabel(table, current) {
|
|
48
|
-
return table.find(([key]) => key === current)?.[1] ?? null;
|
|
49
|
-
}
|
|
50
|
-
/** The History pane's session window cycle, Ctrl+S: the default newest-N read, then the same
|
|
51
|
-
* trailing windows the dashboard's History view and `tumwater history --since` offer — 1h,
|
|
52
|
-
* 6h, 1d, and the 7d cap the CLI's flag and the dashboard both stop at. The label is what the
|
|
53
|
-
* hint line names; the length is what readTickRowsSince takes. `null` is the default read
|
|
54
|
-
* through readTickRows, kept first so the pane opens on today's behavior. */
|
|
55
|
-
const HISTORY_WINDOWS = [
|
|
56
|
-
[null, "default"],
|
|
57
|
-
[3_600_000, "1h"],
|
|
58
|
-
[21_600_000, "6h"],
|
|
59
|
-
[86_400_000, "1d"],
|
|
60
|
-
[604_800_000, "7d"],
|
|
61
|
-
];
|
|
62
|
-
/** The Ctrl+G scope cycle: fleet-wide (null), then each unique base role the roster names, in
|
|
63
|
-
* roster order. An instance id (`feature-2`) folds into its base role because the scoped
|
|
64
|
-
* collectors aggregate by base role — report-data.ts's role scope groups an instance's usage
|
|
65
|
-
* with its siblings' — so cycling through the instance ids would relabel the same body with
|
|
66
|
-
* names that collect it identically. `feature`/`feature-2` therefore offer one slot, and the
|
|
67
|
-
* name the tab strip and hint show is the name the collector honors. */
|
|
68
|
-
function scopeCycle(roleIds) {
|
|
69
|
-
const bases = [];
|
|
70
|
-
for (const id of roleIds) {
|
|
71
|
-
const base = baseRoleOf(id);
|
|
72
|
-
if (!bases.includes(base))
|
|
73
|
-
bases.push(base);
|
|
74
|
-
}
|
|
75
|
-
return [null, ...bases];
|
|
76
|
-
}
|
|
77
|
-
/** The History pane's session outcome cycle, Ctrl+O: all ticks, then the canonical classes
|
|
78
|
-
* `tumwater history --result` and the dashboard's History filter speak. The class list comes
|
|
79
|
-
* from history-data.ts (HISTORY_RESULT_CLASSES) rather than a copy, so the TUI, the CLI, and
|
|
80
|
-
* the dashboard bucket a result the same way; the class id doubles as the label. `null` is no
|
|
81
|
-
* filter, kept first so the pane opens on today's behavior. */
|
|
82
|
-
const HISTORY_RESULTS = [
|
|
83
|
-
[null, "all"],
|
|
84
|
-
...HISTORY_RESULT_CLASSES.map((c) => [c, c]),
|
|
85
|
-
];
|
|
86
|
-
/** The History pane's body for one trailing window, one outcome class, one exact-loop scope,
|
|
87
|
-
* and one free-text search: the paneCache the Ctrl+T/Ctrl+S/Ctrl+O/Ctrl+G/`/` handlers store.
|
|
88
|
-
* The default (null/null/null/null) read is byte-for-byte the newest-N pane that predates the
|
|
89
|
-
* window, outcome, loop, and search cycles — readTickRows with HISTORY_DEFAULT_TICKS,
|
|
90
|
-
* unfiltered. A trailing window reads through the same readTickRowsSince collector `tumwater
|
|
91
|
-
* history --since` uses, a class filters through history-data.ts's canonical historyClass rule,
|
|
92
|
-
* and the search filters through tickRowGrepHaystack — the same haystack and case-insensitive
|
|
93
|
-
* substring rule `tumwater history --grep` and the dashboard's History search use — so the TUI,
|
|
94
|
-
* the CLI, and the dashboard cannot disagree about the rows. The role is the exact loop id the
|
|
95
|
-
* raw roster (and `history --role`, and the dashboard picker) names — readTickRows filters
|
|
96
|
-
* `e.loop === role`, so an instance id is its own scope, never folded into a base role. An empty
|
|
97
|
-
* read with no class and no search returns "" so the pane keeps its own no-ticks note instead of
|
|
98
|
-
* a lone header; when a class or a search is active and the read returned rows, an empty
|
|
99
|
-
* filtered body names whichever filter the CLI's precedence blames, so the pane states the
|
|
100
|
-
* filter instead of claiming the log is empty. Otherwise the body is prefixed with the active
|
|
101
|
-
* role/window/result/search labels and, when the read cannot prove full coverage, the same
|
|
102
|
-
* sparse-window note every windowed render prints (SPARSE_WINDOW_NOTE). */
|
|
103
|
-
function historyPaneCache(root, sinceMs, resultClass, role, grep) {
|
|
104
|
-
const { rows, covered } = sinceMs === null
|
|
105
|
-
? { rows: readTickRows(root, HISTORY_DEFAULT_TICKS, role), covered: true }
|
|
106
|
-
: readTickRowsSince(root, sinceMs, role);
|
|
107
|
-
// The search runs first, exactly as `tumwater history` orders --grep ahead of --result, and
|
|
108
|
-
// its admitted count is saved so the empty-body wording blames the filter that actually
|
|
109
|
-
// emptied the set (`afterGrep > 0` means rows matched the pattern and the class then removed
|
|
110
|
-
// them). The search applies history-data.ts's shared tick-grep rule (tickRowsMatchingGrep),
|
|
111
|
-
// the same one the CLI and the dashboard search use, so the three surfaces cannot disagree.
|
|
112
|
-
const grepped = grep === null ? rows : tickRowsMatchingGrep(rows, grep);
|
|
113
|
-
const afterGrep = grepped.length;
|
|
114
|
-
const filtered = resultClass === null ? grepped : grepped.filter((r) => historyClass(r.result) === resultClass);
|
|
115
|
-
const body = historyPaneText(filtered);
|
|
116
|
-
// null when no class is active, so the header/empty-body paths below only name a result
|
|
117
|
-
// filter the session actually holds (cycleLabel(HISTORY_RESULTS, null) is the "all" label).
|
|
118
|
-
const resultLabel = resultClass === null ? null : cycleLabel(HISTORY_RESULTS, resultClass);
|
|
119
|
-
// The role first, then the window/result/search labels: the loop is the coarsest cut, and a
|
|
120
|
-
// role-only read (no window, no class) still prints its scope line above the rows.
|
|
121
|
-
const header = [
|
|
122
|
-
...(role === null ? [] : ["role: " + role]),
|
|
123
|
-
...(sinceMs === null ? [] : ["window: last " + (cycleLabel(HISTORY_WINDOWS, sinceMs) ?? "default")]),
|
|
124
|
-
...(resultLabel === null ? [] : ["result: " + resultLabel]),
|
|
125
|
-
...(grep === null ? [] : ["search: " + grep]),
|
|
126
|
-
].join("\n");
|
|
127
|
-
if (!body) {
|
|
128
|
-
// The class names itself only when it emptied a set grep admitted (`afterGrep > 0`) — the
|
|
129
|
-
// CLI's condition and the dashboard's `hasRows` draw the same line — so an empty log or
|
|
130
|
-
// window keeps the pane's own no-ticks note instead of blaming a filter that removed
|
|
131
|
-
// nothing. A pattern that admitted nothing names itself next, with the CLI's wording. An
|
|
132
|
-
// empty read with neither filter returns "" so the pane keeps its own note, and a role-only
|
|
133
|
-
// empty read returns "" too (the CLI's empty-state wording does not name --role), so the
|
|
134
|
-
// pane never grows a lone `role:` header.
|
|
135
|
-
if (resultClass !== null && afterGrep > 0) {
|
|
136
|
-
return header + "\nno ticks with result \"" + resultLabel + "\"";
|
|
137
|
-
}
|
|
138
|
-
if (grep !== null)
|
|
139
|
-
return header + "\nno ticks matching \"" + grep + "\"";
|
|
140
|
-
return "";
|
|
141
|
-
}
|
|
142
|
-
return (header ? header + "\n" : "") + body + (sinceMs !== null && !covered ? "\n" + SPARSE_WINDOW_NOTE : "");
|
|
143
|
-
}
|
|
144
|
-
/** The Usage and Failures panes' session window cycle, Ctrl+E: the default whole-day report,
|
|
145
|
-
* then 1h, 6h, 1d, and the REPORT_SINCE_MAX_MS 7d cap — the trailing windows
|
|
146
|
-
* `tumwater report --since` / `tumwater report --failures --since` collect. (The dashboard's
|
|
147
|
-
* report tabs stop at 1d.) The label is what the hint line names; the
|
|
148
|
-
* length is what collectReportSince / collectFailureReportSince take. `null` is the default
|
|
149
|
-
* REPORT_DEFAULT_DAYS shape, kept first so the panes open on today's behavior. */
|
|
150
|
-
const REPORT_WINDOWS = [
|
|
151
|
-
[null, "default"],
|
|
152
|
-
[3_600_000, "1h"],
|
|
153
|
-
[21_600_000, "6h"],
|
|
154
|
-
[86_400_000, "1d"],
|
|
155
|
-
[REPORT_SINCE_MAX_MS, "7d"],
|
|
156
|
-
];
|
|
157
|
-
/** The Usage pane's body for one window and role scope: the paneCache the Ctrl+T/Ctrl+G/Ctrl+E
|
|
158
|
-
* handlers store. The default (null) read is byte-for-byte the day report the pane showed
|
|
159
|
-
* before the window cycle — collectReport at REPORT_DEFAULT_DAYS; a trailing window reads
|
|
160
|
-
* through the same collectReportSince collector `tumwater report --since` uses, so the TUI,
|
|
161
|
-
* CLI, and dashboard cannot disagree. Both honor the Ctrl+G role scope, so the two session
|
|
162
|
-
* cycles compose. */
|
|
163
|
-
function usagePaneCache(root, sinceMs, scope) {
|
|
164
|
-
if (sinceMs === null)
|
|
165
|
-
return renderReportMarkdown(collectReport(root, REPORT_DEFAULT_DAYS, scope));
|
|
166
|
-
return renderSinceReportMarkdown(collectReportSince(root, sinceMs, scope));
|
|
167
|
-
}
|
|
168
|
-
/** The Failures pane's body for one window and role scope, the digest sibling of
|
|
169
|
-
* usagePaneCache; renderFailureMarkdown renders both the day and --since shapes. */
|
|
170
|
-
function failuresPaneCache(root, sinceMs, scope) {
|
|
171
|
-
if (sinceMs === null)
|
|
172
|
-
return renderFailureMarkdown(collectFailureReport(root, REPORT_DEFAULT_DAYS, scope));
|
|
173
|
-
return renderFailureMarkdown(collectFailureReportSince(root, sinceMs, scope));
|
|
174
|
-
}
|
|
175
15
|
/** The framework-free owner of the TUI's keypress handling (extracted from runTui, and fed
|
|
176
16
|
* by ink's `useInput` through tui-keymap.ts's inkKeyToReadline adapter): the factory owns
|
|
177
|
-
* every mutable local the keypress dispatch used to keep in runTui's closure — the prompt line
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* seams it cannot own itself: the repo root the disk actions run against, the quit
|
|
181
|
-
* (Ctrl+D), a render request callback (every branch that painted a frame), and an
|
|
182
|
-
* so the flash expiry is testable without waiting the real 3 s. Rendering
|
|
183
|
-
* stays the caller: it syncs snapshot data and line budgets in, and reads
|
|
184
|
-
* out to compose the frame — the same data flow, one module boundary added.
|
|
17
|
+
* every mutable local the keypress dispatch used to keep in runTui's closure — the prompt line
|
|
18
|
+
* and its modal editors (owned by the tui-editors.ts sub-factory it constructs), the view and
|
|
19
|
+
* pane-navigation state, the flash notice, and the line budgets render feeds back. It takes
|
|
20
|
+
* only the seams it cannot own itself: the repo root the disk actions run against, the quit
|
|
21
|
+
* callback (Ctrl+D), a render request callback (every branch that painted a frame), and an
|
|
22
|
+
* injectable clock so the flash expiry is testable without waiting the real 3 s. Rendering
|
|
23
|
+
* (src/ui/tui/tui.tsx) stays the caller: it syncs snapshot data and line budgets in, and reads
|
|
24
|
+
* the resulting state out to compose the frame — the same data flow, one module boundary added.
|
|
25
|
+
* The prompt-line editor half of this closure lives in tui-editors.ts and the pane-navigation
|
|
26
|
+
* half in the tui-panes.ts factory beside its per-pane modules; this file keeps the outer
|
|
27
|
+
* dispatch, the flash notice, and the director gate. */
|
|
185
28
|
export function createTuiKeys(deps) {
|
|
186
29
|
const now = deps.now ?? Date.now;
|
|
187
30
|
const { root } = deps;
|
|
188
|
-
const collectPending = deps.collectPending ??
|
|
31
|
+
const collectPending = deps.collectPending ?? collectTeamChanges;
|
|
32
|
+
const runDoctorSeam = deps.runDoctor ?? ((r) => runDoctor(r, undefined, undefined, "never"));
|
|
189
33
|
const collectChange = deps.collectChange ?? collectRoleChange;
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
let cursor = 0;
|
|
196
|
-
// Ctrl+B budget-edit mode on the prompt line: while set, the line edits the daily cost cap
|
|
197
|
-
// (pre-filled with the current cap — empty when disabled) instead of a director prompt.
|
|
198
|
-
// The previous prompt text is saved so leaving the mode restores it byte-for-byte.
|
|
199
|
-
let budgetMode = false;
|
|
200
|
-
let savedInput = "";
|
|
201
|
-
let savedCursor = 0;
|
|
202
|
-
// Ctrl+R role-prompt mode on the prompt line: while set (to the viewed loop's role id), the
|
|
203
|
-
// line edits a prompt for that one loop instead of a director prompt. It keeps its OWN saved
|
|
204
|
-
// pair, separate from budget mode's: the two modes are mutually exclusive (each refuses to
|
|
205
|
-
// open while the other holds the line), and with separate pairs neither can clobber the
|
|
206
|
-
// other's saved draft even if a guard is ever loosened.
|
|
207
|
-
let rolePromptFor = null;
|
|
208
|
-
let roleSavedInput = "";
|
|
209
|
-
let roleSavedCursor = 0;
|
|
210
|
-
// Ctrl+L defer mode on the prompt line: while set, the line edits a duration that rides
|
|
211
|
-
// into the next delivered prompt as the same `tumwater:not-before` marker `tumwater prompt
|
|
212
|
-
// --at` and the dashboard composer write. deferMs is a DURATION, not an epoch: submit
|
|
213
|
-
// computes now() + deferMs, so a prompt queued later still defers by the full span. It is
|
|
214
|
-
// session-only and one-shot: a successful submit or an empty line clears it. Its own saved
|
|
215
|
-
// pair, mutually exclusive with the budget/role/search editors on the one prompt line.
|
|
216
|
-
let deferMs = null;
|
|
217
|
-
let deferMode = false;
|
|
218
|
-
let deferSavedInput = "";
|
|
219
|
-
let deferSavedCursor = 0;
|
|
220
|
-
let pauseEditMode = false;
|
|
221
|
-
let pauseEditRole = null;
|
|
222
|
-
let pauseEditWake = false;
|
|
223
|
-
let pauseSavedInput = "";
|
|
224
|
-
let pauseSavedCursor = 0;
|
|
225
|
-
// The prompt line's session history (tui-prompt-history.ts's rules): every successfully submitted
|
|
226
|
-
// prompt — director and per-role alike — joins it, and Up/Down walk it the way readline
|
|
227
|
-
// does. Not persisted: a TUI session starts blank, like a fresh shell.
|
|
228
|
-
let promptHistory = newPromptHistory();
|
|
229
|
-
// The last snapshot's cap, captured by render so the Ctrl+B handler can pre-fill without
|
|
230
|
-
// re-reading config itself (render already polls snapshot every second).
|
|
231
|
-
let currentCapUsd = 0;
|
|
232
|
-
// The last snapshot's free flag: true when every model the fleet could use is unpriced,
|
|
233
|
-
// so a cap could never bind — the Ctrl+B handler flashes a notice instead of opening
|
|
234
|
-
// the editor (BUGS.md 2026-09-14), leaving the prompt line untouched.
|
|
235
|
-
let currentBudgetFree = false;
|
|
34
|
+
const tickStat = deps.tickStat ?? tickDetailStat;
|
|
35
|
+
// The project-status pane's flat entry list (plans, bugs, questions, then the newest done plans
|
|
36
|
+
// and fixed bugs), read fresh — tui-backlog.ts's one definition, shared with the render so
|
|
37
|
+
// stale-selection clamping cannot drift.
|
|
38
|
+
const flat = () => browsableBacklogEntries(root);
|
|
236
39
|
let flash = "";
|
|
237
40
|
let flashUntil = 0;
|
|
238
41
|
// Flash a one-line notice on the prompt line: the single owner of the message+expiry
|
|
@@ -247,277 +50,53 @@ export function createTuiKeys(deps) {
|
|
|
247
50
|
const flashError = (err) => {
|
|
248
51
|
flashMessage(`error: ${errorMessage(err)}`);
|
|
249
52
|
};
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// The
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// The Queued pane's in-place editor (Ctrl+E): the role and queue-file basename of the row
|
|
275
|
-
// the prompt line is rewriting, with its own saved-draft pair exactly like the budget and
|
|
276
|
-
// role-prompt editors. All four editors share the one prompt line and are mutually
|
|
277
|
-
// exclusive, so no editor can clobber another's saved draft.
|
|
278
|
-
let queuedEditFor = null;
|
|
279
|
-
let queuedSavedInput = "";
|
|
280
|
-
let queuedSavedCursor = 0;
|
|
281
|
-
let answerFor = null;
|
|
282
|
-
let answerSavedInput = "";
|
|
283
|
-
let answerSavedCursor = 0;
|
|
284
|
-
let entryEditFor = null;
|
|
285
|
-
let entryEditSavedInput = "";
|
|
286
|
-
let entryEditSavedCursor = 0;
|
|
287
|
-
// The within-body scroll offset (line index of the window head, 0 = head) for the selected
|
|
288
|
-
// entry's body — PgDn/PgUp in project-status entry mode. Reset whenever the selection
|
|
289
|
-
// changes or clears, so every newly opened entry starts at its head.
|
|
290
|
-
let entryScroll = 0;
|
|
291
|
-
// The Markdown/row panes' (usage report, failures, history) rendered bodies, computed once
|
|
292
|
-
// per activation: the Ctrl+T handler sets it when cycling into one and nulls it on leaving.
|
|
293
|
-
// collectReport/collectFailureReport tail-scan events.jsonl and readTickRows reads the log,
|
|
294
|
-
// so they must not run on every second's re-render — those only re-window this string.
|
|
295
|
-
let paneCache = null;
|
|
296
|
-
// Ctrl+N toggles the viewed loop's pane between its live transcript and a read-only
|
|
297
|
-
// notebook view (its note and next tick's prompt). Like paneCache, noteCache is computed
|
|
298
|
-
// once on entry and only re-windowed each frame; both reset when the mode changes or the
|
|
299
|
-
// view cycles.
|
|
300
|
-
let noteMode = false;
|
|
301
|
-
let noteCache = null;
|
|
302
|
-
// Ctrl+N on the History pane swaps the row list for the newest completed tick's full event
|
|
303
|
-
// trail (renderTickDetail over readTickDetail) — the same text `tumwater tick --last` and
|
|
304
|
-
// the dashboard's History drill-down serve. Session-only like the other pane snapshots:
|
|
305
|
-
// computed once on entry and only re-windowed each frame.
|
|
306
|
-
let trailMode = false;
|
|
307
|
-
let trailCache = null;
|
|
308
|
-
// Ctrl+N's third step: the viewed loop's collected unlanded change (the CLI's
|
|
309
|
-
// `renderRoleChange` text). changeCache is null until the async collector resolves, and the
|
|
310
|
-
// pane paints a loading note meanwhile; both reset on leaving the mode or cycling the view.
|
|
311
|
-
let changeMode = false;
|
|
312
|
-
let changeCache = null;
|
|
313
|
-
// The within-body scroll offset (line index of the window head) shared by the two Markdown
|
|
314
|
-
// panes and the notebook view — PgDn/PgUp there. Reset to the head whenever the view is
|
|
315
|
-
// entered, like entryScroll.
|
|
316
|
-
let paneScroll = 0;
|
|
317
|
-
// The Activity pane's body-window start into the loaded event tail, or null to follow the
|
|
318
|
-
// live tail (the newest eventBudget events). PgUp pauses follow and pages back; PgDn returns
|
|
319
|
-
// toward the tail and re-enables follow at the bottom. Reset on every Ctrl+T view cycle, so
|
|
320
|
-
// re-entering Activity always starts at the live tail.
|
|
321
|
-
let activityScroll = null;
|
|
322
|
-
// The Activity pane's session exact-loop scope (Ctrl+G), a raw roster id or null for the
|
|
323
|
-
// fleet's full tail. Session-only like the view/scroll state; syncSnapshot clears it when the
|
|
324
|
-
// roster drops the id. The raw ids, not scopeCycle's base roles: the feed filters by exact
|
|
325
|
-
// loop (eventRole), so `feature-2` is its own slot.
|
|
326
|
-
let activityRole = null;
|
|
327
|
-
// The Activity pane's session free-text search (the `/` prompt-line editor), a
|
|
328
|
-
// case-insensitive substring over eventMatchesGrep's type-prefixed rendered line — the same
|
|
329
|
-
// rule `tumwater logs --grep` and the dashboard search use. null is no search; session-only
|
|
330
|
-
// like the scope, so the next read computes with it.
|
|
331
|
-
let activityGrep = null;
|
|
332
|
-
// The History pane's session trailing window (Ctrl+S), a HISTORY_WINDOWS length or null for
|
|
333
|
-
// the default newest-N read. Session-only like the view/scroll state; the next Ctrl+T into
|
|
334
|
-
// History computes the cache with it.
|
|
335
|
-
let historyWindow = null;
|
|
336
|
-
// The Usage and Failures panes' session trailing window (Ctrl+E), a REPORT_WINDOWS length or
|
|
337
|
-
// null for the default day report. Session-only like the view/scroll state; the next Ctrl+T
|
|
338
|
-
// into either pane computes the cache with it.
|
|
339
|
-
let reportWindow = null;
|
|
340
|
-
// The History pane's session outcome filter (Ctrl+O), a HISTORY_RESULTS class or null for no
|
|
341
|
-
// filter. Session-only like the window; the next Ctrl+T into History computes with it.
|
|
342
|
-
let historyResult = null;
|
|
343
|
-
// The History pane's session exact-loop scope (Ctrl+G), a raw roster id or null for the
|
|
344
|
-
// fleet's full read. Session-only like the window/result; syncSnapshot clears it when the
|
|
345
|
-
// roster drops the id. The raw ids, not scopeCycle's base roles: History filters by exact
|
|
346
|
-
// loop (`history --role`), so `feature-2` is its own slot.
|
|
347
|
-
let historyRole = null;
|
|
348
|
-
// The History pane's session free-text search (the `/` prompt-line editor), a
|
|
349
|
-
// case-insensitive substring over history-data.ts's tickRowGrepHaystack — the same rule
|
|
350
|
-
// `tumwater history --grep` and the dashboard search use. null is no search; session-only
|
|
351
|
-
// like the window/result/role, so the next Ctrl+T into History computes the cache with it.
|
|
352
|
-
let historyGrep = null;
|
|
353
|
-
// The History search editor on the prompt line: `/` enters it (only on History with an empty
|
|
354
|
-
// line), pre-filling the applied pattern; Enter applies it, Esc restores the draft. Its own
|
|
355
|
-
// saved-draft pair, exactly like the budget and role-prompt editors: the three share one
|
|
356
|
-
// prompt line and are mutually exclusive, so no editor can clobber another's saved draft.
|
|
357
|
-
let searchMode = false;
|
|
358
|
-
// Which pane the shared `/` editor feeds: History's tick search or Activity's event search.
|
|
359
|
-
// The target is captured when `/` opens so a roster change mid-edit cannot retarget Enter.
|
|
360
|
-
let searchFor = "history";
|
|
361
|
-
let searchSavedInput = "";
|
|
362
|
-
let searchSavedCursor = 0;
|
|
363
|
-
// The activity pane's current line budget, refreshed by every render so keypress handlers
|
|
364
|
-
// can page within it without re-deriving the height math. An open backlog entry spends the
|
|
365
|
-
// pane's first line on its title, so its body pages by one line less (entryBudget).
|
|
366
|
-
let eventBudget = 0;
|
|
367
|
-
let entryBudget = 0;
|
|
368
|
-
let roleIds = [];
|
|
53
|
+
// Commit an operator's backlog edit to main and flash when the commit did not land: the
|
|
54
|
+
// one home for the "written but NOT committed" warning every edit, file, cancel, and
|
|
55
|
+
// answer arm shares.
|
|
56
|
+
const commitBacklogEdit = (file, subject) => {
|
|
57
|
+
void commitOperatorBacklogEdit(root, file, stampedSubject("operator", subject)).then((committed) => {
|
|
58
|
+
if (!committed)
|
|
59
|
+
flashMessage(`${file} written but NOT committed to main — commit it before the next fast-forward`);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
// The pane-navigation state is constructed first because the prompt-line editors take its
|
|
63
|
+
// leaves (the queued refresh, the selection clear, the applied searches).
|
|
64
|
+
const panes = createTuiPanes({
|
|
65
|
+
root,
|
|
66
|
+
now,
|
|
67
|
+
requestRender: deps.requestRender,
|
|
68
|
+
flat,
|
|
69
|
+
flashMessage,
|
|
70
|
+
flashError,
|
|
71
|
+
commitBacklogEdit,
|
|
72
|
+
collectPending,
|
|
73
|
+
runDoctor: runDoctorSeam,
|
|
74
|
+
collectChange,
|
|
75
|
+
tickStat,
|
|
76
|
+
});
|
|
369
77
|
// Whether the director's status row is in flight, fed by render each frame: the gate for
|
|
370
78
|
// Ctrl+C's interrupt — with no director tick running, Ctrl+C flashes a notice instead of
|
|
371
79
|
// writing an abort marker nothing would consume.
|
|
372
80
|
let directorInFlight = false;
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (!rolePromptFor)
|
|
390
|
-
return;
|
|
391
|
-
rolePromptFor = null;
|
|
392
|
-
input = roleSavedInput;
|
|
393
|
-
cursor = roleSavedCursor;
|
|
394
|
-
promptHistory = resetPromptRecall(promptHistory);
|
|
395
|
-
};
|
|
396
|
-
// Leave defer-edit mode (Esc, Ctrl+L again, or Ctrl+T): restore the saved prompt text. The
|
|
397
|
-
// pending deferMs itself is untouched — Esc cancels the edit, not a delay already saved.
|
|
398
|
-
const exitDeferMode = () => {
|
|
399
|
-
if (!deferMode)
|
|
400
|
-
return;
|
|
401
|
-
deferMode = false;
|
|
402
|
-
input = deferSavedInput;
|
|
403
|
-
cursor = deferSavedCursor;
|
|
404
|
-
promptHistory = resetPromptRecall(promptHistory);
|
|
405
|
-
};
|
|
406
|
-
// Leave pause-edit mode (Esc, Ctrl+F again, a written pause, or Ctrl+T): restore the draft.
|
|
407
|
-
const exitPauseEditMode = () => {
|
|
408
|
-
if (!pauseEditMode)
|
|
409
|
-
return;
|
|
410
|
-
pauseEditMode = false;
|
|
411
|
-
pauseEditRole = null;
|
|
412
|
-
pauseEditWake = false;
|
|
413
|
-
input = pauseSavedInput;
|
|
414
|
-
cursor = pauseSavedCursor;
|
|
415
|
-
promptHistory = resetPromptRecall(promptHistory);
|
|
416
|
-
};
|
|
417
|
-
// Leave the backlog pane's answer editor (Esc, a recorded answer, or Ctrl+T): restore the
|
|
418
|
-
// saved prompt text and reset the recall state, mirroring exitQueuedEdit.
|
|
419
|
-
const exitAnswerMode = () => {
|
|
420
|
-
if (!answerFor)
|
|
421
|
-
return;
|
|
422
|
-
answerFor = null;
|
|
423
|
-
input = answerSavedInput;
|
|
424
|
-
cursor = answerSavedCursor;
|
|
425
|
-
promptHistory = resetPromptRecall(promptHistory);
|
|
426
|
-
};
|
|
427
|
-
// Leave the backlog pane's title editor (Esc, a saved edit, or Ctrl+T): the same settle
|
|
428
|
-
// contract as exitAnswerMode.
|
|
429
|
-
const exitEntryEdit = () => {
|
|
430
|
-
if (!entryEditFor)
|
|
431
|
-
return;
|
|
432
|
-
entryEditFor = null;
|
|
433
|
-
input = entryEditSavedInput;
|
|
434
|
-
cursor = entryEditSavedCursor;
|
|
435
|
-
promptHistory = resetPromptRecall(promptHistory);
|
|
436
|
-
};
|
|
437
|
-
// Leave the Queued pane's in-place editor (Esc, a saved edit, or Ctrl+T): restore the saved
|
|
438
|
-
// prompt text, and reset the recall state with it — the same settle contract the other
|
|
439
|
-
// editors honor.
|
|
440
|
-
const exitQueuedEdit = () => {
|
|
441
|
-
if (!queuedEditFor)
|
|
442
|
-
return;
|
|
443
|
-
queuedEditFor = null;
|
|
444
|
-
input = queuedSavedInput;
|
|
445
|
-
cursor = queuedSavedCursor;
|
|
446
|
-
promptHistory = resetPromptRecall(promptHistory);
|
|
447
|
-
};
|
|
448
|
-
/** Recompute the active pane's cached Markdown after `changed` state moved, re-entering the
|
|
449
|
-
* body at its head, and return whether the current view holds such a pane. "report" covers
|
|
450
|
-
* the Usage/Failures panes (fed by the report window and base-role scope); "history" the
|
|
451
|
-
* History pane (its window, outcome filter, exact-loop scope, and free-text search); "all"
|
|
452
|
-
* either. A view that is not the named pane is left untouched, so advancing one window
|
|
453
|
-
* redraws only the pane it feeds. The one home of the view-index→collector mapping the pane
|
|
454
|
-
* key handlers and syncSnapshot share. */
|
|
455
|
-
const refreshPaneCache = (changed) => {
|
|
456
|
-
const isReport = view === roleIds.length + 2 || view === roleIds.length + 3;
|
|
457
|
-
const isHistory = view === roleIds.length + 4;
|
|
458
|
-
if ((changed === "report" || changed === "all") && isReport) {
|
|
459
|
-
paneCache = view === roleIds.length + 2
|
|
460
|
-
? usagePaneCache(root, reportWindow, scopeRole ?? undefined)
|
|
461
|
-
: failuresPaneCache(root, reportWindow, scopeRole ?? undefined);
|
|
462
|
-
}
|
|
463
|
-
else if ((changed === "history" || changed === "all") && isHistory) {
|
|
464
|
-
paneCache = historyPaneCache(root, historyWindow, historyResult, historyRole, historyGrep);
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
return false;
|
|
468
|
-
}
|
|
469
|
-
paneScroll = 0;
|
|
470
|
-
return true;
|
|
471
|
-
};
|
|
472
|
-
// Re-read the pane-scoped newest completed tick into trailCache for an on-screen trail
|
|
473
|
-
// (Ctrl+N), so the trail cannot keep showing a retired loop's tick after Ctrl+G or a roster
|
|
474
|
-
// change drops the History scope. The reader throw is caught and flashed rather than escaping
|
|
475
|
-
// a key or sync path, the same guard the initial Ctrl+N entry uses.
|
|
476
|
-
const refreshTrail = () => {
|
|
477
|
-
try {
|
|
478
|
-
const row = readTickRows(root, 1, historyRole)[0];
|
|
479
|
-
const detail = row === undefined ? null : readTickDetail(root, row.loop, row.tick);
|
|
480
|
-
trailCache = detail === null ? null : renderTickDetail(detail);
|
|
481
|
-
}
|
|
482
|
-
catch (err) {
|
|
483
|
-
trailCache = null;
|
|
484
|
-
flashError(err);
|
|
485
|
-
}
|
|
486
|
-
};
|
|
487
|
-
// Re-read the Queued pane's rows and cached list after the queue changed (entry or cancel)
|
|
488
|
-
// and clamp the row selection to the shortened queue — a selection past the new end lands
|
|
489
|
-
// on the last remaining prompt, never on a stale role/position. An emptied queue clears it
|
|
490
|
-
// back to list mode. The refreshed list and any clamped-in row share paneScroll, so the
|
|
491
|
-
// offset resets too: a cancel that moves the selection opens the newly selected prompt at
|
|
492
|
-
// its head. The one home of the queuedRows/paneCache pairing the Ctrl+T entry and the Ctrl+X
|
|
493
|
-
// cancel share, so the pane's list and its selectable rows cannot drift.
|
|
494
|
-
const refreshQueued = () => {
|
|
495
|
-
queuedRows = queuedListRows(root, null, listWideScope(root));
|
|
496
|
-
paneCache = renderQueuedList(queuedRows, now());
|
|
497
|
-
selectedQueued = selectedQueued === null || queuedRows.length === 0
|
|
498
|
-
? null
|
|
499
|
-
: Math.min(selectedQueued, queuedRows.length - 1);
|
|
500
|
-
paneScroll = 0;
|
|
501
|
-
};
|
|
502
|
-
// Leave the shared search editor (Enter applies, Esc cancels, Ctrl+T cycles): restore the
|
|
503
|
-
// saved prompt text, and reset the recall state with it — the same settle contract the budget
|
|
504
|
-
// and role-prompt editors honor. The applied activityGrep/historyGrep itself persists across
|
|
505
|
-
// view cycling, like historyWindow/historyResult.
|
|
506
|
-
const exitHistorySearch = () => {
|
|
507
|
-
if (!searchMode)
|
|
508
|
-
return;
|
|
509
|
-
searchMode = false;
|
|
510
|
-
input = searchSavedInput;
|
|
511
|
-
cursor = searchSavedCursor;
|
|
512
|
-
promptHistory = resetPromptRecall(promptHistory);
|
|
513
|
-
};
|
|
81
|
+
// The prompt-line editors, constructed with the pane-side leaves they call on commit/exit:
|
|
82
|
+
// the disk writes, the flash pair, the selections/caches they return to, and the clock.
|
|
83
|
+
const editors = createTuiEditors({
|
|
84
|
+
root,
|
|
85
|
+
now,
|
|
86
|
+
flat,
|
|
87
|
+
refreshQueued: panes.refreshQueued,
|
|
88
|
+
refreshNotes: panes.refreshNotes,
|
|
89
|
+
refreshBrief: panes.refreshBrief,
|
|
90
|
+
clearBacklogSelection: panes.clearBacklogSelection,
|
|
91
|
+
applyHistorySearch: panes.applyHistorySearch,
|
|
92
|
+
applyActivitySearch: panes.applyActivitySearch,
|
|
93
|
+
flashMessage,
|
|
94
|
+
flashError,
|
|
95
|
+
commitBacklogEdit,
|
|
96
|
+
});
|
|
514
97
|
const handleKey = (str, key) => {
|
|
515
|
-
// Every key starts disarmed
|
|
516
|
-
|
|
517
|
-
const armed = cancelArmed;
|
|
518
|
-
cancelArmed = null;
|
|
519
|
-
const armedNotes = notesClearArmed;
|
|
520
|
-
notesClearArmed = null;
|
|
98
|
+
// Every key starts disarmed, whichever arm then consumes it.
|
|
99
|
+
panes.beginKey();
|
|
521
100
|
if (key.ctrl && key.name === "d") {
|
|
522
101
|
deps.quit();
|
|
523
102
|
return;
|
|
@@ -527,7 +106,7 @@ export function createTuiKeys(deps) {
|
|
|
527
106
|
// Ctrl+D): when the director row is in flight, write the same abort marker the
|
|
528
107
|
// CLI's `abort --role director` does, through the shared requestAbort core so the
|
|
529
108
|
// marker format and wording cannot drift; when none is, flash a notice and leave
|
|
530
|
-
// the
|
|
109
|
+
// the team untouched. A failed marker write flashes the reason instead of
|
|
531
110
|
// escaping the handler — the same contract the per-loop controls above honor.
|
|
532
111
|
if (!directorInFlight) {
|
|
533
112
|
flashMessage("no director task in flight");
|
|
@@ -544,1237 +123,45 @@ export function createTuiKeys(deps) {
|
|
|
544
123
|
deps.requestRender();
|
|
545
124
|
return;
|
|
546
125
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
// Mutually exclusive with the role-prompt, History-search, defer, and Queued-edit
|
|
553
|
-
// editors: all five share one prompt line, so a mode may not enter while another
|
|
554
|
-
// holds it. Flash the way out instead of silently dropping the held mode's saved
|
|
555
|
-
// draft (or parsing its text as a cap on Enter).
|
|
556
|
-
flashMessage(searchMode
|
|
557
|
-
? "finish or cancel the search first (Esc cancels)"
|
|
558
|
-
: pauseEditMode
|
|
559
|
-
? "finish or cancel the pause first (Esc cancels)"
|
|
560
|
-
: deferMode
|
|
561
|
-
? "finish or cancel the defer edit first (Esc cancels)"
|
|
562
|
-
: queuedEditFor
|
|
563
|
-
? "finish or cancel the queued edit first (Esc cancels)"
|
|
564
|
-
: answerFor
|
|
565
|
-
? "finish or cancel the answer first (Esc cancels)"
|
|
566
|
-
: entryEditFor
|
|
567
|
-
? "finish or cancel the entry edit first (Esc cancels)"
|
|
568
|
-
: `finish or cancel the prompt for ${rolePromptFor} first (Esc cancels)`);
|
|
569
|
-
deps.requestRender();
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
if (budgetMode) {
|
|
573
|
-
exitBudgetMode();
|
|
574
|
-
}
|
|
575
|
-
else if (currentBudgetFree) {
|
|
576
|
-
// An all-free fleet has no spend a cap could bind: the editor would pre-fill a
|
|
577
|
-
// value that can never take effect, so Ctrl+B flashes a one-line notice and
|
|
578
|
-
// leaves the prompt line untouched (toggle/Esc logic unchanged).
|
|
579
|
-
flashMessage("budget n/a — all models free");
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
// Mid-recall, hand the arrows' saved draft back before the cap editor takes the
|
|
583
|
-
// line — the same settle the role editor does — so leaving budget mode restores
|
|
584
|
-
// the draft, not a history entry, with no stale recall state riding along.
|
|
585
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
586
|
-
promptHistory = settled.history;
|
|
587
|
-
savedInput = settled.text;
|
|
588
|
-
savedCursor = settled.cursor;
|
|
589
|
-
budgetMode = true;
|
|
590
|
-
input = currentCapUsd > 0 ? String(currentCapUsd) : "";
|
|
591
|
-
cursor = input.length;
|
|
592
|
-
flashMessage("edit daily cost budget (USD): Enter to save, Esc to cancel");
|
|
593
|
-
}
|
|
594
|
-
deps.requestRender();
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
if (key.ctrl && key.name === "l") {
|
|
598
|
-
// Ctrl+L toggles defer-edit mode on the prompt line (mnemonic: **l**ater): the line edits
|
|
599
|
-
// a duration that rides into the next delivered prompt as the same `tumwater:not-before`
|
|
600
|
-
// marker `tumwater prompt --at` and the dashboard composer write. Entering pre-fills the
|
|
601
|
-
// pending delay (empty when none) and saves the prompt text; leaving restores it. Empty
|
|
602
|
-
// means "deliver now", so Esc cancels the edit without clearing an already-saved delay.
|
|
603
|
-
if (budgetMode || rolePromptFor || searchMode || pauseEditMode || queuedEditFor || answerFor || entryEditFor) {
|
|
604
|
-
// Mutually exclusive with the budget, role-prompt, History-search, pause, and Queued-edit
|
|
605
|
-
// editors: all five share one prompt line, so a mode may not enter while another
|
|
606
|
-
// holds it. Flash the way out instead of silently dropping the held mode's saved draft.
|
|
607
|
-
flashMessage(budgetMode
|
|
608
|
-
? "finish or cancel the budget edit first (Esc cancels)"
|
|
609
|
-
: pauseEditMode
|
|
610
|
-
? "finish or cancel the pause first (Esc cancels)"
|
|
611
|
-
: searchMode
|
|
612
|
-
? "finish or cancel the search first (Esc cancels)"
|
|
613
|
-
: queuedEditFor
|
|
614
|
-
? "finish or cancel the queued edit first (Esc cancels)"
|
|
615
|
-
: answerFor
|
|
616
|
-
? "finish or cancel the answer first (Esc cancels)"
|
|
617
|
-
: entryEditFor
|
|
618
|
-
? "finish or cancel the entry edit first (Esc cancels)"
|
|
619
|
-
: `finish or cancel the prompt for ${rolePromptFor} first (Esc cancels)`);
|
|
620
|
-
deps.requestRender();
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
if (deferMode) {
|
|
624
|
-
exitDeferMode();
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
// Mid-recall, hand the arrows' saved draft back before the defer editor takes the
|
|
628
|
-
// line — the same settle the budget and role editors do — so leaving restores the
|
|
629
|
-
// draft, not a history entry, with no stale recall state riding along.
|
|
630
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
631
|
-
promptHistory = settled.history;
|
|
632
|
-
deferSavedInput = settled.text;
|
|
633
|
-
deferSavedCursor = settled.cursor;
|
|
634
|
-
deferMode = true;
|
|
635
|
-
input = deferMs === null ? "" : durationLabel(deferMs);
|
|
636
|
-
cursor = input.length;
|
|
637
|
-
flashMessage("defer the next prompt: a duration like 45m (empty sends now) — Enter to save, Esc to cancel");
|
|
638
|
-
}
|
|
639
|
-
deps.requestRender();
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
if (key.ctrl && key.name === "f" && !budgetMode && !rolePromptFor && !searchMode && !deferMode && !queuedEditFor && !answerFor && !entryEditFor) {
|
|
643
|
-
// Ctrl+F toggles the whole-fleet pause (mnemonic: **F**leet) through the same
|
|
644
|
-
// pauseFleet/resumeFleet marker writers `tumwater pause`/`resume` and the dashboard's
|
|
645
|
-
// /api/pause call use, so the surfaces cannot drift on marker format or idempotence.
|
|
646
|
-
// The shared fleetPausePhrase/fleetResumePhrase base lines keep the flash wording the
|
|
647
|
-
// event feed and failure digest already read; a no-op flashes the same
|
|
648
|
-
// already-paused/was-not-paused contract the CLI prints. Guarded out of the budget and
|
|
649
|
-
// role-prompt editors, whose prompt line the fleet toggle must not disturb — the same
|
|
650
|
-
// inertness the per-loop keys honor below. The write can fail (a lock timeout, a torn
|
|
651
|
-
// fs), so flash the reason instead of letting the throw kill the TUI.
|
|
652
|
-
if (pauseEditMode) {
|
|
653
|
-
exitPauseEditMode();
|
|
654
|
-
deps.requestRender();
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
try {
|
|
658
|
-
if (isFleetPaused(root)) {
|
|
659
|
-
flashMessage(resumeFleet(root) ? fleetResumePhrase() : "fleet was not paused");
|
|
660
|
-
}
|
|
661
|
-
else {
|
|
662
|
-
// An unpaused fleet opens the one-line editor: "[duration] [reason]", empty pauses
|
|
663
|
-
// until resumed. A paused fleet resumes at once.
|
|
664
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
665
|
-
promptHistory = settled.history;
|
|
666
|
-
pauseSavedInput = settled.text;
|
|
667
|
-
pauseSavedCursor = settled.cursor;
|
|
668
|
-
pauseEditMode = true;
|
|
669
|
-
input = "";
|
|
670
|
-
cursor = 0;
|
|
671
|
-
flashMessage('pause the fleet: [duration] [reason], e.g. "2h deploy freeze" (empty pauses until resumed) — Enter to pause, Esc to cancel');
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
catch (err) {
|
|
675
|
-
flashError(err);
|
|
676
|
-
}
|
|
677
|
-
deps.requestRender();
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
// Per-loop controls on the transcript pane: Ctrl+P
|
|
681
|
-
// toggles the viewed loop's pause, Ctrl+A aborts its in-flight tick, Ctrl+W opens the wake editor,
|
|
682
|
-
// and Ctrl+R opens the role-prompt editor for it.
|
|
683
|
-
// They call the same marker-writing/submit cores the CLI's --role flags do, so the
|
|
684
|
-
// surfaces cannot drift on marker format, idempotence, or wording. Guarded to the
|
|
685
|
-
// transcript views and out of budget-edit mode; everywhere else the keys fall through
|
|
686
|
-
// to applyKey, where only Ctrl+U (kill to line start) and Ctrl+K (kill to line end)
|
|
687
|
-
// edit the prompt line and the rest of these ctrl-key presses stay inert.
|
|
688
|
-
// Every branch is a disk write that can fail (a lock timeout, a torn fs), and an
|
|
689
|
-
// unguarded throw would escape this keypress handler and kill the TUI — flash the
|
|
690
|
-
// reason instead, the same contract the prompt-submit path below honors.
|
|
691
|
-
if (key.ctrl && (key.name === "p" || key.name === "w") && pauseEditRole !== null) {
|
|
692
|
-
// A second Ctrl+P or Ctrl+W closes the per-loop pause/wake editor, mirroring Ctrl+F's second press.
|
|
693
|
-
exitPauseEditMode();
|
|
694
|
-
deps.requestRender();
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
if (!budgetMode &&
|
|
698
|
-
!deferMode && !pauseEditMode &&
|
|
699
|
-
!queuedEditFor && !answerFor && !entryEditFor &&
|
|
700
|
-
roleIds.length > 0 &&
|
|
701
|
-
view >= 1 && view <= roleIds.length &&
|
|
702
|
-
key.ctrl && (key.name === "p" || key.name === "a" || key.name === "w" || key.name === "r")) {
|
|
703
|
-
const role = roleIds[view - 1]; // defined: view is clamped inside the transcript range
|
|
704
|
-
try {
|
|
705
|
-
if (key.name === "r") {
|
|
706
|
-
// Ctrl+R toggles role-prompt mode for the viewed loop (mnemonic: p**R**ompt —
|
|
707
|
-
// Ctrl+P is taken by pause): entering saves the director draft in this mode's own
|
|
708
|
-
// pair and blanks the line; entering again (or Esc) restores it. Mutually
|
|
709
|
-
// exclusive with budget mode by the outer guard and the mirrored refusal in the
|
|
710
|
-
// Ctrl+B branch.
|
|
711
|
-
if (rolePromptFor) {
|
|
712
|
-
exitRolePromptMode();
|
|
713
|
-
flashMessage("role prompt cancelled");
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
// Mid-recall, the line holds a history entry while the real draft sits in the
|
|
717
|
-
// recall state: settle first, so the role editor saves (and later restores) the
|
|
718
|
-
// draft the operator was writing, and no stale index/draft crosses modes —
|
|
719
|
-
// otherwise Esc here would surface role-mode recalls on the director line.
|
|
720
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
721
|
-
promptHistory = settled.history;
|
|
722
|
-
roleSavedInput = settled.text;
|
|
723
|
-
roleSavedCursor = settled.cursor;
|
|
724
|
-
rolePromptFor = role;
|
|
725
|
-
input = "";
|
|
726
|
-
cursor = 0;
|
|
727
|
-
flashMessage(`prompt for ${role}: Enter to send, Esc to cancel`);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
else if (key.name === "p") {
|
|
731
|
-
// Toggle by the marker's current state, read fresh: pauseRole/resumeRole's false
|
|
732
|
-
// return means another window raced us to the same state, and the wording
|
|
733
|
-
// helpers render that honestly — the changed-state contract the CLI prints.
|
|
734
|
-
if (pausedRoles(root).includes(role)) {
|
|
735
|
-
flashMessage(roleResumeMessage(root, role, resumeRole(root, role)));
|
|
736
|
-
}
|
|
737
|
-
else if (rolePromptFor || searchMode) {
|
|
738
|
-
flashMessage(rolePromptFor
|
|
739
|
-
? "finish or cancel the role prompt first (Esc cancels)"
|
|
740
|
-
: "finish or cancel the search first (Esc cancels)");
|
|
741
|
-
}
|
|
742
|
-
else {
|
|
743
|
-
// An unpaused loop opens the one-line duration editor; nothing is written until
|
|
744
|
-
// Enter. Empty pauses until resumed.
|
|
745
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
746
|
-
promptHistory = settled.history;
|
|
747
|
-
pauseSavedInput = settled.text;
|
|
748
|
-
pauseSavedCursor = settled.cursor;
|
|
749
|
-
pauseEditMode = true;
|
|
750
|
-
pauseEditRole = role;
|
|
751
|
-
input = "";
|
|
752
|
-
cursor = 0;
|
|
753
|
-
flashMessage(`pause ${role}: [duration], e.g. "2h" (empty pauses until resumed) — Enter to pause, Esc to cancel`);
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
else if (key.name === "a") {
|
|
757
|
-
const result = requestAbort(root, role);
|
|
758
|
-
flashMessage(result.ok ? result.message : `error: ${result.error}`);
|
|
759
|
-
}
|
|
760
|
-
else if (rolePromptFor || searchMode) {
|
|
761
|
-
flashMessage(rolePromptFor
|
|
762
|
-
? "finish or cancel the role prompt first (Esc cancels)"
|
|
763
|
-
: "finish or cancel the search first (Esc cancels)");
|
|
764
|
-
}
|
|
765
|
-
else {
|
|
766
|
-
// Ctrl+W opens the one-line wake editor; nothing is written until Enter. Empty wakes now.
|
|
767
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
768
|
-
promptHistory = settled.history;
|
|
769
|
-
pauseSavedInput = settled.text;
|
|
770
|
-
pauseSavedCursor = settled.cursor;
|
|
771
|
-
pauseEditMode = true;
|
|
772
|
-
pauseEditRole = role;
|
|
773
|
-
pauseEditWake = true;
|
|
774
|
-
input = "";
|
|
775
|
-
cursor = 0;
|
|
776
|
-
flashMessage(`wake ${role}: [duration], e.g. "2h", or "cancel" (empty wakes now) — Enter to apply, Esc to cancel`);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
catch (err) {
|
|
780
|
-
flashError(err);
|
|
781
|
-
}
|
|
782
|
-
deps.requestRender();
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
if (key.ctrl && key.name === "n") {
|
|
786
|
-
// On the Backlog view Ctrl+N files a new entry: it opens the title editor with a null
|
|
787
|
-
// position (new plan), and pressed again inside that editor flips plan <-> bug.
|
|
788
|
-
if (entryEditFor !== null && entryEditFor.position === null) {
|
|
789
|
-
const other = entryEditFor.kind === "plan" ? "bug" : "plan";
|
|
790
|
-
entryEditFor = { ...entryEditFor, kind: other };
|
|
791
|
-
flashMessage(`new ${other} — Enter to file, Ctrl+N switches to ${other === "plan" ? "bug" : "plan"}, Esc cancels`);
|
|
792
|
-
deps.requestRender();
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
if (view === roleIds.length + 1 &&
|
|
796
|
-
!budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor) {
|
|
797
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
798
|
-
promptHistory = settled.history;
|
|
799
|
-
entryEditSavedInput = settled.text;
|
|
800
|
-
entryEditSavedCursor = settled.cursor;
|
|
801
|
-
entryEditFor = { kind: "plan", position: null, title: "" };
|
|
802
|
-
input = "";
|
|
803
|
-
cursor = 0;
|
|
804
|
-
flashMessage("new plan — Enter to file, Ctrl+N switches to bug, Esc cancels");
|
|
805
|
-
deps.requestRender();
|
|
806
|
-
return;
|
|
807
|
-
}
|
|
808
|
-
// Ctrl+N cycles the viewed loop's pane live transcript → notebook → change → transcript
|
|
809
|
-
// (PLANS.md "Inspect a loop's notebook and next tick prompt in the TUI" and "Show a
|
|
810
|
-
// loop's pending change in the TUI transcript pane"). The change step renders the same
|
|
811
|
-
// collectRoleChange/renderRoleChange view the CLI's `tumwater diff --role <id>` and the
|
|
812
|
-
// dashboard drawer use. Guarded to a transcript view and out of budget-edit and
|
|
813
|
-
// role-prompt modes, the same way the per-loop controls below are; everywhere else the
|
|
814
|
-
// key falls through to applyKey. On the History pane the same key swaps the row list for
|
|
815
|
-
// one tick's full event trail instead (the CLI's `tumwater tick --last` text), so it is
|
|
816
|
-
// handled before the transcript guard below.
|
|
817
|
-
if (view === roleIds.length + 4 && !budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor) {
|
|
818
|
-
if (trailMode) {
|
|
819
|
-
trailMode = false;
|
|
820
|
-
trailCache = null;
|
|
821
|
-
}
|
|
822
|
-
else {
|
|
823
|
-
// The newest completed tick in the pane's scope: the loop's when Ctrl+G set
|
|
824
|
-
// historyRole, else the fleet's. A log holding none flashes and leaves the rows.
|
|
825
|
-
const row = readTickRows(root, 1, historyRole)[0];
|
|
826
|
-
if (row === undefined) {
|
|
827
|
-
flashMessage("no completed ticks to inspect");
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
// readTickDetail reads the log and renderTickDetail renders it; a throw (an
|
|
831
|
-
// injected seam or an unexpected failure) flashes the reason and leaves the rows
|
|
832
|
-
// shown rather than escaping the handler and killing the TUI.
|
|
833
|
-
try {
|
|
834
|
-
const detail = readTickDetail(root, row.loop, row.tick);
|
|
835
|
-
trailCache = detail === null ? null : renderTickDetail(detail);
|
|
836
|
-
trailMode = true;
|
|
837
|
-
}
|
|
838
|
-
catch (err) {
|
|
839
|
-
trailCache = null;
|
|
840
|
-
flashError(err);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
paneScroll = 0; // either way the pane starts at its head
|
|
845
|
-
deps.requestRender();
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
if (budgetMode || rolePromptFor || deferMode || pauseEditMode || queuedEditFor || answerFor || entryEditFor || roleIds.length === 0 || view < 1 || view > roleIds.length) {
|
|
849
|
-
deps.requestRender();
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
const role = roleIds[view - 1]; // defined: view is clamped inside the transcript range
|
|
853
|
-
if (changeMode) {
|
|
854
|
-
changeMode = false;
|
|
855
|
-
changeCache = null;
|
|
856
|
-
}
|
|
857
|
-
else if (noteMode) {
|
|
858
|
-
// notebook → change: reset the notebook snapshot and kick off the collector
|
|
859
|
-
// fire-and-forget, caching the rendered text when it resolves (the Pending pane's
|
|
860
|
-
// pattern). Guard on the change pane still being current — changeMode on, same role
|
|
861
|
-
// at the same index — so a cycle away or a roster shift cannot overwrite the pane now
|
|
862
|
-
// shown. The default collector degrades rather than throws, so the rejection arm only
|
|
863
|
-
// covers an injected seam (or an unexpected throw).
|
|
864
|
-
noteMode = false;
|
|
865
|
-
noteCache = null;
|
|
866
|
-
changeMode = true;
|
|
867
|
-
changeCache = null;
|
|
868
|
-
const target = role;
|
|
869
|
-
void collectChange(root, target).then((change) => {
|
|
870
|
-
if (!changeMode || roleIds[view - 1] !== target)
|
|
871
|
-
return;
|
|
872
|
-
changeCache = renderRoleChange(change);
|
|
873
|
-
deps.requestRender();
|
|
874
|
-
}, (err) => {
|
|
875
|
-
if (!changeMode || roleIds[view - 1] !== target)
|
|
876
|
-
return;
|
|
877
|
-
flashError(err);
|
|
878
|
-
deps.requestRender();
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
else {
|
|
882
|
-
// rolePayload reads the notebook and assembles the next tick's prompt off disk; a
|
|
883
|
-
// throw leaves the transcript shown (the mode stays off) and flashes the reason
|
|
884
|
-
// instead of escaping the handler and killing the TUI.
|
|
885
|
-
try {
|
|
886
|
-
noteCache = roleInspectMarkdown(root, role);
|
|
887
|
-
noteMode = true;
|
|
888
|
-
}
|
|
889
|
-
catch (err) {
|
|
890
|
-
noteCache = null;
|
|
891
|
-
flashError(err);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
paneScroll = 0; // either way the pane starts at its head
|
|
895
|
-
deps.requestRender();
|
|
896
|
-
return;
|
|
897
|
-
}
|
|
898
|
-
if (key.ctrl && key.name === "g" && !budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor) {
|
|
899
|
-
// Ctrl+G cycles the active pane's loop scope (mnemonic: the **g**lobal scope / the
|
|
900
|
-
// **g**roup). Guarded out of the budget and role-prompt editors, whose prompt line the
|
|
901
|
-
// key must not disturb — the same inertness the per-loop keys honor. On the History
|
|
902
|
-
// pane it walks the raw roster ids (`[null, ...roleIds]`) so an instance id is its own
|
|
903
|
-
// slot, matching `history --role` and the dashboard picker; on every other view it cycles
|
|
904
|
-
// the base-role report scope the Usage and Failures collectors aggregate by, exactly as
|
|
905
|
-
// before. A pane showing the scope recomputes its cached Markdown and re-enters the body
|
|
906
|
-
// at its head; every other view keeps the scope for the Ctrl+T entry that later computes
|
|
907
|
-
// with it.
|
|
908
|
-
if (view === 0) {
|
|
909
|
-
// The Activity feed scopes over the same raw roster ids, so `feature-2` is its own
|
|
910
|
-
// slot; the filter reads exact loop ids through eventRole. The scroll resets because
|
|
911
|
-
// its offset indexed the old (unfiltered or differently filtered) list.
|
|
912
|
-
const slots = [null, ...roleIds];
|
|
913
|
-
const slot = slots.indexOf(activityRole);
|
|
914
|
-
activityRole = slots[(slot + 1) % slots.length] ?? null;
|
|
915
|
-
activityScroll = null;
|
|
916
|
-
}
|
|
917
|
-
else if (view === roleIds.length + 4) {
|
|
918
|
-
const slots = [null, ...roleIds];
|
|
919
|
-
const slot = slots.indexOf(historyRole);
|
|
920
|
-
historyRole = slots[(slot + 1) % slots.length] ?? null;
|
|
921
|
-
// A trail already on screen follows the scope: re-read the new scope's newest
|
|
922
|
-
// completed tick (or drop to the pane's no-trail note when it has none) so the trail
|
|
923
|
-
// and the tab strip cannot name different loops.
|
|
924
|
-
if (trailMode)
|
|
925
|
-
refreshTrail();
|
|
926
|
-
}
|
|
927
|
-
else {
|
|
928
|
-
const slots = scopeCycle(roleIds);
|
|
929
|
-
const slot = slots.indexOf(scopeRole);
|
|
930
|
-
scopeRole = slots[(slot + 1) % slots.length] ?? null;
|
|
931
|
-
}
|
|
932
|
-
refreshPaneCache("all");
|
|
933
|
-
deps.requestRender();
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
if (key.ctrl && key.name === "e" &&
|
|
937
|
-
!budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && queuedEditFor === null && !answerFor && !entryEditFor &&
|
|
938
|
-
view === roleIds.length + 6 && selectedQueued !== null) {
|
|
939
|
-
// Ctrl+E opens the Queued pane's in-place editor on the selected row (mnemonic:
|
|
940
|
-
// **E**dit) through the same readQueuedPromptBody/editQueuedFile pair the dashboard's
|
|
941
|
-
// Edit uses: the body is pre-filled marker- and attachment-stripped, and Enter writes
|
|
942
|
-
// it back to the same queue file, so its position, enqueue stamp, deferral, and
|
|
943
|
-
// attached images survive. A row that vanished between selection and this press (the
|
|
944
|
-
// loop dequeued it, or another operator cancelled it) flashes the gone notice and
|
|
945
|
-
// refreshes the list. A no-selection press (list mode) falls through to the report
|
|
946
|
-
// window below, exactly as before.
|
|
947
|
-
try {
|
|
948
|
-
const position = Math.min(selectedQueued, queuedRows.length - 1);
|
|
949
|
-
const row = queuedRows[position];
|
|
950
|
-
if (row !== undefined) {
|
|
951
|
-
const body = readQueuedPromptBody(root, row.role, row.file);
|
|
952
|
-
if (body === null) {
|
|
953
|
-
flashMessage(`queued prompt #${row.position} for ${row.role} is gone`);
|
|
954
|
-
refreshQueued();
|
|
955
|
-
}
|
|
956
|
-
else {
|
|
957
|
-
// Mid-recall, the line holds a history entry while the real draft sits in the
|
|
958
|
-
// recall state: settle first, so the queued editor saves (and later restores)
|
|
959
|
-
// the draft the operator was writing.
|
|
960
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
961
|
-
promptHistory = settled.history;
|
|
962
|
-
queuedSavedInput = settled.text;
|
|
963
|
-
queuedSavedCursor = settled.cursor;
|
|
964
|
-
queuedEditFor = { role: row.role, file: row.file };
|
|
965
|
-
input = body;
|
|
966
|
-
cursor = body.length;
|
|
967
|
-
flashMessage(`editing queued prompt #${row.position} for ${row.role} — Enter to save, Esc to cancel`);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
catch (err) {
|
|
972
|
-
flashError(err);
|
|
973
|
-
}
|
|
974
|
-
deps.requestRender();
|
|
975
|
-
return;
|
|
976
|
-
}
|
|
977
|
-
if (key.ctrl && key.name === "a" &&
|
|
978
|
-
!budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor &&
|
|
979
|
-
view === roleIds.length + 1 && selectedEntry !== null) {
|
|
980
|
-
// Ctrl+A opens the answer editor on the selected open question (mnemonic: **A**nswer);
|
|
981
|
-
// Enter records the decision through tryAnswerQuestion, the core `tumwater questions
|
|
982
|
-
// answer` and the dashboard's form share. The position is the question's 1-based index
|
|
983
|
-
// among the open questions: labeledBacklogEntries lists plans, then bugs, then questions.
|
|
984
|
-
try {
|
|
985
|
-
const entries = flat();
|
|
986
|
-
const sel = Math.min(selectedEntry, entries.length - 1);
|
|
987
|
-
const entry = entries[sel];
|
|
988
|
-
if (entry?.label !== "question") {
|
|
989
|
-
flashMessage("only an open question can be answered here");
|
|
990
|
-
}
|
|
991
|
-
else {
|
|
992
|
-
const position = sel - entries.filter((e) => e.label !== "question").length + 1;
|
|
993
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
994
|
-
promptHistory = settled.history;
|
|
995
|
-
answerSavedInput = settled.text;
|
|
996
|
-
answerSavedCursor = settled.cursor;
|
|
997
|
-
answerFor = { position, title: entry.title };
|
|
998
|
-
input = "";
|
|
999
|
-
cursor = 0;
|
|
1000
|
-
flashMessage(`answering question #${position} — Enter to record, Esc to cancel`);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
catch (err) {
|
|
1004
|
-
flashError(err);
|
|
1005
|
-
}
|
|
1006
|
-
deps.requestRender();
|
|
1007
|
-
return;
|
|
1008
|
-
}
|
|
1009
|
-
if (key.ctrl && key.name === "e" &&
|
|
1010
|
-
!budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor &&
|
|
1011
|
-
view === roleIds.length + 1 && selectedEntry !== null) {
|
|
1012
|
-
// Ctrl+E opens the title editor on the selected plan or bug (mnemonic: **E**dit); Enter
|
|
1013
|
-
// saves through tryEditEntry, the non-exiting core the dashboard drawer shares, keeping
|
|
1014
|
-
// the body and stamp. The prompt line is single-line, so only the title is editable here.
|
|
1015
|
-
try {
|
|
1016
|
-
const entries = flat();
|
|
1017
|
-
const sel = Math.min(selectedEntry, entries.length - 1);
|
|
1018
|
-
const picked = entries[sel];
|
|
1019
|
-
if (picked === undefined) {
|
|
1020
|
-
// an emptied list has nothing to edit
|
|
1021
|
-
}
|
|
1022
|
-
else if (picked.label === "question") {
|
|
1023
|
-
flashMessage("only a plan or open bug can be edited here");
|
|
1024
|
-
}
|
|
1025
|
-
else {
|
|
1026
|
-
const kind = picked.label === "plan" ? "plan" : "bug";
|
|
1027
|
-
const position = kind === "plan" ? sel + 1 : sel - entries.filter((e) => e.label === "plan").length + 1;
|
|
1028
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
1029
|
-
promptHistory = settled.history;
|
|
1030
|
-
entryEditSavedInput = settled.text;
|
|
1031
|
-
entryEditSavedCursor = settled.cursor;
|
|
1032
|
-
entryEditFor = { kind, position, title: picked.title };
|
|
1033
|
-
input = stripEntryStamp(picked.title);
|
|
1034
|
-
cursor = input.length;
|
|
1035
|
-
flashMessage(`editing ${kind} #${position} — Enter to save, Esc to cancel`);
|
|
1036
|
-
}
|
|
1037
|
-
}
|
|
1038
|
-
catch (err) {
|
|
1039
|
-
flashError(err);
|
|
1040
|
-
}
|
|
1041
|
-
deps.requestRender();
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
if (key.ctrl && key.name === "e" && !budgetMode && !rolePromptFor && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor) {
|
|
1045
|
-
// Ctrl+E advances the Usage and Failures panes' trailing report window (mnemonic:
|
|
1046
|
-
// **E**lapsed). Guarded out of the budget and role-prompt editors like the other
|
|
1047
|
-
// mode-sensitive keys. The window is session state: advancing it off either report pane
|
|
1048
|
-
// only changes the label, and the next Ctrl+T into one computes the cache with it; on a
|
|
1049
|
-
// report pane this handler recomputes the cached Markdown from the newly windowed
|
|
1050
|
-
// collector and re-enters the body at its head.
|
|
1051
|
-
reportWindow = nextInCycle(REPORT_WINDOWS, reportWindow);
|
|
1052
|
-
refreshPaneCache("report");
|
|
1053
|
-
deps.requestRender();
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
if (key.ctrl && key.name === "s" && !budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor) {
|
|
1057
|
-
// Ctrl+S advances the History pane's trailing window (mnemonic: **S**ince). Guarded out
|
|
1058
|
-
// of budget-edit and role-prompt modes like the other mode-sensitive keys. The window is
|
|
1059
|
-
// session state: advancing it off the History view only changes the label, and the next
|
|
1060
|
-
// Ctrl+T into History computes the cache with it; on the History view this handler
|
|
1061
|
-
// re-reads the rows now and resets the scroll so the changed body re-enters at its head.
|
|
1062
|
-
historyWindow = nextInCycle(HISTORY_WINDOWS, historyWindow);
|
|
1063
|
-
refreshPaneCache("history");
|
|
1064
|
-
deps.requestRender();
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
if (key.ctrl && key.name === "o" && !budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor) {
|
|
1068
|
-
// Ctrl+O advances the History pane's outcome filter (mnemonic: **O**utcome). Guarded out
|
|
1069
|
-
// of budget-edit and role-prompt modes like Ctrl+S beside it. The class is session state:
|
|
1070
|
-
// advancing it off the History view only changes the label, and the next Ctrl+T into
|
|
1071
|
-
// History computes the cache with it; on the History view this handler re-reads and
|
|
1072
|
-
// re-filters the rows now and resets the scroll so the changed body re-enters at its head.
|
|
1073
|
-
historyResult = nextInCycle(HISTORY_RESULTS, historyResult);
|
|
1074
|
-
refreshPaneCache("history");
|
|
1075
|
-
deps.requestRender();
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
if (key.ctrl && key.name === "x" && noteMode &&
|
|
1079
|
-
!budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor &&
|
|
1080
|
-
view >= 1 && view <= roleIds.length) {
|
|
1081
|
-
// Ctrl+X twice clears the viewed loop's notebook through the writer the CLI's
|
|
1082
|
-
// `--clear-notes` and the dashboard drawer share, then re-renders the notebook view.
|
|
1083
|
-
const role = roleIds[view - 1];
|
|
1084
|
-
if (armedNotes === role) {
|
|
1085
|
-
try {
|
|
1086
|
-
flashMessage(`${role}: ${saveRoleNote(roleNotesPath(root, role), "")}`);
|
|
1087
|
-
noteCache = roleInspectMarkdown(root, role);
|
|
1088
|
-
paneScroll = 0;
|
|
1089
|
-
}
|
|
1090
|
-
catch (err) {
|
|
1091
|
-
flashError(err);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
else {
|
|
1095
|
-
notesClearArmed = role;
|
|
1096
|
-
flashMessage(`press Ctrl+X again to clear ${role}'s notebook — any other key keeps it`);
|
|
1097
|
-
}
|
|
1098
|
-
deps.requestRender();
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
if (key.ctrl && key.name === "x" &&
|
|
1102
|
-
!budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor &&
|
|
1103
|
-
view === roleIds.length + 6 && selectedQueued !== null) {
|
|
1104
|
-
// Ctrl+X cancels the selected queued prompt through the CLI's own per-loop path
|
|
1105
|
-
// (`tumwater prompt --cancel <n> --role <id>`): cancelRolePrompt removes that loop's
|
|
1106
|
-
// Nth queue file and logs the prompt_cancelled event, so the pane, the CLI, and the
|
|
1107
|
-
// dashboard cancel cannot drift. Then re-read the queue so the pane's list and rows
|
|
1108
|
-
// reflect the removal and the selection clamps. A no-selection press (list mode), and
|
|
1109
|
-
// every other view, leaves the queue untouched.
|
|
1110
|
-
try {
|
|
1111
|
-
const row = queuedRows[Math.min(selectedQueued, queuedRows.length - 1)];
|
|
1112
|
-
if (row !== undefined) {
|
|
1113
|
-
const outcome = cancelRolePrompt(root, row.role, row.position);
|
|
1114
|
-
// A gone outcome is the concurrent-dequeue race (cancelRolePrompt's own contract),
|
|
1115
|
-
// not a failure: flash it honestly and still refresh the listing.
|
|
1116
|
-
flashMessage(outcome.status === "cancelled"
|
|
1117
|
-
? `cancelled queued prompt #${row.position} for ${row.role}`
|
|
1118
|
-
: `queued prompt #${row.position} for ${row.role} was already gone`);
|
|
1119
|
-
refreshQueued();
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
catch (err) {
|
|
1123
|
-
flashError(err);
|
|
1124
|
-
}
|
|
1125
|
-
deps.requestRender();
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
if (key.ctrl && key.name === "x" &&
|
|
1129
|
-
!budgetMode && !rolePromptFor && !searchMode && !deferMode && !pauseEditMode && !queuedEditFor && !answerFor && !entryEditFor &&
|
|
1130
|
-
view === roleIds.length + 1) {
|
|
1131
|
-
// Ctrl+X twice cancels the selected plan or bug through the non-exiting core the
|
|
1132
|
-
// dashboard drawer uses; the title check inside tryCancelEntry refuses a stale selection.
|
|
1133
|
-
if (selectedEntry !== null) {
|
|
1134
|
-
const entries = flat();
|
|
1135
|
-
const sel = Math.min(selectedEntry, entries.length - 1);
|
|
1136
|
-
const picked = entries[sel];
|
|
1137
|
-
if (picked === undefined) {
|
|
1138
|
-
// an emptied list has nothing to cancel
|
|
1139
|
-
}
|
|
1140
|
-
else if (picked.label === "question") {
|
|
1141
|
-
flashMessage("only a plan or open bug can be cancelled here");
|
|
1142
|
-
}
|
|
1143
|
-
else {
|
|
1144
|
-
const kind = picked.label === "plan" ? "plan" : "bug";
|
|
1145
|
-
const position = kind === "plan" ? sel + 1 : sel - entries.filter((e) => e.label === "plan").length + 1;
|
|
1146
|
-
const target = { kind, position, title: picked.title };
|
|
1147
|
-
if (armed !== null && armed.kind === kind && armed.position === position) {
|
|
1148
|
-
// The armed title (not the re-read one) goes to tryCancelEntry, so an entry that
|
|
1149
|
-
// changed since the arm reports `moved` instead of being cancelled.
|
|
1150
|
-
try {
|
|
1151
|
-
const outcome = tryCancelEntry(root, kind, position, armed.title);
|
|
1152
|
-
selectedEntry = null;
|
|
1153
|
-
entryScroll = 0;
|
|
1154
|
-
flashMessage(outcome.status === "cancelled"
|
|
1155
|
-
? `cancelled ${kind} #${position}: ${target.title}`
|
|
1156
|
-
: `that ${kind} was already removed or moved — nothing cancelled`);
|
|
1157
|
-
}
|
|
1158
|
-
catch (err) {
|
|
1159
|
-
flashError(err);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
else {
|
|
1163
|
-
cancelArmed = target;
|
|
1164
|
-
flashMessage(`press Ctrl+X again to cancel ${kind} #${position} — any other key keeps it`);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
126
|
+
// The prompt-line editors own every arm that reads or writes the one prompt line — the
|
|
127
|
+
// toggles and per-loop controls that open them, the escape/Enter arms that close and
|
|
128
|
+
// commit them, and the `/` search. Route the key to them first; when they consumed it,
|
|
129
|
+
// the dispatch is done (they always render on a handled key).
|
|
130
|
+
if (editors.handleKey(str, key, panes.editorCtx())) {
|
|
1168
131
|
deps.requestRender();
|
|
1169
132
|
return;
|
|
1170
133
|
}
|
|
1171
134
|
if (key.ctrl && key.name === "t") {
|
|
1172
|
-
// Ctrl+T exits
|
|
1173
|
-
// never strands
|
|
1174
|
-
// mode composes the same way: cycling away restores the saved
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
exitPauseEditMode();
|
|
1179
|
-
exitHistorySearch();
|
|
1180
|
-
exitQueuedEdit();
|
|
1181
|
-
exitAnswerMode();
|
|
1182
|
-
exitEntryEdit();
|
|
1183
|
-
noteMode = false; // a cycle away leaves notebook and change modes and drops their snapshots
|
|
1184
|
-
noteCache = null;
|
|
1185
|
-
changeMode = false;
|
|
1186
|
-
changeCache = null;
|
|
1187
|
-
trailMode = false; // the tick trail is History-only; a cycle away drops its snapshot too
|
|
1188
|
-
trailCache = null;
|
|
1189
|
-
view = (view + 1) % (roleIds.length + 7); // events → each loop's transcript → project status → usage report → failures → history → pending → queued → events
|
|
1190
|
-
selectedEntry = null; // leaving a view drops any entry selection…
|
|
1191
|
-
selectedQueued = null; // …and any queued-row selection, so re-entering starts at the list
|
|
1192
|
-
entryScroll = 0; // …and its within-body scroll, so re-entering starts at the list/head
|
|
1193
|
-
paneScroll = 0; // the Markdown panes always re-enter at their head
|
|
1194
|
-
activityScroll = null; // Activity always re-enters at the live tail
|
|
1195
|
-
// Compute the pane's Markdown once per activation (this handler renders immediately), and
|
|
1196
|
-
// drop it on leaving — a visit's first frame is fresh, later frames only re-window.
|
|
1197
|
-
if (!refreshPaneCache("all")) {
|
|
1198
|
-
if (view === roleIds.length + 5) {
|
|
1199
|
-
// Pending is the cycle's one async collector: collectFleetChanges awaits git plumbing,
|
|
1200
|
-
// so kick it off fire-and-forget, cache the rendered text when it resolves, and repaint.
|
|
1201
|
-
// The durable land queue needs no git plumbing, so read and render it through the CLI's
|
|
1202
|
-
// own pair (`tumwater queue`) UP FRONT: the queue paints on entry, and the git-backed
|
|
1203
|
-
// roster is appended below it when the collector resolves, so the three surfaces
|
|
1204
|
-
// (CLI, dashboard, TUI) cannot drift on order, age, skips, or wording.
|
|
1205
|
-
// Guard on the PENDING pane still being current — view === roleIds.length + 5 — not just
|
|
1206
|
-
// on the raw index: an operator who cycled away, or a roster change that shifted which
|
|
1207
|
-
// pane the same index names, must not have this resolution overwrite the pane now shown.
|
|
1208
|
-
// The default collector degrades rather than throws, so the rejection arm only covers an
|
|
1209
|
-
// injected seam (or an unexpected throw).
|
|
1210
|
-
const queueText = renderLandingQueue(readLandingQueue(root), now());
|
|
1211
|
-
paneCache = queueText;
|
|
1212
|
-
void collectPending(root).then((fleet) => {
|
|
1213
|
-
if (view !== roleIds.length + 5)
|
|
1214
|
-
return;
|
|
1215
|
-
paneCache = queueText + "\n\n" + renderFleetChange(fleet);
|
|
1216
|
-
deps.requestRender();
|
|
1217
|
-
}, (err) => {
|
|
1218
|
-
if (view !== roleIds.length + 5)
|
|
1219
|
-
return;
|
|
1220
|
-
// Keep the queue visible rather than replacing it with a loading note: it is
|
|
1221
|
-
// already known and unaffected by the git read that failed.
|
|
1222
|
-
flashError(err);
|
|
1223
|
-
deps.requestRender();
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
else if (view === roleIds.length + 6) {
|
|
1227
|
-
// Queued is a synchronous read of the same queue list `tumwater prompt --list`
|
|
1228
|
-
// prints (renderQueuedList over queuedListRows), so it paints on entry with no
|
|
1229
|
-
// async collector. The now() clock keeps the age/countdown suffix testable.
|
|
1230
|
-
refreshQueued();
|
|
1231
|
-
}
|
|
1232
|
-
else {
|
|
1233
|
-
paneCache = null;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
deps.requestRender();
|
|
135
|
+
// Ctrl+T exits every open editor too — view cycling is orthogonal to them, so a cycle
|
|
136
|
+
// never strands an editor's pre-filled text in the prompt line. Each editor restores its
|
|
137
|
+
// saved draft. Role-prompt mode composes the same way: cycling away restores the saved
|
|
138
|
+
// director draft.
|
|
139
|
+
editors.exitAll();
|
|
140
|
+
panes.cycleView();
|
|
1237
141
|
return;
|
|
1238
142
|
}
|
|
1239
|
-
if (
|
|
1240
|
-
(view === 0 || view === roleIds.length + 4) && input === "") {
|
|
1241
|
-
// `/` opens a free-text search on the prompt line (the pager's search key; this project
|
|
1242
|
-
// already reserves `/` for a meta action): over the History pane's tick rows on History,
|
|
1243
|
-
// over the Activity feed's events on Activity. Guarded to those two views and to an empty
|
|
1244
|
-
// line, so elsewhere it types into the prompt as today and a director draft is never
|
|
1245
|
-
// disturbed. Pre-fills the applied pattern so Enter re-submits it and the operator can
|
|
1246
|
-
// edit it; Esc cancels without applying.
|
|
1247
|
-
const settled = settlePromptRecall(promptHistory, input, cursor);
|
|
1248
|
-
promptHistory = settled.history;
|
|
1249
|
-
searchSavedInput = settled.text;
|
|
1250
|
-
searchSavedCursor = settled.cursor;
|
|
1251
|
-
searchFor = view === 0 ? "activity" : "history";
|
|
1252
|
-
searchMode = true;
|
|
1253
|
-
input = (searchFor === "activity" ? activityGrep : historyGrep) ?? "";
|
|
1254
|
-
cursor = input.length;
|
|
1255
|
-
flashMessage(searchFor === "activity" ? "search events: Enter to filter, Esc to cancel" : "search ticks: Enter to filter, Esc to cancel");
|
|
1256
|
-
deps.requestRender();
|
|
143
|
+
if (panes.handleKey(key, editors.state()))
|
|
1257
144
|
return;
|
|
1258
|
-
}
|
|
1259
145
|
const arrow = arrowDir(key.name);
|
|
1260
|
-
|
|
1261
|
-
if (view === roleIds.length + 1 && arrow !== null) {
|
|
1262
|
-
// In the project-status pane, up/down browse entries in full instead of editing the
|
|
1263
|
-
// prompt; every other view keeps today's behavior (arrows are ignored by applyKey).
|
|
1264
|
-
// An open answer editor pins the selection (its position was taken from it).
|
|
1265
|
-
if (!answerFor && !entryEditFor) {
|
|
1266
|
-
const count = flat().length;
|
|
1267
|
-
selectedEntry = moveEntrySelection(count, selectedEntry, arrow);
|
|
1268
|
-
entryScroll = 0; // a newly opened entry starts at its body's head
|
|
1269
|
-
}
|
|
1270
|
-
deps.requestRender();
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
if (view === roleIds.length + 6 && arrow !== null) {
|
|
1274
|
-
// In the Queued pane, up/down select a queued prompt row in full (wrapping, the backlog
|
|
1275
|
-
// pane's rule) instead of editing the prompt; an empty queue has nothing to select, so
|
|
1276
|
-
// moveEntrySelection clears the selection. Every other view keeps today's behavior.
|
|
1277
|
-
// The selected body shares paneScroll with the list, so a newly opened row must start
|
|
1278
|
-
// at its head (the status pane's entryScroll reset on the same move), or it renders
|
|
1279
|
-
// scrolled into the middle of the previous row's body.
|
|
1280
|
-
selectedQueued = moveEntrySelection(queuedRows.length, selectedQueued, arrow);
|
|
1281
|
-
paneScroll = 0;
|
|
1282
|
-
deps.requestRender();
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
if (page !== null && view === 0) {
|
|
1286
|
-
// The Activity pane keeps the live tail by default; PgUp pauses follow and pages back
|
|
1287
|
-
// through the loaded event tail, PgDn returns toward it and re-enables follow once the
|
|
1288
|
-
// window reaches the tail. Both directions clamp to [0, total - eventBudget], and a log
|
|
1289
|
-
// with eventBudget or fewer events stays on the tail. The read is the pane's own fixed
|
|
1290
|
-
// tail term (and active loop/text filter), so it hits readEvents' stat cache rather than
|
|
1291
|
-
// rescanning the log.
|
|
1292
|
-
const total = activityEvents(root, activityRole, activityGrep).length;
|
|
1293
|
-
const maxStart = Math.max(0, total - eventBudget);
|
|
1294
|
-
const current = activityScroll ?? maxStart;
|
|
1295
|
-
const next = stepEntryScroll(current, total, eventBudget, page);
|
|
1296
|
-
activityScroll = next >= maxStart ? null : next;
|
|
1297
|
-
deps.requestRender();
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
if (page !== null && (noteMode || changeMode || trailMode)) {
|
|
1301
|
-
// Within-body scroll in the notebook, change, and tick-trail views: PgDn/PgUp page the
|
|
1302
|
-
// cached inspection Markdown, clamped at both ends, exactly like the usage/failures
|
|
1303
|
-
// panes below.
|
|
1304
|
-
const total = (noteMode ? (noteCache ?? "") : trailMode ? (trailCache ?? "") : (changeCache ?? "")).split("\n").length;
|
|
1305
|
-
paneScroll = stepEntryScroll(paneScroll, total, eventBudget, page);
|
|
146
|
+
if (arrow !== null && editors.onArrow(arrow)) {
|
|
1306
147
|
deps.requestRender();
|
|
1307
148
|
return;
|
|
1308
149
|
}
|
|
1309
|
-
|
|
1310
|
-
// Within-body scroll in the Markdown panes (usage report, failures): PgDn/PgUp page the
|
|
1311
|
-
// cached string, clamped at both ends. The total is derived from the cache, mirroring how
|
|
1312
|
-
// entry mode derives it from e.body; every other view ignores these keys as today.
|
|
1313
|
-
const total = (paneCache ?? "").split("\n").length;
|
|
1314
|
-
paneScroll = stepEntryScroll(paneScroll, total, eventBudget, page);
|
|
1315
|
-
deps.requestRender();
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
if (view === roleIds.length + 1 && page !== null) {
|
|
1319
|
-
// Within-body scroll in project-status ENTRY mode: PgDn/PgUp page the selected entry's
|
|
1320
|
-
// body, clamped at both ends. No-op in list mode and for bodies that fit — every other
|
|
1321
|
-
// view ignores these keys as today (applyKey drops them).
|
|
1322
|
-
if (selectedEntry !== null && !answerFor && !entryEditFor) {
|
|
1323
|
-
const entries = flat();
|
|
1324
|
-
const sel = entries.length > 0 ? Math.min(selectedEntry, entries.length - 1) : null;
|
|
1325
|
-
const body = sel === null ? "" : entries[sel].body;
|
|
1326
|
-
entryScroll = stepEntryScroll(entryScroll, body ? body.split("\n").length : 0, entryBudget, page);
|
|
1327
|
-
}
|
|
1328
|
-
deps.requestRender();
|
|
1329
|
-
return;
|
|
1330
|
-
}
|
|
1331
|
-
if (!budgetMode && !deferMode && !pauseEditMode && !searchMode && arrow !== null) {
|
|
1332
|
-
// Up/Down walk the submitted-prompt history the way readline does (tui-prompt-history.ts's
|
|
1333
|
-
// recall rules). The backlog pane keeps the arrows for entry browsing (its branch
|
|
1334
|
-
// above already returned), and budget and defer modes keep them out — recalling a
|
|
1335
|
-
// prompt into the cap or delay field would be a paste, not a recall. No history in
|
|
1336
|
-
// this direction: fall through to applyKey, which ignores arrows as before.
|
|
1337
|
-
const recalled = recallPromptHistory(promptHistory, input, cursor, key.name === "up" ? "older" : "newer");
|
|
1338
|
-
if (recalled) {
|
|
1339
|
-
promptHistory = recalled.history;
|
|
1340
|
-
input = recalled.text;
|
|
1341
|
-
cursor = recalled.cursor;
|
|
1342
|
-
}
|
|
1343
|
-
}
|
|
1344
|
-
if (key.name === "escape" && selectedQueued !== null && view === roleIds.length + 6 &&
|
|
1345
|
-
!budgetMode && !deferMode && !pauseEditMode && !rolePromptFor && !searchMode && !queuedEditFor && !answerFor && !entryEditFor) {
|
|
1346
|
-
// Esc on the Queued pane returns it to list mode; the cached list is untouched (nothing
|
|
1347
|
-
// was cancelled), so the pane shows the same renderQueuedList it entered with. The
|
|
1348
|
-
// shared paneScroll resets with the body switch, like a selection move.
|
|
1349
|
-
selectedQueued = null;
|
|
1350
|
-
paneScroll = 0;
|
|
1351
|
-
deps.requestRender();
|
|
1352
|
-
return;
|
|
1353
|
-
}
|
|
1354
|
-
if (key.name === "escape" && searchMode) {
|
|
1355
|
-
// Esc cancels the History search without applying it; the applied pattern is unchanged.
|
|
1356
|
-
exitHistorySearch();
|
|
1357
|
-
deps.requestRender();
|
|
1358
|
-
return;
|
|
1359
|
-
}
|
|
1360
|
-
if (key.name === "escape" && budgetMode) {
|
|
1361
|
-
// Esc cancels budget-edit mode, restoring the previous prompt text.
|
|
1362
|
-
exitBudgetMode();
|
|
1363
|
-
deps.requestRender();
|
|
1364
|
-
return;
|
|
1365
|
-
}
|
|
1366
|
-
if (key.name === "escape" && rolePromptFor) {
|
|
1367
|
-
// Esc cancels role-prompt mode the same way, restoring the previous prompt text.
|
|
1368
|
-
exitRolePromptMode();
|
|
1369
|
-
deps.requestRender();
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
|
-
if (key.name === "escape" && pauseEditMode) {
|
|
1373
|
-
// Esc cancels the pause editor without writing, restoring the previous prompt text.
|
|
1374
|
-
exitPauseEditMode();
|
|
1375
|
-
deps.requestRender();
|
|
1376
|
-
return;
|
|
1377
|
-
}
|
|
1378
|
-
if (key.name === "escape" && deferMode) {
|
|
1379
|
-
// Esc cancels the defer editor, restoring the previous prompt text and leaving any
|
|
1380
|
-
// already-saved pending delay untouched.
|
|
1381
|
-
exitDeferMode();
|
|
1382
|
-
deps.requestRender();
|
|
1383
|
-
return;
|
|
1384
|
-
}
|
|
1385
|
-
if (key.name === "escape" && entryEditFor) {
|
|
1386
|
-
// Esc cancels the title editor, restoring the previous prompt text; nothing is written.
|
|
1387
|
-
const filing = entryEditFor.position === null;
|
|
1388
|
-
exitEntryEdit();
|
|
1389
|
-
flashMessage(filing ? "new entry cancelled" : "edit cancelled");
|
|
1390
|
-
deps.requestRender();
|
|
1391
|
-
return;
|
|
1392
|
-
}
|
|
1393
|
-
if (key.name === "escape" && answerFor) {
|
|
1394
|
-
// Esc cancels the answer editor, restoring the previous prompt text; nothing is written.
|
|
1395
|
-
exitAnswerMode();
|
|
1396
|
-
flashMessage("answer cancelled");
|
|
1397
|
-
deps.requestRender();
|
|
1398
|
-
return;
|
|
1399
|
-
}
|
|
1400
|
-
if (key.name === "escape" && queuedEditFor) {
|
|
1401
|
-
// Esc cancels the Queued pane's in-place editor, restoring the previous prompt text.
|
|
1402
|
-
exitQueuedEdit();
|
|
1403
|
-
deps.requestRender();
|
|
1404
|
-
return;
|
|
1405
|
-
}
|
|
1406
|
-
if (key.name === "return") {
|
|
1407
|
-
if (searchMode) {
|
|
1408
|
-
// Enter applies the trimmed line to the pane the editor opened over: Activity sets
|
|
1409
|
-
// activityGrep (an empty line clears it) and resets the scroll to the live tail — the
|
|
1410
|
-
// offset indexed the old filtered list; History recomputes its row cache now (the pane
|
|
1411
|
-
// is showing) and re-enters at its head. A successful apply restores the saved prompt
|
|
1412
|
-
// draft through exitHistorySearch, the shared editor's exit.
|
|
1413
|
-
const pattern = input.trim();
|
|
1414
|
-
if (searchFor === "activity") {
|
|
1415
|
-
activityGrep = pattern === "" ? null : pattern;
|
|
1416
|
-
exitHistorySearch();
|
|
1417
|
-
activityScroll = null;
|
|
1418
|
-
flashMessage(activityGrep === null ? "search cleared" : `search: "${activityGrep}"`);
|
|
1419
|
-
}
|
|
1420
|
-
else {
|
|
1421
|
-
historyGrep = pattern === "" ? null : pattern;
|
|
1422
|
-
exitHistorySearch();
|
|
1423
|
-
paneCache = historyPaneCache(root, historyWindow, historyResult, historyRole, historyGrep);
|
|
1424
|
-
paneScroll = 0;
|
|
1425
|
-
flashMessage(historyGrep === null ? "search cleared" : `search: "${historyGrep}"`);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
else if (budgetMode) {
|
|
1429
|
-
// Enter in budget mode parses + saves the cap through the shared setter (which
|
|
1430
|
-
// writes tumwater.json atomically; the running orchestrator picks it up on its next
|
|
1431
|
-
// ~2 s poll). An invalid value flashes and STAYS in edit mode so the operator can
|
|
1432
|
-
// fix it; success restores the saved prompt text.
|
|
1433
|
-
const parsed = parseBudgetInput(input);
|
|
1434
|
-
if (!parsed.ok) {
|
|
1435
|
-
flashMessage(parsed.error);
|
|
1436
|
-
}
|
|
1437
|
-
else {
|
|
1438
|
-
const result = setDailyBudgetUsd(root, parsed.value);
|
|
1439
|
-
if (result.ok) {
|
|
1440
|
-
exitBudgetMode();
|
|
1441
|
-
currentCapUsd = parsed.value; // the next render's snapshot agrees within ~2 s
|
|
1442
|
-
flashMessage(parsed.value === 0
|
|
1443
|
-
? "budget disabled"
|
|
1444
|
-
: `budget set to ${usdCap(parsed.value)}`);
|
|
1445
|
-
}
|
|
1446
|
-
else {
|
|
1447
|
-
flashMessage(result.error); // broken config or write failure — stay in edit mode
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
else if (pauseEditMode) {
|
|
1452
|
-
// Enter in pause-edit mode parses "[duration] [reason]" and writes the same marker
|
|
1453
|
-
// `tumwater pause --for/--reason` does. An invalid entry flashes and STAYS in the editor.
|
|
1454
|
-
const target = pauseEditRole;
|
|
1455
|
-
const wakeParsed = pauseEditWake && target !== null ? parseRoleWakeInput(input) : null;
|
|
1456
|
-
const parsed = target !== null ? parseRolePauseInput(input) : parsePauseInput(input);
|
|
1457
|
-
if (wakeParsed) {
|
|
1458
|
-
if (!wakeParsed.ok) {
|
|
1459
|
-
flashMessage(wakeParsed.error);
|
|
1460
|
-
}
|
|
1461
|
-
else {
|
|
1462
|
-
try {
|
|
1463
|
-
const message = wakeParsed.cancel ? cancelWakes(root, target) : requestWake(root, [target], wakeParsed.inMs);
|
|
1464
|
-
exitPauseEditMode();
|
|
1465
|
-
flashMessage(message);
|
|
1466
|
-
}
|
|
1467
|
-
catch (err) {
|
|
1468
|
-
flashError(err);
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
else if (!parsed.ok) {
|
|
1473
|
-
flashMessage(parsed.error);
|
|
1474
|
-
}
|
|
1475
|
-
else if (target !== null) {
|
|
1476
|
-
try {
|
|
1477
|
-
const at = now();
|
|
1478
|
-
const until = parsed.forMs === undefined ? undefined : at + parsed.forMs;
|
|
1479
|
-
const changed = pauseRole(root, target, until);
|
|
1480
|
-
exitPauseEditMode();
|
|
1481
|
-
flashMessage(rolePauseMessage(root, target, changed, until === undefined ? undefined : { ms: parsed.forMs, untilMs: until }, at));
|
|
1482
|
-
}
|
|
1483
|
-
catch (err) {
|
|
1484
|
-
flashError(err);
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
else {
|
|
1488
|
-
const fleet = parsed;
|
|
1489
|
-
try {
|
|
1490
|
-
const until = fleet.forMs === undefined ? undefined : now() + fleet.forMs;
|
|
1491
|
-
const changed = pauseFleet(root, until, fleet.reason);
|
|
1492
|
-
exitPauseEditMode();
|
|
1493
|
-
const forSuffix = fleet.forMs === undefined ? "" : ` for ${durationLabel(fleet.forMs)}`;
|
|
1494
|
-
flashMessage(changed || until !== undefined
|
|
1495
|
-
? `${fleetPausePhrase()}${forSuffix}${changed && fleet.reason ? `: ${fleet.reason}` : ""}`
|
|
1496
|
-
: "fleet is already paused");
|
|
1497
|
-
}
|
|
1498
|
-
catch (err) {
|
|
1499
|
-
flashError(err);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
}
|
|
1503
|
-
else if (deferMode) {
|
|
1504
|
-
// Enter in defer mode parses the duration: empty clears the pending delay (deliver
|
|
1505
|
-
// now), a valid value stores the session duration (applied as now() + deferMs at
|
|
1506
|
-
// submit), and an invalid spelling or an over-cap value flashes and STAYS in the
|
|
1507
|
-
// editor so the operator can fix it. Either way the saved draft is restored.
|
|
1508
|
-
const parsed = parseDeferInput(input);
|
|
1509
|
-
if (!parsed.ok) {
|
|
1510
|
-
flashMessage(parsed.error);
|
|
1511
|
-
}
|
|
1512
|
-
else {
|
|
1513
|
-
deferMs = parsed.ms;
|
|
1514
|
-
exitDeferMode();
|
|
1515
|
-
flashMessage(deferMs === null ? "defer cleared" : `next prompt delivers in ${durationLabel(deferMs)}`);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
else if (rolePromptFor) {
|
|
1519
|
-
// Enter in role-prompt mode submits for the viewed loop through the same path
|
|
1520
|
-
// `tumwater prompt --role` uses: the shared submitRolePromptAndWake enqueues into
|
|
1521
|
-
// that loop's own queue (length-capped by the shared rule), logs under the loop,
|
|
1522
|
-
// and wakes it so a live fleet's loop comes in within one poll. An invalid
|
|
1523
|
-
// (whitespace-only) line flashes and STAYS in edit mode; a failed queue write
|
|
1524
|
-
// flashes the reason and keeps the text, the same contract the director path
|
|
1525
|
-
// below honors; success restores the saved draft.
|
|
1526
|
-
const parsed = parseRolePromptInput(input);
|
|
1527
|
-
if (!parsed.ok) {
|
|
1528
|
-
flashMessage(parsed.error);
|
|
1529
|
-
}
|
|
1530
|
-
else {
|
|
1531
|
-
const role = rolePromptFor;
|
|
1532
|
-
try {
|
|
1533
|
-
const pending = deferMs;
|
|
1534
|
-
submitRolePromptAndWake(root, role, parsed.value, undefined, pending === null ? undefined : now() + pending);
|
|
1535
|
-
promptHistory = pushPromptHistory(promptHistory, parsed.value);
|
|
1536
|
-
deferMs = null;
|
|
1537
|
-
exitRolePromptMode();
|
|
1538
|
-
flashMessage(pending === null
|
|
1539
|
-
? `queued for the ${role} loop`
|
|
1540
|
-
: `queued for the ${role} loop — delivers in ${durationLabel(pending)}`);
|
|
1541
|
-
}
|
|
1542
|
-
catch (err) {
|
|
1543
|
-
flashError(err);
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
else if (entryEditFor) {
|
|
1548
|
-
// Enter saves the new title through tryEditEntry with the body re-read now, so a
|
|
1549
|
-
// concurrent body edit is not clobbered by a stale copy. The entry is matched by flat
|
|
1550
|
-
// index and title; a mismatch is treated as moved without writing. A blank title and a
|
|
1551
|
-
// throw flash and STAY in the editor so the text survives.
|
|
1552
|
-
const target = entryEditFor;
|
|
1553
|
-
if (!input.trim()) {
|
|
1554
|
-
flashMessage("an entry needs a title");
|
|
1555
|
-
}
|
|
1556
|
-
else if (target.position === null) {
|
|
1557
|
-
// New entry (Ctrl+N): file by title with an empty body and wake its loop, the same
|
|
1558
|
-
// steps handleBacklogFile takes; a length problem or a throw stays in the editor.
|
|
1559
|
-
try {
|
|
1560
|
-
const { role, wakeText } = OPERATOR_FILE_KINDS[target.kind];
|
|
1561
|
-
const problem = promptLengthProblem(wakeText(collapseWhitespace(input)), role);
|
|
1562
|
-
if (problem) {
|
|
1563
|
-
flashMessage(problem);
|
|
1564
|
-
}
|
|
1565
|
-
else {
|
|
1566
|
-
const filed = fileEntryWithBody(root, target.kind, input, "");
|
|
1567
|
-
submitRolePromptAndWake(root, role, wakeText(filed.title));
|
|
1568
|
-
exitEntryEdit();
|
|
1569
|
-
flashMessage(`filed ${target.kind}: ${filed.title} — woke ${role}`);
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
catch (err) {
|
|
1573
|
-
flashError(err);
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
else {
|
|
1577
|
-
try {
|
|
1578
|
-
const entries = flat();
|
|
1579
|
-
const plans = entries.filter((e) => e.label === "plan").length;
|
|
1580
|
-
const entry = entries[target.kind === "plan" ? target.position - 1 : plans + target.position - 1];
|
|
1581
|
-
const outcome = entry?.label === target.kind && entry.title === target.title
|
|
1582
|
-
? tryEditEntry(root, target.kind, target.position, target.title, input, entry.body)
|
|
1583
|
-
: { status: "moved", title: "" };
|
|
1584
|
-
exitEntryEdit();
|
|
1585
|
-
if (outcome.status === "edited") {
|
|
1586
|
-
flashMessage(`edited ${target.kind} #${target.position}: ${outcome.title}`);
|
|
1587
|
-
}
|
|
1588
|
-
else {
|
|
1589
|
-
selectedEntry = null;
|
|
1590
|
-
entryScroll = 0;
|
|
1591
|
-
flashMessage(`that ${target.kind} was already removed or moved — nothing edited`);
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
catch (err) {
|
|
1595
|
-
flashError(err);
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
}
|
|
1599
|
-
else if (answerFor) {
|
|
1600
|
-
// Enter records the answer through tryAnswerQuestion, titled so a question that moved
|
|
1601
|
-
// since Ctrl+A is refused. A whitespace-only line, an unclosed fence, and a throw all
|
|
1602
|
-
// flash and STAY in the editor so the text survives; answered and missing/moved both
|
|
1603
|
-
// leave the editor and return the pane to list mode (the entry left the list).
|
|
1604
|
-
const target = answerFor;
|
|
1605
|
-
if (!input.trim()) {
|
|
1606
|
-
flashMessage("questions answer needs a decision");
|
|
1607
|
-
}
|
|
1608
|
-
else {
|
|
1609
|
-
try {
|
|
1610
|
-
const outcome = tryAnswerQuestion(root, target.position, input, target.title);
|
|
1611
|
-
if (outcome.status === "fenced") {
|
|
1612
|
-
flashMessage(UNCLOSED_FENCE_MESSAGE);
|
|
1613
|
-
}
|
|
1614
|
-
else {
|
|
1615
|
-
exitAnswerMode();
|
|
1616
|
-
selectedEntry = null;
|
|
1617
|
-
flashMessage(outcome.status === "answered"
|
|
1618
|
-
? `answered question #${target.position}: ${outcome.title}`
|
|
1619
|
-
: "that question was already answered or moved — nothing recorded");
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
catch (err) {
|
|
1623
|
-
flashError(err);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
else if (queuedEditFor) {
|
|
1628
|
-
// Enter saves the queued editor's line through the same file-addressed
|
|
1629
|
-
// editQueuedFile pair the dashboard's Edit uses: the write targets the queue file
|
|
1630
|
-
// the row named, so a concurrent dequeue cannot land it on the wrong prompt. An
|
|
1631
|
-
// invalid (whitespace-only) line flashes and STAYS in the editor; a gone outcome
|
|
1632
|
-
// is the concurrent-dequeue race, flashed honestly and returned to the list; a
|
|
1633
|
-
// throw keeps the text in the editor for a retry.
|
|
1634
|
-
const parsed = parseRolePromptInput(input);
|
|
1635
|
-
if (!parsed.ok) {
|
|
1636
|
-
flashMessage(parsed.error);
|
|
1637
|
-
}
|
|
1638
|
-
else {
|
|
1639
|
-
const target = queuedEditFor;
|
|
1640
|
-
try {
|
|
1641
|
-
const outcome = editQueuedFile(root, target.role, target.file, parsed.value);
|
|
1642
|
-
exitQueuedEdit();
|
|
1643
|
-
refreshQueued();
|
|
1644
|
-
flashMessage(outcome.status === "edited"
|
|
1645
|
-
? `edited queued prompt for ${target.role}`
|
|
1646
|
-
: "queued prompt was already gone");
|
|
1647
|
-
}
|
|
1648
|
-
catch (err) {
|
|
1649
|
-
flashError(err);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
else {
|
|
1654
|
-
const prompt = input.trim();
|
|
1655
|
-
if (!prompt) {
|
|
1656
|
-
// Whitespace-only input queues nothing; drop it like an empty line.
|
|
1657
|
-
input = "";
|
|
1658
|
-
cursor = 0;
|
|
1659
|
-
}
|
|
1660
|
-
else {
|
|
1661
|
-
try {
|
|
1662
|
-
const pending = deferMs;
|
|
1663
|
-
submitPrompt(root, prompt, undefined, pending === null ? undefined : now() + pending);
|
|
1664
|
-
promptHistory = pushPromptHistory(promptHistory, prompt);
|
|
1665
|
-
deferMs = null;
|
|
1666
|
-
input = "";
|
|
1667
|
-
cursor = 0;
|
|
1668
|
-
flashMessage(pending === null
|
|
1669
|
-
? "queued for the director loop"
|
|
1670
|
-
: `queued for the director loop — delivers in ${durationLabel(pending)}`);
|
|
1671
|
-
}
|
|
1672
|
-
catch (err) {
|
|
1673
|
-
// The queue write failed (disk full, permissions): keep the operator's text so
|
|
1674
|
-
// it can be resubmitted, and flash the reason — the same contract the GUI's
|
|
1675
|
-
// prompt form honors. Clearing the line first would silently lose the prompt,
|
|
1676
|
-
// and an unguarded throw would escape the keypress handler and kill the TUI.
|
|
1677
|
-
flashError(err);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
else {
|
|
1683
|
-
const next = applyKey(input, cursor, str, key);
|
|
1684
|
-
input = next.text;
|
|
1685
|
-
cursor = next.cursor;
|
|
1686
|
-
}
|
|
150
|
+
editors.apply(str, key);
|
|
1687
151
|
deps.requestRender();
|
|
1688
152
|
};
|
|
1689
153
|
return {
|
|
1690
154
|
handleKey,
|
|
1691
155
|
syncSnapshot(capUsd, budgetFree, roles, directorRunning = false) {
|
|
1692
|
-
|
|
1693
|
-
currentBudgetFree = budgetFree;
|
|
1694
|
-
const previousCount = roleIds.length;
|
|
1695
|
-
roleIds = roles;
|
|
156
|
+
editors.sync(capUsd, budgetFree);
|
|
1696
157
|
directorInFlight = directorRunning;
|
|
1697
|
-
|
|
1698
|
-
// change moves every one of their indices by the same delta: shift the view with them,
|
|
1699
|
-
// or an unadjusted index silently renames to a different pane while its cached body
|
|
1700
|
-
// stays, showing the wrong body under the tab now selected. A transcript view
|
|
1701
|
-
// (1..previousCount) keeps its index — it does not depend on the roster size — and the
|
|
1702
|
-
// clamp below handles one the shrunken roster no longer has.
|
|
1703
|
-
if (roleIds.length !== previousCount && view > previousCount)
|
|
1704
|
-
view += roleIds.length - previousCount;
|
|
1705
|
-
view = Math.min(view, roleIds.length + 6); // clamp a stale index if roles changed
|
|
1706
|
-
// A retired or renamed loop must not leave the report panes silently scoped to an
|
|
1707
|
-
// absent role — drop the scope with the stale view index. The tab strip and hint then
|
|
1708
|
-
// read "fleet", so a report pane still on screen must recompute its cached Markdown
|
|
1709
|
-
// from the fleet-wide collector (and re-enter at the body's head) to match; the other
|
|
1710
|
-
// views compute with the scope on their next Ctrl+T.
|
|
1711
|
-
if (scopeRole !== null && !roleIds.includes(scopeRole)) {
|
|
1712
|
-
scopeRole = null;
|
|
1713
|
-
refreshPaneCache("report");
|
|
1714
|
-
}
|
|
1715
|
-
// The same drop for History's exact-loop scope: a retired or renamed loop must not keep
|
|
1716
|
-
// labeling an empty pane. An on-screen History pane recomputes from the fleet-wide read
|
|
1717
|
-
// so its rows and its `role:` header clear together.
|
|
1718
|
-
if (historyRole !== null && !roleIds.includes(historyRole)) {
|
|
1719
|
-
historyRole = null;
|
|
1720
|
-
refreshPaneCache("history");
|
|
1721
|
-
// The retired scope may have been feeding an on-screen trail: re-read it from the
|
|
1722
|
-
// fleet so the pane and the tab strip cannot name a loop the roster no longer has.
|
|
1723
|
-
if (trailMode)
|
|
1724
|
-
refreshTrail();
|
|
1725
|
-
}
|
|
1726
|
-
// The same drop for the Activity feed's exact-loop scope: a retired or renamed loop must
|
|
1727
|
-
// not leave the pane filtering on an id the roster no longer names. The scroll resets
|
|
1728
|
-
// with it — the offset indexed the old filtered list.
|
|
1729
|
-
if (activityRole !== null && !roleIds.includes(activityRole)) {
|
|
1730
|
-
activityRole = null;
|
|
1731
|
-
activityScroll = null;
|
|
1732
|
-
}
|
|
1733
|
-
},
|
|
1734
|
-
setLineBudgets(evBudget, enBudget) {
|
|
1735
|
-
eventBudget = evBudget;
|
|
1736
|
-
entryBudget = enBudget;
|
|
158
|
+
panes.syncRoster(roles);
|
|
1737
159
|
},
|
|
160
|
+
setLineBudgets: panes.setLineBudgets,
|
|
1738
161
|
state() {
|
|
1739
162
|
return {
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
budgetMode,
|
|
1743
|
-
rolePromptFor,
|
|
1744
|
-
deferMode,
|
|
1745
|
-
pauseEditMode,
|
|
1746
|
-
pauseEditRole,
|
|
1747
|
-
pauseEditWake,
|
|
1748
|
-
defer: deferMs === null ? null : durationLabel(deferMs),
|
|
1749
|
-
view,
|
|
1750
|
-
scopeRole,
|
|
1751
|
-
historyRole,
|
|
1752
|
-
selectedEntry,
|
|
1753
|
-
cancelArmed,
|
|
1754
|
-
entryScroll,
|
|
1755
|
-
selectedQueued,
|
|
1756
|
-
queuedEditFor,
|
|
1757
|
-
answerFor,
|
|
1758
|
-
entryEditFor,
|
|
1759
|
-
queuedRows,
|
|
1760
|
-
paneCache,
|
|
1761
|
-
noteMode,
|
|
1762
|
-
noteCache,
|
|
1763
|
-
trailMode,
|
|
1764
|
-
trailCache,
|
|
1765
|
-
changeMode,
|
|
1766
|
-
changeCache,
|
|
1767
|
-
paneScroll,
|
|
1768
|
-
activityScroll,
|
|
1769
|
-
activityRole,
|
|
1770
|
-
activityGrep,
|
|
1771
|
-
historySince: historyWindow === null ? null : cycleLabel(HISTORY_WINDOWS, historyWindow),
|
|
1772
|
-
reportSince: reportWindow === null ? null : cycleLabel(REPORT_WINDOWS, reportWindow),
|
|
1773
|
-
historyResult: cycleLabel(HISTORY_RESULTS, historyResult),
|
|
1774
|
-
historyGrep,
|
|
1775
|
-
eventBudget,
|
|
1776
|
-
entryBudget,
|
|
1777
|
-
roleIds,
|
|
163
|
+
...editors.state(),
|
|
164
|
+
...panes.state(),
|
|
1778
165
|
directorInFlight,
|
|
1779
166
|
flash: flash && now() < flashUntil ? flash : null,
|
|
1780
167
|
};
|