tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseDurationMs } from "../../cli/cli-args.js";
|
|
1
|
+
import { durationLabel, parseDurationMs } from "../../cli/cli-args.js";
|
|
2
2
|
import { PAUSE_FOR_MAX_MS } from "../../operator/operator-intent.js";
|
|
3
3
|
import { cutSplitsSurrogatePair, gotSuffix } from "../../text/text.js";
|
|
4
4
|
import { displayWidth } from "../../text/text-width.js";
|
|
@@ -10,6 +10,16 @@ function snapCursor(text, cursor) {
|
|
|
10
10
|
const clamp = Math.max(0, Math.min(cursor, text.length));
|
|
11
11
|
return cutSplitsSurrogatePair(text, clamp) ? clamp - 1 : clamp;
|
|
12
12
|
}
|
|
13
|
+
/** Move the cursor one whole code point in `dir` (`-1` left, `+1` right), clamped to the
|
|
14
|
+
* text's ends — the one home of the surrogate-step rule applyKey's left and right arms
|
|
15
|
+
* shared, so neither direction can ever land between a surrogate pair's halves (a cursor
|
|
16
|
+
* there would let a later backspace/delete cut the pair in half). The step backs off one
|
|
17
|
+
* unit past the clamp when the landed index would split a pair, crossing the whole astral
|
|
18
|
+
* character instead. */
|
|
19
|
+
function stepCursor(text, cursor, dir) {
|
|
20
|
+
const n = dir === -1 ? Math.max(0, cursor - 1) : Math.min(text.length, cursor + 1);
|
|
21
|
+
return cutSplitsSurrogatePair(text, n) ? n + dir : n;
|
|
22
|
+
}
|
|
13
23
|
/** The character immediately before index `i`, as its whole [start, i) span: a surrogate
|
|
14
24
|
* pair is never split, so the backward word-scan killWordBefore runs on always steps a
|
|
15
25
|
* whole character and its boundary lands where a cut is safe. Null at the text's start. */
|
|
@@ -19,6 +29,16 @@ function prevChar(text, i) {
|
|
|
19
29
|
const start = cutSplitsSurrogatePair(text, i - 1) ? i - 2 : i - 1;
|
|
20
30
|
return { start, ch: text.slice(start, i) };
|
|
21
31
|
}
|
|
32
|
+
/** Delete the whole character (surrogate pair or single unit) that starts at code-unit index
|
|
33
|
+
* `start` and land the cursor on the deleted range's start — the shared shape of applyKey's
|
|
34
|
+
* backspace and delete arms, which differ only in where the removed character sits relative
|
|
35
|
+
* to the cursor. A surrogate pair is deleted as both units so no lone surrogate — which
|
|
36
|
+
* terminals render as a U+FFFD box — is ever left behind (the surrogate-safe rule snapCursor
|
|
37
|
+
* and prevChar enforce elsewhere). */
|
|
38
|
+
function removeCharAt(text, start) {
|
|
39
|
+
const width = cutSplitsSurrogatePair(text, start + 1) ? 2 : 1;
|
|
40
|
+
return { text: text.slice(0, start) + text.slice(start + width), cursor: start };
|
|
41
|
+
}
|
|
22
42
|
/** Backward word kill (readline's unix-word-rubout, Alt+Backspace): whitespace immediately
|
|
23
43
|
* before the cursor goes first, then the non-whitespace token that precedes it — on
|
|
24
44
|
* "configure the loop" it kills " loop", leaving "configure the". The whitespace-first
|
|
@@ -33,6 +53,77 @@ function killWordBefore(text, c) {
|
|
|
33
53
|
i = p.start;
|
|
34
54
|
return { text: text.slice(0, i) + text.slice(c), cursor: i };
|
|
35
55
|
}
|
|
56
|
+
/** The cursor-movement keys: left/right step one whole code point, home/end jump to the
|
|
57
|
+
* text's start/end. Returns undefined for any other key so the caller's chain falls
|
|
58
|
+
* through to the next helper. */
|
|
59
|
+
function moveKey(text, c, key) {
|
|
60
|
+
switch (key.name) {
|
|
61
|
+
case "left":
|
|
62
|
+
return { text, cursor: stepCursor(text, c, -1) };
|
|
63
|
+
case "right":
|
|
64
|
+
return { text, cursor: stepCursor(text, c, 1) };
|
|
65
|
+
case "home":
|
|
66
|
+
return { text, cursor: 0 };
|
|
67
|
+
case "end":
|
|
68
|
+
return { text, cursor: text.length };
|
|
69
|
+
default:
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** The character-deletion keys: backspace removes the character before the cursor (or, with
|
|
74
|
+
* Alt+Backspace, the whole word before it — readline's unix-word-rubout), delete removes the
|
|
75
|
+
* one at it. Each deletes a surrogate pair whole so no lone surrogate is left behind.
|
|
76
|
+
* Returns undefined for any other key so the caller's chain falls through. */
|
|
77
|
+
function deleteKey(text, c, key) {
|
|
78
|
+
switch (key.name) {
|
|
79
|
+
case "backspace":
|
|
80
|
+
if (key.meta)
|
|
81
|
+
return killWordBefore(text, c); // Alt+Backspace: kill the previous word
|
|
82
|
+
if (c === 0)
|
|
83
|
+
return { text, cursor: 0 };
|
|
84
|
+
// The character before the cursor is a surrogate pair [c-2, c-1]: delete both units.
|
|
85
|
+
return removeCharAt(text, cutSplitsSurrogatePair(text, c - 1) ? c - 2 : c - 1);
|
|
86
|
+
case "delete":
|
|
87
|
+
if (c >= text.length)
|
|
88
|
+
return { text, cursor: c };
|
|
89
|
+
// The character at the cursor is a surrogate pair [c, c+1]: delete both units.
|
|
90
|
+
return removeCharAt(text, c);
|
|
91
|
+
default:
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** The line-kill keys, both gated on Ctrl so a plain "u"/"k" never kills: Ctrl+U discards
|
|
96
|
+
* from the line start to the cursor, Ctrl+K from the cursor to the line end (readline's
|
|
97
|
+
* two directions of half-line discard). Returns undefined when not Ctrl or not one of these
|
|
98
|
+
* keys so the caller's chain falls through. */
|
|
99
|
+
function lineKillKey(text, c, key) {
|
|
100
|
+
if (!key.ctrl)
|
|
101
|
+
return undefined;
|
|
102
|
+
switch (key.name) {
|
|
103
|
+
case "u":
|
|
104
|
+
// Ctrl+U (readline's unix-line-discard) kills from the line start to the cursor; a
|
|
105
|
+
// plain "u" is not this key and reaches the printable insert in applyKey, unchanged.
|
|
106
|
+
return { text: text.slice(c), cursor: 0 };
|
|
107
|
+
case "k":
|
|
108
|
+
// Ctrl+K (readline's kill-line) kills from the cursor to the line end; a plain "k"
|
|
109
|
+
// is not this key and reaches the printable insert in applyKey, unchanged.
|
|
110
|
+
return { text: text.slice(0, c), cursor: c };
|
|
111
|
+
default:
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Printable insertion: any plain printable string is inserted at the cursor (str can hold a
|
|
116
|
+
* whole composed string when IME input arrives as one keypress, advancing the cursor by its
|
|
117
|
+
* full length); every other — including control/meta and empty — key leaves text and the
|
|
118
|
+
* snapped cursor as they are. This is the chain's last member, so its return is total. */
|
|
119
|
+
function insertKey(text, c, str, key) {
|
|
120
|
+
if (str && !key.ctrl && !key.meta && str >= " ") {
|
|
121
|
+
// str can hold a whole composed string (IME input arrives as one keypress); the cursor
|
|
122
|
+
// must land after ALL of it, not one unit in.
|
|
123
|
+
return { text: text.slice(0, c) + str + text.slice(c), cursor: c + str.length };
|
|
124
|
+
}
|
|
125
|
+
return { text, cursor: c };
|
|
126
|
+
}
|
|
36
127
|
/** Apply one keypress to the prompt text (pure, so it is unit-testable without a TTY).
|
|
37
128
|
* Printable characters insert at the cursor — including multi-character strings readline
|
|
38
129
|
* delivers for IME-composed input, which advance the cursor by their full length; backspace
|
|
@@ -54,57 +145,10 @@ function killWordBefore(text, c) {
|
|
|
54
145
|
* the pair in half). */
|
|
55
146
|
export function applyKey(text, cursor, str, key) {
|
|
56
147
|
const c = snapCursor(text, cursor);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return { text, cursor: cutSplitsSurrogatePair(text, n) ? n - 1 : n };
|
|
62
|
-
}
|
|
63
|
-
case "right": {
|
|
64
|
-
const n = Math.min(text.length, c + 1);
|
|
65
|
-
return { text, cursor: cutSplitsSurrogatePair(text, n) ? n + 1 : n };
|
|
66
|
-
}
|
|
67
|
-
case "backspace":
|
|
68
|
-
if (key.meta)
|
|
69
|
-
return killWordBefore(text, c); // Alt+Backspace: kill the previous word
|
|
70
|
-
if (c === 0)
|
|
71
|
-
return { text, cursor: 0 };
|
|
72
|
-
// The character before the cursor is a surrogate pair [c-2, c-1]: delete both units.
|
|
73
|
-
if (cutSplitsSurrogatePair(text, c - 1)) {
|
|
74
|
-
return { text: text.slice(0, c - 2) + text.slice(c), cursor: c - 2 };
|
|
75
|
-
}
|
|
76
|
-
return { text: text.slice(0, c - 1) + text.slice(c), cursor: c - 1 };
|
|
77
|
-
case "home":
|
|
78
|
-
return { text, cursor: 0 };
|
|
79
|
-
case "end":
|
|
80
|
-
return { text, cursor: text.length };
|
|
81
|
-
case "u":
|
|
82
|
-
// Ctrl+U (readline's unix-line-discard) kills from the line start to the cursor; a
|
|
83
|
-
// plain "u" breaks here and reaches the printable insert branch below, unchanged.
|
|
84
|
-
if (key.ctrl)
|
|
85
|
-
return { text: text.slice(c), cursor: 0 };
|
|
86
|
-
break;
|
|
87
|
-
case "k":
|
|
88
|
-
// Ctrl+K (readline's kill-line) kills from the cursor to the line end; a plain "k"
|
|
89
|
-
// breaks here and reaches the printable insert branch below, unchanged.
|
|
90
|
-
if (key.ctrl)
|
|
91
|
-
return { text: text.slice(0, c), cursor: c };
|
|
92
|
-
break;
|
|
93
|
-
case "delete":
|
|
94
|
-
if (c >= text.length)
|
|
95
|
-
return { text, cursor: c };
|
|
96
|
-
// The character at the cursor is a surrogate pair [c, c+1]: delete both units.
|
|
97
|
-
if (cutSplitsSurrogatePair(text, c + 1)) {
|
|
98
|
-
return { text: text.slice(0, c) + text.slice(c + 2), cursor: c };
|
|
99
|
-
}
|
|
100
|
-
return { text: text.slice(0, c) + text.slice(c + 1), cursor: c };
|
|
101
|
-
}
|
|
102
|
-
if (str && !key.ctrl && !key.meta && str >= " ") {
|
|
103
|
-
// str can hold a whole composed string (IME input arrives as one keypress); the cursor
|
|
104
|
-
// must land after ALL of it, not one unit in.
|
|
105
|
-
return { text: text.slice(0, c) + str + text.slice(c), cursor: c + str.length };
|
|
106
|
-
}
|
|
107
|
-
return { text, cursor: c };
|
|
148
|
+
return (moveKey(text, c, key) ??
|
|
149
|
+
deleteKey(text, c, key) ??
|
|
150
|
+
lineKillKey(text, c, key) ??
|
|
151
|
+
insertKey(text, c, str, key));
|
|
108
152
|
}
|
|
109
153
|
/** Five parsers below signal an invalid value through the same `{ ok: false, error }` arm
|
|
110
154
|
* of a `{ ok: true, … } | { ok: false, error: string }` result. `fail` is that arm's one
|
|
@@ -146,12 +190,14 @@ export function parseBudgetInput(text) {
|
|
|
146
190
|
return fail(`budget must be a number of 0 or more, at most ${Number.MAX_SAFE_INTEGER}${gotSuffix(text)}`);
|
|
147
191
|
return { ok: true, value: n };
|
|
148
192
|
}
|
|
149
|
-
/** The
|
|
150
|
-
* PAUSE_FOR_MAX_MS, the limit `pause --for`/`wake --in`/`stop --in` all admit),
|
|
151
|
-
*
|
|
152
|
-
*
|
|
193
|
+
/** The cap refusal every duration editor shares — the same over-cap arm (`ms` past
|
|
194
|
+
* PAUSE_FOR_MAX_MS, the limit `pause --for`/`wake --in`/`stop --in` all admit), labeled
|
|
195
|
+
* through durationLabel(PAUSE_FOR_MAX_MS) so the message can never advertise a bound the
|
|
196
|
+
* constant does not hold (the CLI surfaces already label their cap the same way), spelled
|
|
197
|
+
* once per verb so the wording cannot drift between the four parsers. Returns the error
|
|
198
|
+
* result when `ms` exceeds the cap, else null so the caller keeps its ok result. */
|
|
153
199
|
function overDurationCap(ms, verb) {
|
|
154
|
-
return ms > PAUSE_FOR_MAX_MS ? fail(`${verb} is capped at
|
|
200
|
+
return ms > PAUSE_FOR_MAX_MS ? fail(`${verb} is capped at ${durationLabel(PAUSE_FOR_MAX_MS)}`) : null;
|
|
155
201
|
}
|
|
156
202
|
/** Parse a duration with parseDurationMs and apply the shared 90d cap in one step, mapping
|
|
157
203
|
* both a non-duration spelling (`bad`, caller-specific wording) and an over-cap value to an
|
|
@@ -260,6 +306,62 @@ export function renderInputView(text, cursor, width) {
|
|
|
260
306
|
// showing the character being edited matters more than signalling clipped text.
|
|
261
307
|
return displayWidth(withEllipsis) <= width - 2 ? withEllipsis : slice;
|
|
262
308
|
}
|
|
309
|
+
/** The code-unit start of each character in `chars`: `starts[k]` is how many UTF-16
|
|
310
|
+
* code units precede character `k` (a surrogate pair is two). The cursor is always on a
|
|
311
|
+
* character boundary (applyKey snaps mid-pair cursors; BMP characters are one unit each),
|
|
312
|
+
* so it names a character. */
|
|
313
|
+
function characterStarts(chars) {
|
|
314
|
+
const starts = [];
|
|
315
|
+
let units = 0;
|
|
316
|
+
for (const ch of chars) {
|
|
317
|
+
starts.push(units);
|
|
318
|
+
units += ch.length;
|
|
319
|
+
}
|
|
320
|
+
return starts;
|
|
321
|
+
}
|
|
322
|
+
/** The display columns before each character: `prefix[k]` = columns before character `k`,
|
|
323
|
+
* where each character costs its displayWidth (a CJK character two, an astral pair never
|
|
324
|
+
* measured as halves). */
|
|
325
|
+
function displayPrefix(chars) {
|
|
326
|
+
const prefix = [0];
|
|
327
|
+
for (let k = 0; k < chars.length; k++)
|
|
328
|
+
prefix.push(prefix[k] + displayWidth(chars[k]));
|
|
329
|
+
return prefix;
|
|
330
|
+
}
|
|
331
|
+
/** The character the cursor (already snapped to a boundary, at code-unit `c`) sits on, or
|
|
332
|
+
* `chars.length` past the last as a virtual end marker. */
|
|
333
|
+
function cursorCharIndex(starts, chars, c) {
|
|
334
|
+
for (let k = 0; k < chars.length; k++) {
|
|
335
|
+
if (starts[k] <= c && c < starts[k] + chars[k].length)
|
|
336
|
+
return k;
|
|
337
|
+
}
|
|
338
|
+
return chars.length;
|
|
339
|
+
}
|
|
340
|
+
/** The left extent of the window: starts at the cursor's character and extends left while
|
|
341
|
+
* the column budget holds, so the cursor's character ends up at the right edge. */
|
|
342
|
+
function leftWindowStart(prefix, cursorEnd, room) {
|
|
343
|
+
let startCp = cursorEnd;
|
|
344
|
+
while (startCp > 0 && prefix[cursorEnd] - prefix[startCp - 1] <= room)
|
|
345
|
+
startCp--;
|
|
346
|
+
return startCp;
|
|
347
|
+
}
|
|
348
|
+
/** The right fill of the window: when the window is anchored at the text's start (cursor
|
|
349
|
+
* near the beginning), fill rightward from the cursor's character up to the budget. */
|
|
350
|
+
function rightWindowEnd(prefix, startCp, cursorEnd, n, room) {
|
|
351
|
+
let endCp = cursorEnd;
|
|
352
|
+
if (startCp === 0 && endCp < n) {
|
|
353
|
+
while (endCp < n && prefix[endCp + 1] - prefix[0] <= room)
|
|
354
|
+
endCp++;
|
|
355
|
+
}
|
|
356
|
+
return endCp;
|
|
357
|
+
}
|
|
358
|
+
/** The whole-character overflow fallback: the smallest window around the cursor when the
|
|
359
|
+
* character it sits on alone exceeds `room` — that whole character (two columns when
|
|
360
|
+
* wide), or the whole character before it at end of text. */
|
|
361
|
+
function wholeCharSpan(starts, chars, ci, n) {
|
|
362
|
+
const k = ci < n ? ci : n - 1;
|
|
363
|
+
return { start: starts[k], end: starts[k] + chars[k].length };
|
|
364
|
+
}
|
|
263
365
|
/** The code-unit window `[start, end)` renderInputView shows for a prompt line wider than
|
|
264
366
|
* its `room` display columns. The budget is spent in terminal display columns — each
|
|
265
367
|
* character costs its displayWidth, so a CJK character costs two and astral pairs are never
|
|
@@ -275,50 +377,17 @@ export function renderInputView(text, cursor, width) {
|
|
|
275
377
|
* longer both fit. Pure, so it is unit-testable without a TTY. */
|
|
276
378
|
export function inputViewWindow(text, cursor, room) {
|
|
277
379
|
const c = snapCursor(text, cursor);
|
|
278
|
-
// Per-character decomposition: where each code point starts (in units) and how many
|
|
279
|
-
// terminal columns it renders as. The cursor is always on a character boundary (applyKey
|
|
280
|
-
// snaps mid-pair cursors; BMP characters are one unit each), so it names a character.
|
|
281
380
|
const chars = Array.from(text);
|
|
282
381
|
const n = chars.length;
|
|
283
|
-
const starts =
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
starts.push(units);
|
|
287
|
-
units += ch.length;
|
|
288
|
-
}
|
|
289
|
-
const prefix = [0]; // prefix[k] = display columns before character k
|
|
290
|
-
for (let k = 0; k < n; k++)
|
|
291
|
-
prefix.push(prefix[k] + displayWidth(chars[k]));
|
|
292
|
-
// The character the cursor sits on; past the last character it is a virtual end marker.
|
|
293
|
-
let ci = n;
|
|
294
|
-
for (let k = 0; k < n; k++) {
|
|
295
|
-
if (starts[k] <= c && c < starts[k] + chars[k].length) {
|
|
296
|
-
ci = k;
|
|
297
|
-
break;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
// Window ending at the cursor's character, extended left while the column budget holds
|
|
301
|
-
// (cursor at the right edge); anchored at the text's start instead when the cursor is
|
|
302
|
-
// near the beginning, then filled rightward up to the budget.
|
|
382
|
+
const starts = characterStarts(chars);
|
|
383
|
+
const prefix = displayPrefix(chars);
|
|
384
|
+
const ci = cursorCharIndex(starts, chars, c);
|
|
303
385
|
const cursorEnd = Math.min(n, ci + 1);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
while (endCp < n && prefix[endCp + 1] - prefix[0] <= room)
|
|
310
|
-
endCp++;
|
|
311
|
-
}
|
|
312
|
-
let start = startCp < n ? starts[startCp] : units;
|
|
313
|
-
let end = endCp < n ? starts[endCp] : units;
|
|
314
|
-
if (end <= start && n > 0) {
|
|
315
|
-
// The character under the cursor alone exceeds the budget: show that whole character
|
|
316
|
-
// (two columns when wide), or the whole character before it at end of text.
|
|
317
|
-
const k = ci < n ? ci : n - 1;
|
|
318
|
-
start = starts[k];
|
|
319
|
-
end = starts[k] + chars[k].length;
|
|
320
|
-
}
|
|
321
|
-
return { start, end };
|
|
386
|
+
const startCp = leftWindowStart(prefix, cursorEnd, room);
|
|
387
|
+
const endCp = rightWindowEnd(prefix, startCp, cursorEnd, n, room);
|
|
388
|
+
const start = startCp < n ? starts[startCp] : text.length;
|
|
389
|
+
const end = endCp < n ? starts[endCp] : text.length;
|
|
390
|
+
return end <= start && n > 0 ? wholeCharSpan(starts, chars, ci, n) : { start, end };
|
|
322
391
|
}
|
|
323
392
|
/** The runTui start guard's error message: names the missing stream — a piped stdin and a
|
|
324
393
|
* redirected stdout are distinct failures (the former cannot take typed prompts, the latter
|
|
@@ -3,6 +3,50 @@
|
|
|
3
3
|
* tui-keys.ts's createTuiKeys consumes the readline (str, key) shape — this module mints
|
|
4
4
|
* those names and classifies the vertical directions the dispatch branches steer by. Pure,
|
|
5
5
|
* so every mapping is unit-testable without ink mounted. */
|
|
6
|
+
/** The flag-named keys in their chain order, each an ink flag and the readline name it maps
|
|
7
|
+
* to — the ordered table the inkKeyToReadline chain used to hand-roll as a flat if-chain.
|
|
8
|
+
* Named keys win first (ink sets `ctrl` alongside arrows too). */
|
|
9
|
+
const NAMED_KEYS = [
|
|
10
|
+
["delete", "delete"],
|
|
11
|
+
["tab", "tab"],
|
|
12
|
+
["upArrow", "up"],
|
|
13
|
+
["downArrow", "down"],
|
|
14
|
+
["leftArrow", "left"],
|
|
15
|
+
["rightArrow", "right"],
|
|
16
|
+
["pageUp", "pageup"],
|
|
17
|
+
["pageDown", "pagedown"],
|
|
18
|
+
["home", "home"],
|
|
19
|
+
["end", "end"],
|
|
20
|
+
];
|
|
21
|
+
/** The name a flag-named key maps to — the backspace/backspace+meta pair first, then the
|
|
22
|
+
* NAMED_KEYS table scan (an arrow that also carries `ctrl` still returns the arrow name,
|
|
23
|
+
* because this runs before controlKey). undefined when no named-key flag matches. */
|
|
24
|
+
function namedKeyName(key) {
|
|
25
|
+
if (key.backspace) {
|
|
26
|
+
return key.meta ? { name: "backspace", meta: true } : { name: "backspace" };
|
|
27
|
+
}
|
|
28
|
+
for (const [flag, name] of NAMED_KEYS) {
|
|
29
|
+
if (key[flag])
|
|
30
|
+
return { name };
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** The modifier-chord shape a Ctrl/Alt key maps to, or undefined when neither matches — the
|
|
35
|
+
* Ctrl branch (readline `ctrl: true` plus the bare lowercased letter from `input`), then the
|
|
36
|
+
* Alt single-char branch (`meta: true` plus the lowercased `input`). */
|
|
37
|
+
function controlKey(input, key) {
|
|
38
|
+
if (key.ctrl)
|
|
39
|
+
return { ctrl: true, name: input.toLowerCase() };
|
|
40
|
+
if (key.meta && input.length === 1) {
|
|
41
|
+
return { meta: true, name: input.toLowerCase() };
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/** The printable/empty fallback: text with no key flags flows through as `str`, and an empty
|
|
46
|
+
* non-key input (ink suppresses text for unrecognized sequences) is inert (str undefined). */
|
|
47
|
+
function printableKey(input) {
|
|
48
|
+
return { str: input || undefined, key: {} };
|
|
49
|
+
}
|
|
6
50
|
/** Map one ink `useInput` (input, key) pair to the shape handleKey consumes — ink's parsed
|
|
7
51
|
* flags and input text become the readline (str, key) pair the 2a dispatch was extracted
|
|
8
52
|
* with. Pure, so the mapping is unit-testable without ink mounted. Named keys win first
|
|
@@ -14,35 +58,13 @@ export function inkKeyToReadline(input, key) {
|
|
|
14
58
|
return { str: undefined, key: { name: "escape" } };
|
|
15
59
|
if (key.return)
|
|
16
60
|
return { str: undefined, key: { name: "return" } };
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (key.upArrow)
|
|
25
|
-
return { str: undefined, key: { name: "up" } };
|
|
26
|
-
if (key.downArrow)
|
|
27
|
-
return { str: undefined, key: { name: "down" } };
|
|
28
|
-
if (key.leftArrow)
|
|
29
|
-
return { str: undefined, key: { name: "left" } };
|
|
30
|
-
if (key.rightArrow)
|
|
31
|
-
return { str: undefined, key: { name: "right" } };
|
|
32
|
-
if (key.pageUp)
|
|
33
|
-
return { str: undefined, key: { name: "pageup" } };
|
|
34
|
-
if (key.pageDown)
|
|
35
|
-
return { str: undefined, key: { name: "pagedown" } };
|
|
36
|
-
if (key.home)
|
|
37
|
-
return { str: undefined, key: { name: "home" } };
|
|
38
|
-
if (key.end)
|
|
39
|
-
return { str: undefined, key: { name: "end" } };
|
|
40
|
-
if (key.ctrl)
|
|
41
|
-
return { str: undefined, key: { ctrl: true, name: input.toLowerCase() } };
|
|
42
|
-
if (key.meta && input.length === 1) {
|
|
43
|
-
return { str: undefined, key: { meta: true, name: input.toLowerCase() } };
|
|
44
|
-
}
|
|
45
|
-
return { str: input || undefined, key: {} };
|
|
61
|
+
const named = namedKeyName(key);
|
|
62
|
+
if (named)
|
|
63
|
+
return { str: undefined, key: named };
|
|
64
|
+
const chord = controlKey(input, key);
|
|
65
|
+
if (chord)
|
|
66
|
+
return { str: undefined, key: chord };
|
|
67
|
+
return printableKey(input);
|
|
46
68
|
}
|
|
47
69
|
/** The `"up" | "down"` direction an arrow key names — Up/Down only, so the branches that
|
|
48
70
|
* reserve the arrows for one purpose (entry browse, history recall) reject the page keys —
|
|
@@ -9,7 +9,7 @@ import { loadConfigSafe } from "../../config/config.js";
|
|
|
9
9
|
import { MERGE_LOCK_WAIT_MS } from "../../paths.js";
|
|
10
10
|
import { commitOperatorBacklogEdit } from "../../backlog/backlog-commit.js";
|
|
11
11
|
import { stampedSubject } from "../../git/commit-message.js";
|
|
12
|
-
import { previewRevert, revertMergedCommit } from "../../operator/revert.js";
|
|
12
|
+
import { previewRevert, revertMergedCommit } from "../../operator/operator-revert.js";
|
|
13
13
|
import { requestAbort } from "../../operator/operator-intent.js";
|
|
14
14
|
import { errorMessage } from "../../text/text.js";
|
|
15
15
|
import { DIRECTOR_ROLE } from "../../roles/roles.js";
|
|
@@ -2,10 +2,11 @@ import { activityEvents } from "./tui-pane.js";
|
|
|
2
2
|
import { stepEntryScroll } from "./tui-backlog.js";
|
|
3
3
|
import { pageDir } from "./tui-keymap.js";
|
|
4
4
|
import { promptLineFree } from "./tui-pane-ctx.js";
|
|
5
|
+
import { distinctOrdered } from "../../collections.js";
|
|
5
6
|
/** The Activity pane (view 0) of the TUI (src/ui/tui/tui.tsx): the session live-tail scope
|
|
6
|
-
* (Ctrl+G), the free-text search (`/`), and the
|
|
7
|
-
* pane-navigation monolith; it owns the pane's
|
|
8
|
-
* that touch them. */
|
|
7
|
+
* (Ctrl+G), the event type/notable cull cycle (Ctrl+O), the free-text search (`/`), and the
|
|
8
|
+
* PgUp/PgDn tail-paging arm. Split out of the pane-navigation monolith; it owns the pane's
|
|
9
|
+
* own scroll/scope/search state and the arms that touch them. */
|
|
9
10
|
export function createActivityPane(session) {
|
|
10
11
|
// The Activity pane's body-window start into the loaded event tail, or null to follow the
|
|
11
12
|
// live tail (the newest eventBudget events). PgUp pauses follow and pages back; PgDn returns
|
|
@@ -60,7 +61,7 @@ export function createActivityPane(session) {
|
|
|
60
61
|
const activityTypeCycle = (ctx) => {
|
|
61
62
|
if (!promptLineFree(ctx.es))
|
|
62
63
|
return false;
|
|
63
|
-
const present =
|
|
64
|
+
const present = distinctOrdered(activityEvents(session.root, activityRole, activityGrep, null).map((e) => e.type)).sort();
|
|
64
65
|
const slots = [
|
|
65
66
|
{ notable: false, type: null },
|
|
66
67
|
{ notable: true, type: null },
|
|
@@ -8,6 +8,7 @@ import { requestWake } from "../../operator/operator-intent.js";
|
|
|
8
8
|
import { isOpenBacklogLabel, moveEntrySelection, pageMarkdownBody, visibleBacklogIndexes } from "./tui-backlog.js";
|
|
9
9
|
import { arrowDir, pageDir } from "./tui-keymap.js";
|
|
10
10
|
import { promptLineFree } from "./tui-pane-ctx.js";
|
|
11
|
+
import { guardError } from "./tui-guard.js";
|
|
11
12
|
/** Resolve the selected plan/bug from the flat backlog list, deduping the four entry-action
|
|
12
13
|
* arms' common prologue (Ctrl+X cancel, Ctrl+V move-to-top, Ctrl+R release, Ctrl+E edit): the
|
|
13
14
|
* flat-index clamp and the empty / read-only-done / question guards, whose refusal flash
|
|
@@ -38,10 +39,13 @@ export function selectedEditableEntry(session, selectedEntry, verb) {
|
|
|
38
39
|
return { kind, position, picked };
|
|
39
40
|
}
|
|
40
41
|
/** The project-status pane (view roleIds.length + 1) of the TUI (src/ui/tui/tui.tsx): the
|
|
41
|
-
* entry browser's selection/scroll state,
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* entry browser's selection/scroll state, and its six key arms — the Ctrl+O Ready-only
|
|
43
|
+
* toggle, the two-press Ctrl+X cancel, the one-press Ctrl+V move-to-top, the two-press
|
|
44
|
+
* Ctrl+R release, the arrow up/down entry selection and the PgUp/PgDn body page — each
|
|
45
|
+
* one inner named helper owning its whole guard, flash wording and repaint, dispatched in
|
|
46
|
+
* order by `handleKey`. The entry-action arms are each two-step except the one-press move;
|
|
47
|
+
* the answer/title-edit selection clear the editors call back through
|
|
48
|
+
* clearBacklogSelection.
|
|
45
49
|
* Split out of the pane-navigation monolith; it owns the pane's selection/scroll state and
|
|
46
50
|
* the arms that touch them. */
|
|
47
51
|
export function createBacklogPane(session) {
|
|
@@ -76,7 +80,7 @@ export function createBacklogPane(session) {
|
|
|
76
80
|
// cancel/release read the same as the dashboard drawer's non-exiting action.
|
|
77
81
|
const cancelArmed = (target, armedTitle) => {
|
|
78
82
|
const { kind, position } = target;
|
|
79
|
-
|
|
83
|
+
guardError(session, () => {
|
|
80
84
|
const claims = allLiveClaims(session.root);
|
|
81
85
|
const outcome = tryCancelEntry(session.root, kind, position, armedTitle);
|
|
82
86
|
const claimNote = outcome.status === "cancelled" ? cancelledClaimNote(claims.get(entryKey(outcome.title)) ?? null) : "";
|
|
@@ -90,10 +94,7 @@ export function createBacklogPane(session) {
|
|
|
90
94
|
session.flashMessage(outcome.status === "cancelled"
|
|
91
95
|
? `cancelled ${kind} #${position}: ${target.title}${note === "" ? "" : ` — ${note}`}`
|
|
92
96
|
: `that ${kind} was already removed or moved — nothing cancelled`);
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
session.flashError(err);
|
|
96
|
-
}
|
|
97
|
+
});
|
|
97
98
|
};
|
|
98
99
|
/** ` — still held [...]` for the entry now at a 1-based position, "" when it is eligible. */
|
|
99
100
|
const heldTailAt = (kind, position) => {
|
|
@@ -102,7 +103,7 @@ export function createBacklogPane(session) {
|
|
|
102
103
|
return hold === null ? "" : ` — ${stillHeldNote(hold)}`;
|
|
103
104
|
};
|
|
104
105
|
const moveEntryToTop = (kind, position, title) => {
|
|
105
|
-
|
|
106
|
+
guardError(session, () => {
|
|
106
107
|
const outcome = tryMoveEntryToTop(session.root, kind, position, title);
|
|
107
108
|
let heldTail = "";
|
|
108
109
|
if (outcome.status === "moved_to_top") {
|
|
@@ -116,14 +117,11 @@ export function createBacklogPane(session) {
|
|
|
116
117
|
: outcome.status === "already_first"
|
|
117
118
|
? `that ${kind} is already at the top`
|
|
118
119
|
: `that ${kind} was already removed or moved — nothing moved`);
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
session.flashError(err);
|
|
122
|
-
}
|
|
120
|
+
});
|
|
123
121
|
};
|
|
124
122
|
const releaseArmed = (target, armedTitle) => {
|
|
125
123
|
const { kind, position } = target;
|
|
126
|
-
|
|
124
|
+
guardError(session, () => {
|
|
127
125
|
const outcome = tryReleaseEntry(session.root, kind, position, armedTitle);
|
|
128
126
|
let heldTail = "";
|
|
129
127
|
if (outcome.status === "released") {
|
|
@@ -139,15 +137,15 @@ export function createBacklogPane(session) {
|
|
|
139
137
|
: outcome.status === "not_refused"
|
|
140
138
|
? `that ${kind} carries no Refused note`
|
|
141
139
|
: `that ${kind} was already removed or moved — nothing released`);
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
session.flashError(err);
|
|
145
|
-
}
|
|
140
|
+
});
|
|
146
141
|
};
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
142
|
+
// The Ctrl+O arm: flips the Ready-only filter to keep only the open plans and bugs no hold
|
|
143
|
+
// keeps from being picked, the terminal twin of `tumwater backlog --ready` and the dashboard
|
|
144
|
+
// Backlog card's Ready only toggle. A second press restores the full list. Returns false
|
|
145
|
+
// when the prompt is busy or the Backlog view is not showing, so the key falls through the
|
|
146
|
+
// dispatch as today.
|
|
147
|
+
const backlogToggleReady = (ctx) => {
|
|
148
|
+
if (promptLineFree(ctx.es) && session.view === session.roleIds.length + 1) {
|
|
151
149
|
// Ctrl+O flips the Ready-only filter: keep only the open plans and bugs no hold keeps
|
|
152
150
|
// from being picked (questions and the answered/done/fixed rows stay), the terminal twin
|
|
153
151
|
// of `tumwater backlog --ready` and the dashboard Backlog card's Ready only toggle. A
|
|
@@ -160,9 +158,13 @@ export function createBacklogPane(session) {
|
|
|
160
158
|
session.requestRender();
|
|
161
159
|
return true;
|
|
162
160
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
// The two-press Ctrl+X arm: cancels the selected plan or bug through the non-exiting core
|
|
164
|
+
// the dashboard drawer uses. Returns false when the prompt is busy or the Backlog view is
|
|
165
|
+
// not showing, so the key falls through the dispatch as today.
|
|
166
|
+
const backlogCancel = (ctx) => {
|
|
167
|
+
if (promptLineFree(ctx.es) && session.view === session.roleIds.length + 1) {
|
|
166
168
|
// Ctrl+X twice cancels the selected plan or bug through the non-exiting core the
|
|
167
169
|
// dashboard drawer uses; the title check inside tryCancelEntry refuses a stale selection.
|
|
168
170
|
const selected = selectedEditableEntry(session, selectedEntry, "cancelled");
|
|
@@ -183,9 +185,13 @@ export function createBacklogPane(session) {
|
|
|
183
185
|
session.requestRender();
|
|
184
186
|
return true;
|
|
185
187
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
return false;
|
|
189
|
+
};
|
|
190
|
+
// The one-press Ctrl+V arm: moves the selected plan or bug to the head of its section
|
|
191
|
+
// through the non-exiting core the dashboard drawer uses. Returns false when the prompt is
|
|
192
|
+
// busy or the Backlog view is not showing, so the key falls through the dispatch as today.
|
|
193
|
+
const backlogMoveToTop = (ctx) => {
|
|
194
|
+
if (promptLineFree(ctx.es) && session.view === session.roleIds.length + 1) {
|
|
189
195
|
// Ctrl+V moves the selected plan or bug to the head of its section through the
|
|
190
196
|
// non-exiting core the dashboard drawer uses; the title check inside tryMoveEntryToTop
|
|
191
197
|
// refuses a stale selection. It is the terminal twin of `tumwater plan|bug --top <n>`
|
|
@@ -199,9 +205,13 @@ export function createBacklogPane(session) {
|
|
|
199
205
|
session.requestRender();
|
|
200
206
|
return true;
|
|
201
207
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
return false;
|
|
209
|
+
};
|
|
210
|
+
// The two-press Ctrl+R arm: releases the Refused note on the selected plan or bug through
|
|
211
|
+
// the non-exiting core the dashboard drawer uses. Returns false when the prompt is busy or
|
|
212
|
+
// the Backlog view is not showing, so the key falls through the dispatch as today.
|
|
213
|
+
const backlogRelease = (ctx) => {
|
|
214
|
+
if (promptLineFree(ctx.es) && session.view === session.roleIds.length + 1) {
|
|
205
215
|
// Ctrl+R twice releases the Refused
|
|
206
216
|
// note on the selected plan or bug through the non-exiting core the dashboard drawer
|
|
207
217
|
// uses; the title check inside tryReleaseEntry refuses a stale selection. Releasing is the
|
|
@@ -228,9 +238,12 @@ export function createBacklogPane(session) {
|
|
|
228
238
|
session.requestRender();
|
|
229
239
|
return true;
|
|
230
240
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
241
|
+
return false;
|
|
242
|
+
};
|
|
243
|
+
// The up/down arm: browses entries in full instead of editing the prompt, stepping only
|
|
244
|
+
// through the search/Ready-only kept entries when either filter is applied.
|
|
245
|
+
const backlogSelect = (ctx, arrow) => {
|
|
246
|
+
if (session.view === session.roleIds.length + 1) {
|
|
234
247
|
// In the project-status pane, up/down browse entries in full instead of editing the
|
|
235
248
|
// prompt; every other view keeps today's behavior (arrows are ignored by applyKey).
|
|
236
249
|
// An open answer editor pins the selection (its position was taken from it).
|
|
@@ -253,7 +266,11 @@ export function createBacklogPane(session) {
|
|
|
253
266
|
session.requestRender();
|
|
254
267
|
return true;
|
|
255
268
|
}
|
|
256
|
-
|
|
269
|
+
return false;
|
|
270
|
+
};
|
|
271
|
+
// The PgUp/PgDn arm: scrolls the selected entry's body within the Backlog view's entry mode.
|
|
272
|
+
const backlogPage = (ctx, page) => {
|
|
273
|
+
if (session.view === session.roleIds.length + 1) {
|
|
257
274
|
// Within-body scroll in project-status ENTRY mode: PgDn/PgUp page the selected entry's
|
|
258
275
|
// body, clamped at both ends. No-op in list mode and for bodies that fit — every other
|
|
259
276
|
// view ignores these keys as today (applyKey drops them).
|
|
@@ -268,6 +285,27 @@ export function createBacklogPane(session) {
|
|
|
268
285
|
}
|
|
269
286
|
return false;
|
|
270
287
|
};
|
|
288
|
+
const handleKey = (key, ctx) => {
|
|
289
|
+
// The Backlog view's six key arms live in the named helpers above, each owning its whole
|
|
290
|
+
// guard, flash wording and repaint; this body is only the flat ordered dispatch in today's
|
|
291
|
+
// arm order (Ctrl+O, Ctrl+X, Ctrl+V, Ctrl+R, up/down, PgUp/PgDn), so each key's behavior
|
|
292
|
+
// is found by name.
|
|
293
|
+
if (key.ctrl && key.name === "o")
|
|
294
|
+
return backlogToggleReady(ctx);
|
|
295
|
+
if (key.ctrl && key.name === "x")
|
|
296
|
+
return backlogCancel(ctx);
|
|
297
|
+
if (key.ctrl && key.name === "v")
|
|
298
|
+
return backlogMoveToTop(ctx);
|
|
299
|
+
if (key.ctrl && key.name === "r")
|
|
300
|
+
return backlogRelease(ctx);
|
|
301
|
+
const arrow = arrowDir(key.name);
|
|
302
|
+
if (arrow !== null)
|
|
303
|
+
return backlogSelect(ctx, arrow);
|
|
304
|
+
const page = pageDir(key.name);
|
|
305
|
+
if (page !== null)
|
|
306
|
+
return backlogPage(ctx, page);
|
|
307
|
+
return false;
|
|
308
|
+
};
|
|
271
309
|
return {
|
|
272
310
|
handleKey,
|
|
273
311
|
reset() {
|