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.
Files changed (354) hide show
  1. package/README.md +6 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +6 -6
  4. package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
  5. package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
  6. package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
  7. package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
  8. package/dist/src/agent/backend-traits.js +2 -2
  9. package/dist/src/agent/claude/claude-account.js +6 -27
  10. package/dist/src/agent/claude/claude-backend.js +3 -3
  11. package/dist/src/agent/claude/claude-env.js +27 -31
  12. package/dist/src/agent/claude/claude-stream.js +111 -54
  13. package/dist/src/agent/codex/codex-account.js +8 -0
  14. package/dist/src/agent/codex/codex-args.js +5 -5
  15. package/dist/src/agent/codex/codex-backend.js +2 -2
  16. package/dist/src/agent/codex/codex-env.js +28 -15
  17. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  18. package/dist/src/agent/codex/codex-rollout.js +93 -56
  19. package/dist/src/agent/codex/codex-stream.js +1 -1
  20. package/dist/src/agent/env-scrub.js +18 -0
  21. package/dist/src/agent/image-input.js +68 -11
  22. package/dist/src/agent/notes-shim.js +2 -2
  23. package/dist/src/agent/opencode/opencode-backend.js +26 -24
  24. package/dist/src/agent/opencode/opencode-catalog.js +85 -36
  25. package/dist/src/agent/opencode/opencode-config.js +32 -14
  26. package/dist/src/agent/opencode/opencode-env.js +12 -13
  27. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  28. package/dist/src/agent/session-ledger.js +23 -27
  29. package/dist/src/agent/subscription-account.js +2 -83
  30. package/dist/src/backlog/backlog-archive.js +3 -4
  31. package/dist/src/backlog/backlog-cli-view.js +81 -41
  32. package/dist/src/backlog/backlog-cli.js +57 -26
  33. package/dist/src/backlog/backlog-eligibility.js +56 -122
  34. package/dist/src/backlog/backlog-md.js +5 -5
  35. package/dist/src/backlog/backlog-parts.js +65 -27
  36. package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
  37. package/dist/src/backlog/backlog-release.js +12 -12
  38. package/dist/src/backlog/backlog-structure.js +114 -61
  39. package/dist/src/backlog/backlog.js +3 -2
  40. package/dist/src/baseline/main-baseline.js +148 -84
  41. package/dist/src/baseline/main-guardrails.js +2 -2
  42. package/dist/src/baseline/main-watch.js +84 -50
  43. package/dist/src/brief/brief.js +26 -13
  44. package/dist/src/budget/budget-fallback-view.js +100 -243
  45. package/dist/src/budget/budget-wiring.js +29 -65
  46. package/dist/src/budget/budget.js +4 -5
  47. package/dist/src/budget/fallback-breaker.js +2 -2
  48. package/dist/src/build/build-check-detect.js +43 -14
  49. package/dist/src/build/build-check-events.js +2 -2
  50. package/dist/src/build/build-check-report.js +10 -0
  51. package/dist/src/build/build-check-scoped.js +40 -18
  52. package/dist/src/build/build-check.js +13 -5
  53. package/dist/src/build/build-stage.js +6 -4
  54. package/dist/src/change/change-data.js +1 -4
  55. package/dist/src/cli/cli-command-args.js +89 -52
  56. package/dist/src/cli/cli-completion-command.js +75 -0
  57. package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
  58. package/dist/src/cli/cli-doctor-command.js +103 -0
  59. package/dist/src/cli/cli-flag-specs.js +42 -22
  60. package/dist/src/cli/cli-init.js +36 -14
  61. package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
  62. package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
  63. package/dist/src/cli/cli-query-commands.js +2 -2
  64. package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
  65. package/dist/src/cli/cli-repo-gate.js +2 -2
  66. package/dist/src/cli/cli-role-command.js +2 -41
  67. package/dist/src/cli/cli-start-summary.js +4 -3
  68. package/dist/src/cli/cli-start-supervisor.js +190 -0
  69. package/dist/src/cli/cli-start.js +94 -244
  70. package/dist/src/cli/completion.js +85 -49
  71. package/dist/src/cli/follow-view.js +20 -2
  72. package/dist/src/cli/help.js +6 -13
  73. package/dist/src/cli.js +70 -176
  74. package/dist/src/collections.js +39 -27
  75. package/dist/src/concurrency/check-slots.js +70 -0
  76. package/dist/src/concurrency/lock.js +23 -1
  77. package/dist/src/concurrency/run-permit.js +49 -0
  78. package/dist/src/concurrency/semaphore.js +46 -250
  79. package/dist/src/config/config-editable-keys.js +4 -7
  80. package/dist/src/config/config-fallbacks.js +81 -101
  81. package/dist/src/config/config-field-checks.js +66 -37
  82. package/dist/src/config/config-keys.js +66 -40
  83. package/dist/src/config/config-live.js +4 -28
  84. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  85. package/dist/src/config/config-request.js +163 -102
  86. package/dist/src/config/config-schema.js +10 -12
  87. package/dist/src/config/config-selector-checks.js +1 -1
  88. package/dist/src/config/config-validation-agents.js +2 -2
  89. package/dist/src/config/config-validation-roles.js +8 -72
  90. package/dist/src/config/config-validation.js +4 -39
  91. package/dist/src/config/config-views.js +26 -50
  92. package/dist/src/config/config-write.js +128 -102
  93. package/dist/src/config/config.js +5 -14
  94. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  95. package/dist/src/doctor/doctor-backlog-plans.js +53 -120
  96. package/dist/src/doctor/doctor-backlog.js +16 -14
  97. package/dist/src/doctor/doctor-check-names.js +3 -6
  98. package/dist/src/doctor/doctor-checks.js +8 -8
  99. package/dist/src/doctor/doctor-claude-checks.js +70 -36
  100. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  101. package/dist/src/doctor/doctor-contention-checks.js +57 -117
  102. package/dist/src/doctor/doctor-fallback-checks.js +48 -26
  103. package/dist/src/doctor/doctor-holds.js +8 -47
  104. package/dist/src/doctor/doctor-install.js +1 -1
  105. package/dist/src/doctor/doctor-launch-services.js +1 -1
  106. package/dist/src/doctor/doctor-model-checks.js +178 -94
  107. package/dist/src/doctor/doctor-notify.js +2 -2
  108. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  109. package/dist/src/doctor/doctor-orphans.js +122 -21
  110. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  111. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  112. package/dist/src/doctor/doctor-principles.js +3 -3
  113. package/dist/src/doctor/doctor-project-checks.js +24 -11
  114. package/dist/src/doctor/doctor-role-notes.js +1 -1
  115. package/dist/src/doctor/doctor.js +8 -11
  116. package/dist/src/events/event-format.js +44 -64
  117. package/dist/src/events/event-labels.js +1 -5
  118. package/dist/src/events/event-legacy.js +3 -5
  119. package/dist/src/events/{notify.js → event-notify.js} +6 -6
  120. package/dist/src/events/event-read.js +1 -1
  121. package/dist/src/events/event-window.js +1 -1
  122. package/dist/src/events/events.js +8 -0
  123. package/dist/src/failure/failure-cluster.js +9 -18
  124. package/dist/src/failure/failure-data.js +12 -8
  125. package/dist/src/failure/failure-render.js +46 -36
  126. package/dist/src/failure/failure-state-change.js +54 -120
  127. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  128. package/dist/src/files/files.js +30 -0
  129. package/dist/src/files/json-object.js +8 -8
  130. package/dist/src/files/stat-cache.js +12 -8
  131. package/dist/src/gates/budget-gates.js +85 -84
  132. package/dist/src/gates/gate-polls.js +32 -74
  133. package/dist/src/gates/pause-gates.js +16 -2
  134. package/dist/src/gates/readiness.js +1 -1
  135. package/dist/src/gates/role-cap-gates.js +44 -35
  136. package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
  137. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  138. package/dist/src/git/git-diff.js +6 -2
  139. package/dist/src/git/git.js +75 -45
  140. package/dist/src/git/worktree-pool.js +3 -6
  141. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  142. package/dist/src/gui/gui-args.js +12 -0
  143. package/dist/src/gui/gui-backlog-commands.js +4 -4
  144. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  145. package/dist/src/gui/gui-command.js +43 -24
  146. package/dist/src/gui/gui-config-commands.js +2 -64
  147. package/dist/src/gui/gui-endpoint-commands.js +1 -1
  148. package/dist/src/gui/gui-endpoint-common.js +19 -1
  149. package/dist/src/gui/gui-history-endpoints.js +100 -52
  150. package/dist/src/gui/gui-operator-commands.js +43 -48
  151. package/dist/src/gui/gui-prompt-commands.js +25 -25
  152. package/dist/src/gui/gui-report-endpoints.js +1 -1
  153. package/dist/src/gui/gui-server.js +1 -2
  154. package/dist/src/gui/gui-team-commands.js +2 -1
  155. package/dist/src/history/history-data.js +71 -44
  156. package/dist/src/history/history-queued.js +49 -27
  157. package/dist/src/history/history.js +54 -36
  158. package/dist/src/inbox/prompt-commands.js +59 -42
  159. package/dist/src/init/init.js +88 -40
  160. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  161. package/dist/src/merge/merge-check-failures.js +12 -9
  162. package/dist/src/merge/merge-core.js +7 -32
  163. package/dist/src/merge/merge-drain.js +11 -13
  164. package/dist/src/merge/merge-pipeline.js +6 -7
  165. package/dist/src/merge/merge-post-review-drain.js +11 -14
  166. package/dist/src/merge/merge-queue-render.js +3 -3
  167. package/dist/src/merge/merge-requeue.js +1 -1
  168. package/dist/src/merge/merge-slot.js +0 -8
  169. package/dist/src/merge/merge-stack.js +17 -18
  170. package/dist/src/merge/merge-to-main.js +13 -58
  171. package/dist/src/merge/merge-verify.js +12 -21
  172. package/dist/src/notes-cli.js +52 -42
  173. package/dist/src/operator/operator-commands.js +80 -47
  174. package/dist/src/operator/operator-intent.js +3 -3
  175. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  176. package/dist/src/orchestrator/orchestrator-poll.js +44 -69
  177. package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
  178. package/dist/src/orchestrator/orchestrator.js +8 -21
  179. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  180. package/dist/src/orchestrator/scheduling-scope.js +18 -27
  181. package/dist/src/orchestrator/scheduling-skip.js +10 -14
  182. package/dist/src/orchestrator/startup-migrations.js +1 -1
  183. package/dist/src/paths.js +10 -9
  184. package/dist/src/pi/pi-args.js +5 -2
  185. package/dist/src/pi/pi-backend.js +2 -2
  186. package/dist/src/pi/pi-bin.js +3 -3
  187. package/dist/src/pi/pi-event-line.js +28 -1
  188. package/dist/src/pi/pi-listed-models.js +13 -0
  189. package/dist/src/pi/pi-models.js +43 -29
  190. package/dist/src/pi/pi-settings.js +43 -20
  191. package/dist/src/pi/pi-stream.js +41 -8
  192. package/dist/src/pi/pi-watchdogs.js +43 -15
  193. package/dist/src/pi/pi.js +2 -2
  194. package/dist/src/pi-extension/context-shake.js +32 -20
  195. package/dist/src/pi-extension/kill-guard.js +69 -0
  196. package/dist/src/process/process-group.js +51 -26
  197. package/dist/src/process/process.js +20 -12
  198. package/dist/src/process/run-marker.js +33 -15
  199. package/dist/src/process/supervisor.js +40 -28
  200. package/dist/src/prompt/dialect.js +1 -1
  201. package/dist/src/prompt/gate-notes.js +2 -2
  202. package/dist/src/prompt/gate-prompts.js +1 -44
  203. package/dist/src/prompt/notes-backlog.js +66 -37
  204. package/dist/src/prompt/principles.js +6 -4
  205. package/dist/src/prompt/prompt-followup.js +12 -4
  206. package/dist/src/prompt/prompt.js +88 -38
  207. package/dist/src/redeploy/redeployer.js +21 -1
  208. package/dist/src/report/report-data.js +53 -37
  209. package/dist/src/report/report-fold.js +16 -9
  210. package/dist/src/report/report-render.js +21 -194
  211. package/dist/src/report/report-since-data.js +4 -3
  212. package/dist/src/report/report-since-render.js +173 -0
  213. package/dist/src/report/report-usage-fold.js +8 -8
  214. package/dist/src/report/report.js +46 -22
  215. package/dist/src/review/diff-rules.js +35 -21
  216. package/dist/src/review/post-merge-review-render.js +9 -3
  217. package/dist/src/review/post-merge-review.js +48 -41
  218. package/dist/src/review/review-precheck.js +14 -4
  219. package/dist/src/review/review.js +48 -170
  220. package/dist/src/review/secret-leaks.js +26 -16
  221. package/dist/src/roles/legacy-role-migration.js +6 -3
  222. package/dist/src/roles/role-catalog.js +11 -3
  223. package/dist/src/roles/role-guidance.js +16 -12
  224. package/dist/src/roles/role-render.js +7 -46
  225. package/dist/src/roles/role-view.js +10 -33
  226. package/dist/src/run/acceptance-greps.js +88 -45
  227. package/dist/src/run/ci-parity.js +19 -8
  228. package/dist/src/run/run-apply.js +12 -15
  229. package/dist/src/run/run-detail-data.js +121 -68
  230. package/dist/src/run/run-detail-render.js +92 -35
  231. package/dist/src/run/run-detail.js +72 -41
  232. package/dist/src/run/run-prompt.js +63 -50
  233. package/dist/src/run/run-resume.js +76 -29
  234. package/dist/src/run/run-stage.js +141 -90
  235. package/dist/src/run/run-timing.js +8 -2
  236. package/dist/src/run/run-usage.js +31 -1
  237. package/dist/src/run/run-verdict.js +128 -50
  238. package/dist/src/run/stage-check.js +60 -50
  239. package/dist/src/run/stage-checks-shared.js +28 -0
  240. package/dist/src/run/stage-name-checks.js +111 -92
  241. package/dist/src/run/stage-path-checks.js +69 -42
  242. package/dist/src/run/stage-prose-checks.js +55 -40
  243. package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
  244. package/dist/src/runner/runner-outcome.js +7 -2
  245. package/dist/src/runner/runner-pi.js +9 -22
  246. package/dist/src/runner/runner-recovery.js +5 -13
  247. package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
  248. package/dist/src/runner/runner-run.js +103 -50
  249. package/dist/src/runner/runner-state-load.js +18 -28
  250. package/dist/src/runner/runner-state.js +0 -14
  251. package/dist/src/runner/runner.js +41 -25
  252. package/dist/src/scheduling/backoff.js +8 -3
  253. package/dist/src/scheduling/claim-entries.js +2 -1
  254. package/dist/src/scheduling/claims.js +0 -4
  255. package/dist/src/scheduling/scheduling.js +53 -66
  256. package/dist/src/status/status-budget.js +4 -43
  257. package/dist/src/status/status-data.js +35 -39
  258. package/dist/src/status/status-role-notes.js +2 -2
  259. package/dist/src/status/status-roles.js +9 -18
  260. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  261. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  262. package/dist/src/team/team-hold.js +105 -70
  263. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
  264. package/dist/src/team/team-polls.js +53 -201
  265. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  266. package/dist/src/team/team-state.js +4 -13
  267. package/dist/src/text/datetime.js +16 -7
  268. package/dist/src/text/markdown.js +1 -1
  269. package/dist/src/text/phrases.js +29 -15
  270. package/dist/src/text/suggest.js +1 -1
  271. package/dist/src/text/text.js +6 -5
  272. package/dist/src/ui/alert-feeds.js +46 -111
  273. package/dist/src/ui/badges.js +17 -88
  274. package/dist/src/ui/gui/gui-client-boot.js +2 -8
  275. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  276. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
  277. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  278. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  279. package/dist/src/ui/gui/gui-client-history.js +28 -14
  280. package/dist/src/ui/gui/gui-client-model.js +0 -12
  281. package/dist/src/ui/gui/gui-client-operator.js +7 -5
  282. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  283. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  284. package/dist/src/ui/gui/gui-client-report.js +1 -1
  285. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  286. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  287. package/dist/src/ui/gui/gui-client-team.js +12 -6
  288. package/dist/src/ui/gui/gui-client.js +7 -1
  289. package/dist/src/ui/progress-data.js +107 -44
  290. package/dist/src/ui/run-progress-model.js +20 -4
  291. package/dist/src/ui/status-model.js +2 -24
  292. package/dist/src/ui/status-payload.js +11 -30
  293. package/dist/src/ui/status-render.js +13 -24
  294. package/dist/src/ui/team-alert-backlog.js +14 -204
  295. package/dist/src/ui/team-alert-shared.js +6 -29
  296. package/dist/src/ui/team-alert-status.js +9 -170
  297. package/dist/src/ui/team-alert-tail.js +19 -215
  298. package/dist/src/ui/team-alerts.js +8 -15
  299. package/dist/src/ui/transcript-run.js +27 -16
  300. package/dist/src/ui/tui/tui-backlog.js +2 -2
  301. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  302. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  303. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  304. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  305. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  306. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  307. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  308. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  309. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  310. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  311. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  312. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  313. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  314. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  315. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  316. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  317. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  318. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  319. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  320. package/dist/src/ui/tui/tui-editors.js +5 -10
  321. package/dist/src/ui/tui/tui-frame.js +182 -134
  322. package/dist/src/ui/tui/tui-guard.js +20 -0
  323. package/dist/src/ui/tui/tui-input.js +167 -98
  324. package/dist/src/ui/tui/tui-keymap.js +51 -29
  325. package/dist/src/ui/tui/tui-keys.js +1 -1
  326. package/dist/src/ui/tui/tui-pane-activity.js +5 -4
  327. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  328. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  329. package/dist/src/ui/tui/tui-pane-health.js +252 -230
  330. package/dist/src/ui/tui/tui-pane-history.js +67 -74
  331. package/dist/src/ui/tui/tui-pane-pending.js +288 -247
  332. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  333. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  334. package/dist/src/ui/tui/tui-pane-session.js +15 -10
  335. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  336. package/dist/src/ui/tui/tui-pane.js +14 -11
  337. package/dist/src/ui/tui/tui-panes.js +38 -33
  338. package/dist/src/ui/tui/tui.js +4 -7
  339. package/dist/src/verdict/fix-claim.js +107 -56
  340. package/dist/src/verdict/refusal.js +1 -1
  341. package/dist/src/verdict/reply-contract.js +3 -4
  342. package/package.json +5 -5
  343. package/dist/src/concurrency/provider-lease.js +0 -105
  344. package/dist/src/review/review-followup.js +0 -87
  345. package/dist/src/review/review-run.js +0 -395
  346. package/dist/src/runner/model-fallback.js +0 -76
  347. package/dist/src/runner/runner-fallback.js +0 -66
  348. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  349. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  350. /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
  351. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  352. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  353. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  354. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
@@ -3,7 +3,7 @@ import { renderMergeQueue, revisionNote, retryNote, scopeMergeQueue } from "../.
3
3
  import { cancelQueuedMerge } from "../../merge/merge-cancel.js";
4
4
  import { moveQueuedMergeToTop } from "../../merge/merge-top.js";
5
5
  import { mergeChanges } from "../../merge/merge-slot.js";
6
- import { orchestratorAlive } from "../../team/orchestrator-info.js";
6
+ import { orchestratorAlive } from "../../team/team-orchestrator-info.js";
7
7
  import { queueInFlight } from "../../status/status-polls.js";
8
8
  import { renderChangeOverlaps, renderRoleChanges, renderTeamChange } from "../../change/change-render.js";
9
9
  import { scopeChangeOverlaps } from "../../change/change-data.js";
@@ -15,7 +15,9 @@ import { renderPostReviews, postReviewStatusLabel, scopePostReviews, POST_REVIEW
15
15
  import { acceptPostReview } from "../../review/post-merge-review-accept.js";
16
16
  import { squash } from "../../text/text.js";
17
17
  import { promptLineFree } from "./tui-pane-ctx.js";
18
+ import { paneAtHead } from "./tui-pane-session.js";
18
19
  import { arrowDir } from "./tui-keymap.js";
20
+ import { distinctOrdered } from "../../collections.js";
19
21
  import { moveEntrySelection } from "./tui-backlog.js";
20
22
  /** The overlapping-files block under the roster: empty for a no-base view (the roster above
21
23
  * already says the main branch is missing), else the CLI's renderChangeOverlaps under a
@@ -37,32 +39,19 @@ export function pendingOverlapsText(view, scope) {
37
39
  export function overlapHolders(view) {
38
40
  if (view.state === "no-base")
39
41
  return [];
40
- const seen = [];
41
- for (const o of view.overlaps)
42
- for (const r of o.roles)
43
- if (!seen.includes(r))
44
- seen.push(r);
45
- return seen;
42
+ return distinctOrdered(view.overlaps.flatMap((o) => o.roles));
46
43
  }
47
44
  /** The distinct roles named across the merge queue, in first-seen (queue) order; empty for an
48
45
  * empty queue. The queue's own roles lead the pane's Ctrl+G scope cycle, before the overlap
49
46
  * holders — the queue scope is the `tumwater queue --role <id>` twin. */
50
47
  export function mergeQueueRoles(view) {
51
- const seen = [];
52
- for (const e of view.entries)
53
- if (!seen.includes(e.role))
54
- seen.push(e.role);
55
- return seen;
48
+ return distinctOrdered(view.entries.map((e) => e.role));
56
49
  }
57
50
  /** The distinct roles named across the post-merge review queue, in first-seen (FIFO) order;
58
51
  * empty for an empty queue. The review block's contribution to the pane's Ctrl+G scope cycle,
59
52
  * after the queue's roles — the review scope is the `tumwater reviews --role <id>` twin. */
60
53
  export function postReviewRoles(entries) {
61
- const seen = [];
62
- for (const e of entries)
63
- if (!seen.includes(e.role))
64
- seen.push(e.role);
65
- return seen;
54
+ return distinctOrdered(entries.map((e) => e.role));
66
55
  }
67
56
  /** The heading over one overlap's drill-down: its place in the list and the path. */
68
57
  export function overlapDrillHeading(index, total, path) {
@@ -274,6 +263,19 @@ export function createPendingPane(session) {
274
263
  }
275
264
  }
276
265
  };
266
+ // Re-render the whole queue body from scratch after a filter (Ctrl+G role scope, Ctrl+O
267
+ // review state) cycled: rebuild the rows off the current filter, drop any overlap drill,
268
+ // reset the composed body and the pane cache to the new content, and repaint from the top.
269
+ // The shared tail of the two filter-cycle arms, so both filters repaint identically.
270
+ const repaintQueueBody = () => {
271
+ rebuildQueueRows();
272
+ overlapIndex = -1;
273
+ ++drillGen;
274
+ head = queueAndReviews() + rosterText;
275
+ compose();
276
+ session.paneCache = base;
277
+ paneAtHead(session);
278
+ };
277
279
  const refresh = () => {
278
280
  // Generation guard: each refresh bumps the counter, and a resolution paints only if it
279
281
  // is the latest request's — Ctrl+R held twice, or a refresh raced with a re-entry,
@@ -307,275 +309,314 @@ export function createPendingPane(session) {
307
309
  session.requestRender();
308
310
  });
309
311
  };
310
- const handleKey = (key, ctx) => {
311
- if (key.ctrl && key.name === "r" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
312
- // Ctrl+R on the Pending pane refreshes in place — the terminal twin of the dashboard
313
- // Pending view's Refresh button: re-read the durable merge queue and restart the
314
- // change-roster collector, painting what the queue holds now. The pane re-enters at
315
- // its head so the changed body starts at the top; the roster's git read is a fresh
316
- // collector run, and a run already in flight is superseded by the new read.
317
- refresh();
318
- session.paneScroll = 0;
319
- session.requestRender();
320
- return true;
321
- }
322
- if (key.ctrl && key.name === "g" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
323
- const queueRoles = lastQueue ? mergeQueueRoles(lastQueue) : [];
324
- const reviewRoles = postReviewRoles(lastReviews);
325
- const holders = lastOverlaps?.state === "ready" ? overlapHolders(lastOverlaps) : [];
326
- const roles = [
327
- ...queueRoles,
328
- ...reviewRoles.filter((r) => !queueRoles.includes(r)),
329
- ...holders.filter((h) => !queueRoles.includes(h) && !reviewRoles.includes(h)),
330
- ];
331
- if (roles.length === 0) {
332
- roleScope = null;
333
- rebuildQueueRows();
334
- head = queueAndReviews() + rosterText;
335
- compose();
336
- session.paneCache = base;
337
- session.flashMessage("no queued merges, post-merge reviews or overlapping files to scope");
338
- return true;
339
- }
340
- const slots = [null, ...roles];
341
- const at = roleScope === null ? 0 : slots.indexOf(roleScope);
342
- roleScope = slots[(at + 1) % slots.length] ?? null;
312
+ // The shared Pending-pane key-arm guard: the prompt line is free (an editor would keep the
313
+ // key for its own view's meaning) and the Pending view (roleIds.length + 5) is current. True
314
+ // means the arm may run.
315
+ const onPendingView = (ctx) => promptLineFree(ctx.es) && session.view === session.roleIds.length + 5;
316
+ // Ctrl+R on the Pending pane refreshes in place — the terminal twin of the dashboard
317
+ // Pending view's Refresh button: re-read the durable merge queue and restart the
318
+ // change-roster collector, painting what the queue holds now. The pane re-enters at
319
+ // its head so the changed body starts at the top; the roster's git read is a fresh
320
+ // collector run, and a run already in flight is superseded by the new read.
321
+ const pendingRefresh = (ctx) => {
322
+ if (!onPendingView(ctx))
323
+ return false;
324
+ refresh();
325
+ paneAtHead(session);
326
+ return true;
327
+ };
328
+ // Ctrl+G on the Pending pane cycles roleScope through each role the merge queue, the
329
+ // post-merge reviews and the overlapping files name and back to all (the `queue --role <id>`,
330
+ // `reviews --role <id>` and `diff --overlaps --role <id>` twins, one scope covering all three
331
+ // surfaces); with nothing to scope it resets to null and flashes the empty message. Rebounds
332
+ // the queue and review rows off the new scope and repaints from the top.
333
+ const pendingScopeCycle = (ctx) => {
334
+ if (!onPendingView(ctx))
335
+ return false;
336
+ const queueRoles = lastQueue ? mergeQueueRoles(lastQueue) : [];
337
+ const reviewRoles = postReviewRoles(lastReviews);
338
+ const holders = lastOverlaps?.state === "ready" ? overlapHolders(lastOverlaps) : [];
339
+ const roles = [
340
+ ...queueRoles,
341
+ ...reviewRoles.filter((r) => !queueRoles.includes(r)),
342
+ ...holders.filter((h) => !queueRoles.includes(h) && !reviewRoles.includes(h)),
343
+ ];
344
+ if (roles.length === 0) {
345
+ roleScope = null;
343
346
  rebuildQueueRows();
344
- overlapIndex = -1;
345
- ++drillGen;
346
347
  head = queueAndReviews() + rosterText;
347
348
  compose();
348
349
  session.paneCache = base;
349
- session.paneScroll = 0;
350
- session.requestRender();
350
+ session.flashMessage("no queued merges, post-merge reviews or overlapping files to scope");
351
351
  return true;
352
352
  }
353
- if (key.ctrl && key.name === "o" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
354
- // Ctrl+O on the Pending pane cycles the post-merge review state filter through all,
355
- // waiting, failing and rejected and back — the `tumwater reviews --status` twin and the
356
- // dashboard Post-merge reviews State picker, meaning the full state vocabulary even when
357
- // the queue holds none of a state (the renderer's `no post-merge reviews matching
358
- // --status …` line is the empty-selection state). Each kept review keeps its whole-queue
359
- // position, so Ctrl+A still accepts the right entry under any filter. The pane's own arm
360
- // is routed ahead of History's in tui-panes.ts, so it never advances the History outcome
361
- // filter; the Ctrl+G role scope composes with this filter in the block's header.
362
- const slots = [null, ...POST_REVIEW_STATES];
363
- const at = reviewState === null ? 0 : slots.indexOf(reviewState);
364
- reviewState = slots[(at + 1) % slots.length] ?? null;
365
- rebuildQueueRows();
353
+ const slots = [null, ...roles];
354
+ const at = roleScope === null ? 0 : slots.indexOf(roleScope);
355
+ roleScope = slots[(at + 1) % slots.length] ?? null;
356
+ repaintQueueBody();
357
+ return true;
358
+ };
359
+ // Ctrl+O on the Pending pane cycles the post-merge review state filter through all,
360
+ // waiting, failing and rejected and back — the `tumwater reviews --status` twin and the
361
+ // dashboard Post-merge reviews State picker, meaning the full state vocabulary even when
362
+ // the queue holds none of a state (the renderer's `no post-merge reviews matching
363
+ // --status …` line is the empty-selection state). Each kept review keeps its whole-queue
364
+ // position, so Ctrl+A still accepts the right entry under any filter. The pane's own arm
365
+ // is routed ahead of History's in tui-panes.ts, so it never advances the History outcome
366
+ // filter; the Ctrl+G role scope composes with this filter in the block's header.
367
+ const pendingReviewStateCycle = (ctx) => {
368
+ if (!onPendingView(ctx))
369
+ return false;
370
+ const slots = [null, ...POST_REVIEW_STATES];
371
+ const at = reviewState === null ? 0 : slots.indexOf(reviewState);
372
+ reviewState = slots[(at + 1) % slots.length] ?? null;
373
+ repaintQueueBody();
374
+ return true;
375
+ };
376
+ // Ctrl+N on the Pending pane steps through the overlapping files, painting each holder's
377
+ // change limited to that file (the `tumwater diff --role a,b --path <path>` view), scoped
378
+ // by the Ctrl+G role selection; an exhausted list returns to the composed base. Each step
379
+ // re-runs the collector guarded against a stale resolution (drill/refresh generation and
380
+ // the view still being current).
381
+ const pendingOverlapDrill = (ctx) => {
382
+ if (!onPendingView(ctx))
383
+ return false;
384
+ const overlaps = lastOverlaps && roleScope !== null ? scopeChangeOverlaps(lastOverlaps, [roleScope]) : lastOverlaps;
385
+ if (overlaps?.state !== "ready" || overlaps.overlaps.length === 0) {
386
+ session.flashMessage("no overlapping files to step through");
387
+ return true;
388
+ }
389
+ if (++overlapIndex >= overlaps.overlaps.length) {
366
390
  overlapIndex = -1;
367
391
  ++drillGen;
368
- head = queueAndReviews() + rosterText;
369
- compose();
370
392
  session.paneCache = base;
371
- session.paneScroll = 0;
372
- session.requestRender();
393
+ paneAtHead(session);
373
394
  return true;
374
395
  }
375
- if (key.ctrl && key.name === "n" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
376
- const overlaps = lastOverlaps && roleScope !== null ? scopeChangeOverlaps(lastOverlaps, [roleScope]) : lastOverlaps;
377
- if (overlaps?.state !== "ready" || overlaps.overlaps.length === 0) {
378
- session.flashMessage("no overlapping files to step through");
379
- return true;
380
- }
381
- if (++overlapIndex >= overlaps.overlaps.length) {
382
- overlapIndex = -1;
383
- ++drillGen;
384
- session.paneCache = base;
385
- session.paneScroll = 0;
386
- session.requestRender();
387
- return true;
388
- }
389
- const { path, roles } = overlaps.overlaps[overlapIndex];
390
- const heading = overlapDrillHeading(overlapIndex, overlaps.overlaps.length, path);
391
- session.paneCache = heading + "\n\nloading…";
392
- session.paneScroll = 0;
396
+ const { path, roles } = overlaps.overlaps[overlapIndex];
397
+ const heading = overlapDrillHeading(overlapIndex, overlaps.overlaps.length, path);
398
+ session.paneCache = heading + "\n\nloading…";
399
+ paneAtHead(session);
400
+ const gen = ++drillGen;
401
+ const refresh0 = refreshGen;
402
+ const current = () => gen === drillGen && refresh0 === refreshGen && session.view === session.roleIds.length + 5;
403
+ void Promise.all(roles.map((r) => session.collectChange(session.root, r, { path }))).then((views) => {
404
+ if (!current())
405
+ return;
406
+ session.paneCache = heading + "\n\n" + renderRoleChanges({ mainBranch: overlaps.mainBranch, roles: views });
393
407
  session.requestRender();
394
- const gen = ++drillGen;
395
- const refresh0 = refreshGen;
396
- const current = () => gen === drillGen && refresh0 === refreshGen && session.view === session.roleIds.length + 5;
397
- void Promise.all(roles.map((r) => session.collectChange(session.root, r, { path }))).then((views) => {
398
- if (!current())
399
- return;
400
- session.paneCache = heading + "\n\n" + renderRoleChanges({ mainBranch: overlaps.mainBranch, roles: views });
401
- session.requestRender();
402
- }, (err) => {
403
- if (!current())
404
- return;
405
- session.flashError(err);
406
- session.requestRender();
407
- });
408
- return true;
409
- }
410
- const arrow = arrowDir(key.name);
411
- if (session.view === session.roleIds.length + 5 && arrow !== null && promptLineFree(ctx.es)) {
412
- // In the Pending pane, up/down select a queued merge row in full (wrapping, the backlog
413
- // pane's rule) instead of editing the prompt; an empty queue has nothing to select, so
414
- // moveEntrySelection clears the selection. The selected detail shares paneScroll with the
415
- // list, so a newly opened row must start at its head, or it renders scrolled into the
416
- // middle of the previous row's body.
417
- selectedQueue = moveEntrySelection(queueRows.length, selectedQueue, arrow);
418
- const selRow = selectedQueue === null ? undefined : queueRows[selectedQueue];
419
- queueDetail = selRow ? rowDetail(selRow) : "";
420
- session.paneScroll = 0;
408
+ }, (err) => {
409
+ if (!current())
410
+ return;
411
+ session.flashError(err);
421
412
  session.requestRender();
413
+ });
414
+ return true;
415
+ };
416
+ // Up/down in the Pending pane select a queued merge row in full (wrapping, the backlog
417
+ // pane's rule) instead of editing the prompt; an empty queue has nothing to select, so
418
+ // moveEntrySelection clears the selection. The selected detail shares paneScroll with the
419
+ // list, so a newly opened row must start at its head, or it renders scrolled into the
420
+ // middle of the previous row's body.
421
+ const pendingSelectRow = (arrow, ctx) => {
422
+ if (!onPendingView(ctx))
423
+ return false;
424
+ selectedQueue = moveEntrySelection(queueRows.length, selectedQueue, arrow);
425
+ const selRow = selectedQueue === null ? undefined : queueRows[selectedQueue];
426
+ queueDetail = selRow ? rowDetail(selRow) : "";
427
+ paneAtHead(session);
428
+ return true;
429
+ };
430
+ // Esc on the Pending pane returns it to list mode; the cached list is untouched (nothing
431
+ // was cancelled), so the pane shows the same renderMergeQueue it entered with. The shared
432
+ // paneScroll resets with the body switch, like a selection move.
433
+ const pendingClearSelection = (ctx) => {
434
+ if (!onPendingView(ctx) || selectedQueue === null)
435
+ return false;
436
+ selectedQueue = null;
437
+ queueDetail = "";
438
+ paneAtHead(session);
439
+ return true;
440
+ };
441
+ // Ctrl+X on the Pending pane cancels the selected queued merge (the terminal twin of
442
+ // `tumwater queue --cancel <n>`): a review row names its own action, the first press on a
443
+ // merge arms it (any other key keeps it), and the second press re-reads the queue, guards
444
+ // the in-flight and shifted-away races, then removes the entry through the CLI's own core.
445
+ const pendingCancelMerge = (ctx) => {
446
+ if (!onPendingView(ctx))
447
+ return false;
448
+ const row = selectedRow();
449
+ if (row === null)
422
450
  return true;
423
- }
424
- if (key.name === "escape" && session.view === session.roleIds.length + 5 && selectedQueue !== null && promptLineFree(ctx.es)) {
425
- // Esc on the Pending pane returns it to list mode; the cached list is untouched (nothing
426
- // was cancelled), so the pane shows the same renderMergeQueue it entered with. The shared
427
- // paneScroll resets with the body switch, like a selection move.
428
- selectedQueue = null;
429
- queueDetail = "";
430
- session.paneScroll = 0;
451
+ if (row.kind === "review") {
452
+ session.flashMessage("that row is a post-merge review — Ctrl+A accepts it");
431
453
  session.requestRender();
432
454
  return true;
433
455
  }
434
- if (key.ctrl && key.name === "x" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
435
- const row = selectedRow();
436
- if (row === null)
437
- return true;
438
- if (row.kind === "review") {
439
- session.flashMessage("that row is a post-merge review — Ctrl+A accepts it");
440
- session.requestRender();
441
- return true;
442
- }
443
- if (session.armedQueue?.sha === row.entry.sha && session.armedQueue?.position === row.position) {
444
- // Second press on the same entry: re-read the queue, guard the in-flight race, then
445
- // remove it through the CLI's own core (`tumwater queue --cancel <n>`), so the pane,
446
- // the CLI, and the dashboard cancel cannot drift.
447
- const confirm = async () => {
448
- if (queueShiftedAway(row))
449
- return;
450
- if (guardMergeInFlight(row))
451
- return;
452
- const outcome = await cancelQueuedMerge(session.root, row.position);
453
- if (outcome.status === "cancelled") {
454
- selectedQueue = null;
455
- queueDetail = "";
456
- session.flashMessage(`cancelled queue entry ${row.position}: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
457
- refresh();
458
- session.requestRender();
459
- }
460
- else {
461
- selectedQueue = null;
462
- queueDetail = "";
463
- session.flashMessage("that merge already left the queue");
464
- refresh();
465
- session.requestRender();
466
- }
467
- };
468
- void confirm();
469
- }
470
- else {
471
- session.queueArmed = { position: row.position, sha: row.entry.sha };
472
- session.flashMessage(`press Ctrl+X again to cancel merge #${row.position} — any other key keeps it`);
473
- session.requestRender();
474
- }
475
- return true;
476
- }
477
- if (key.ctrl && key.name === "a" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
478
- // Ctrl+A on the Pending pane accepts the selected post-merge review unreviewed (the
479
- // terminal twin of `tumwater reviews --accept <n>`), once selected and twice pressed
480
- // like the queue's Ctrl+X cancel. A merge row names its own action instead; an unarmed
481
- // press sets reviewArmed, and the second press re-reads the queue and settles the entry
482
- // through the CLI's own core, so the pane and the CLI cannot drift.
483
- const review = selectedRow();
484
- if (review === null)
485
- return true;
486
- if (review.kind === "merge") {
487
- session.flashMessage("that row is a queued merge — Ctrl+X cancels it");
488
- session.requestRender();
489
- return true;
490
- }
491
- if (session.armedReview?.position === review.position && session.armedReview?.commit === review.entry.commit) {
492
- const confirm = async () => {
493
- const entries = readPostReviews(session.root);
494
- if (entries[review.position - 1]?.commit !== review.entry.commit) {
495
- session.flashMessage("the review queue changed — try again");
496
- refresh();
497
- session.requestRender();
498
- return;
499
- }
500
- const outcome = acceptPostReview(session.root, review.position);
501
- if (outcome.status === "accepted") {
502
- selectedQueue = null;
503
- queueDetail = "";
504
- session.flashMessage(`accepted post-merge review of ${shortSha(outcome.commit)} unreviewed — ${outcome.summary}`);
505
- refresh();
506
- session.requestRender();
507
- }
508
- else if (outcome.status === "settled") {
509
- session.flashMessage(`review #${review.position} already merged its fix on main — nothing to accept`);
510
- refresh();
511
- session.requestRender();
512
- }
513
- else {
514
- session.flashMessage("the review queue changed — try again");
515
- refresh();
516
- session.requestRender();
517
- }
518
- };
519
- void confirm();
520
- }
521
- else {
522
- session.reviewArmed = { position: review.position, commit: review.entry.commit };
523
- session.flashMessage(`press Ctrl+A again to accept review #${review.position} — any other key keeps it`);
524
- session.requestRender();
525
- }
526
- return true;
527
- }
528
- if (key.ctrl && key.name === "v" && promptLineFree(ctx.es) && session.view === session.roleIds.length + 5) {
529
- // Move the selected merge to the head of the queue — the pane's twin of `tumwater
530
- // queue --top <n>`, through the same core the CLI and the dashboard use, so the three
531
- // cannot drift on the rename or the already-first/out-of-range outcomes. A single
532
- // keypress is enough because the move is reversible and non-destructive (unlike
533
- // Ctrl+X's two-step cancel arm).
534
- const row = selectedRow();
535
- if (row === null)
536
- return true;
537
- if (row.kind === "review") {
538
- session.flashMessage("that row is a post-merge review — Ctrl+A accepts it");
539
- session.requestRender();
540
- return true;
541
- }
542
- const top = async () => {
543
- if (row.position === 1) {
544
- session.flashMessage("merge #1 is already at the front");
545
- session.requestRender();
546
- return;
547
- }
456
+ if (session.armedQueue?.sha === row.entry.sha && session.armedQueue?.position === row.position) {
457
+ // Second press on the same entry: re-read the queue, guard the in-flight race, then
458
+ // remove it through the CLI's own core (`tumwater queue --cancel <n>`), so the pane,
459
+ // the CLI, and the dashboard cancel cannot drift.
460
+ const confirm = async () => {
548
461
  if (queueShiftedAway(row))
549
462
  return;
550
463
  if (guardMergeInFlight(row))
551
464
  return;
552
- const outcome = moveQueuedMergeToTop(session.root, row.position);
553
- if (outcome.status === "topped") {
465
+ const outcome = await cancelQueuedMerge(session.root, row.position);
466
+ if (outcome.status === "cancelled") {
554
467
  selectedQueue = null;
555
468
  queueDetail = "";
556
- session.flashMessage(`moved merge #${row.position} to the front: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
469
+ session.flashMessage(`cancelled queue entry ${row.position}: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
470
+ refresh();
471
+ session.requestRender();
472
+ }
473
+ else {
474
+ selectedQueue = null;
475
+ queueDetail = "";
476
+ session.flashMessage("that merge already left the queue");
477
+ refresh();
478
+ session.requestRender();
479
+ }
480
+ };
481
+ void confirm();
482
+ }
483
+ else {
484
+ session.queueArmed = { position: row.position, sha: row.entry.sha };
485
+ session.flashMessage(`press Ctrl+X again to cancel merge #${row.position} — any other key keeps it`);
486
+ session.requestRender();
487
+ }
488
+ return true;
489
+ };
490
+ // Ctrl+A on the Pending pane accepts the selected post-merge review unreviewed (the
491
+ // terminal twin of `tumwater reviews --accept <n>`), once selected and twice pressed
492
+ // like the queue's Ctrl+X cancel. A merge row names its own action instead; an unarmed
493
+ // press sets reviewArmed, and the second press re-reads the queue and settles the entry
494
+ // through the CLI's own core, so the pane and the CLI cannot drift.
495
+ const pendingAcceptReview = (ctx) => {
496
+ if (!onPendingView(ctx))
497
+ return false;
498
+ const review = selectedRow();
499
+ if (review === null)
500
+ return true;
501
+ if (review.kind === "merge") {
502
+ session.flashMessage("that row is a queued merge — Ctrl+X cancels it");
503
+ session.requestRender();
504
+ return true;
505
+ }
506
+ if (session.armedReview?.position === review.position && session.armedReview?.commit === review.entry.commit) {
507
+ const confirm = async () => {
508
+ const entries = readPostReviews(session.root);
509
+ if (entries[review.position - 1]?.commit !== review.entry.commit) {
510
+ session.flashMessage("the review queue changed — try again");
557
511
  refresh();
558
512
  session.requestRender();
513
+ return;
559
514
  }
560
- else if (outcome.status === "already-first") {
561
- session.flashMessage(`merge #${row.position} is already at the front`);
515
+ const outcome = acceptPostReview(session.root, review.position);
516
+ if (outcome.status === "accepted") {
517
+ selectedQueue = null;
518
+ queueDetail = "";
519
+ session.flashMessage(`accepted post-merge review of ${shortSha(outcome.commit)} unreviewed — ${outcome.summary}`);
562
520
  refresh();
563
521
  session.requestRender();
564
522
  }
565
- else if (outcome.status === "out-of-range") {
566
- session.flashMessage("the merge queue changed — try again");
523
+ else if (outcome.status === "settled") {
524
+ session.flashMessage(`review #${review.position} already merged its fix on main — nothing to accept`);
567
525
  refresh();
568
526
  session.requestRender();
569
527
  }
570
528
  else {
571
- session.flashMessage("that merge already left the queue");
529
+ session.flashMessage("the review queue changed — try again");
572
530
  refresh();
573
531
  session.requestRender();
574
532
  }
575
533
  };
576
- void top();
534
+ void confirm();
535
+ }
536
+ else {
537
+ session.reviewArmed = { position: review.position, commit: review.entry.commit };
538
+ session.flashMessage(`press Ctrl+A again to accept review #${review.position} — any other key keeps it`);
539
+ session.requestRender();
540
+ }
541
+ return true;
542
+ };
543
+ // Ctrl+V moves the selected merge to the head of the queue — the pane's twin of `tumwater
544
+ // queue --top <n>`, through the same core the CLI and the dashboard use, so the three
545
+ // cannot drift on the rename or the already-first/out-of-range outcomes. A single
546
+ // keypress is enough because the move is reversible and non-destructive (unlike
547
+ // Ctrl+X's two-step cancel arm); a review row names its own action instead.
548
+ const pendingMoveToTop = (ctx) => {
549
+ if (!onPendingView(ctx))
550
+ return false;
551
+ const row = selectedRow();
552
+ if (row === null)
553
+ return true;
554
+ if (row.kind === "review") {
555
+ session.flashMessage("that row is a post-merge review — Ctrl+A accepts it");
556
+ session.requestRender();
577
557
  return true;
578
558
  }
559
+ const top = async () => {
560
+ if (row.position === 1) {
561
+ session.flashMessage(`merge #${row.position} is already at the front`);
562
+ session.requestRender();
563
+ return;
564
+ }
565
+ if (queueShiftedAway(row))
566
+ return;
567
+ if (guardMergeInFlight(row))
568
+ return;
569
+ const outcome = moveQueuedMergeToTop(session.root, row.position);
570
+ if (outcome.status === "topped") {
571
+ selectedQueue = null;
572
+ queueDetail = "";
573
+ session.flashMessage(`moved merge #${row.position} to the front: ${roleLabel(outcome.role)} ${shortSha(outcome.sha)} — ${outcome.summary}`);
574
+ refresh();
575
+ session.requestRender();
576
+ }
577
+ else if (outcome.status === "already-first") {
578
+ session.flashMessage(`merge #${row.position} is already at the front`);
579
+ refresh();
580
+ session.requestRender();
581
+ }
582
+ else if (outcome.status === "out-of-range") {
583
+ session.flashMessage("the merge queue changed — try again");
584
+ refresh();
585
+ session.requestRender();
586
+ }
587
+ else {
588
+ session.flashMessage("that merge already left the queue");
589
+ refresh();
590
+ session.requestRender();
591
+ }
592
+ };
593
+ void top();
594
+ return true;
595
+ };
596
+ const ctrlRoutes = {
597
+ r: pendingRefresh,
598
+ g: pendingScopeCycle,
599
+ o: pendingReviewStateCycle,
600
+ n: pendingOverlapDrill,
601
+ x: pendingCancelMerge,
602
+ a: pendingAcceptReview,
603
+ v: pendingMoveToTop,
604
+ };
605
+ const ctrlRoute = (key, ctx) => {
606
+ if (!key.ctrl || key.name === undefined)
607
+ return null;
608
+ const route = ctrlRoutes[key.name];
609
+ return route === undefined ? null : route(ctx);
610
+ };
611
+ const handleKey = (key, ctx) => {
612
+ const ctrl = ctrlRoute(key, ctx);
613
+ if (ctrl !== null)
614
+ return ctrl;
615
+ const arrow = arrowDir(key.name);
616
+ if (arrow !== null)
617
+ return pendingSelectRow(arrow, ctx);
618
+ if (key.name === "escape")
619
+ return pendingClearSelection(ctx);
579
620
  return false;
580
621
  };
581
622
  return {