tumwater 0.4.2 → 0.4.3

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 (305) 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} +25 -12
  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 +1 -8
  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 +9 -0
  14. package/dist/src/agent/codex/codex-backend.js +2 -2
  15. package/dist/src/agent/codex/codex-env.js +28 -15
  16. package/dist/src/agent/codex/codex-hook-trust.js +39 -20
  17. package/dist/src/agent/codex/codex-rollout.js +93 -56
  18. package/dist/src/agent/codex/codex-stream.js +1 -1
  19. package/dist/src/agent/env-scrub.js +18 -0
  20. package/dist/src/agent/image-input.js +68 -11
  21. package/dist/src/agent/notes-shim.js +2 -2
  22. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  23. package/dist/src/agent/opencode/opencode-catalog.js +50 -27
  24. package/dist/src/agent/opencode/opencode-config.js +32 -14
  25. package/dist/src/agent/opencode/opencode-env.js +12 -13
  26. package/dist/src/agent/opencode/opencode-stream.js +47 -25
  27. package/dist/src/agent/session-ledger.js +1 -1
  28. package/dist/src/agent/subscription-account.js +3 -3
  29. package/dist/src/backlog/backlog-cli-view.js +81 -41
  30. package/dist/src/backlog/backlog-cli.js +57 -26
  31. package/dist/src/backlog/backlog-eligibility.js +58 -102
  32. package/dist/src/backlog/backlog-md.js +5 -5
  33. package/dist/src/backlog/backlog-parts.js +65 -27
  34. package/dist/src/backlog/backlog-release.js +12 -12
  35. package/dist/src/backlog/backlog-structure.js +114 -61
  36. package/dist/src/backlog/backlog.js +1 -1
  37. package/dist/src/baseline/main-baseline.js +148 -84
  38. package/dist/src/budget/budget-fallback-view.js +100 -243
  39. package/dist/src/budget/budget-wiring.js +29 -65
  40. package/dist/src/budget/fallback-breaker.js +2 -2
  41. package/dist/src/build/build-check-detect.js +43 -14
  42. package/dist/src/build/build-check-events.js +2 -2
  43. package/dist/src/build/build-check-scoped.js +40 -16
  44. package/dist/src/build/build-check.js +13 -5
  45. package/dist/src/cli/cli-command-args.js +89 -52
  46. package/dist/src/cli/cli-flag-specs.js +42 -22
  47. package/dist/src/cli/cli-init.js +34 -12
  48. package/dist/src/cli/cli-query-commands.js +1 -1
  49. package/dist/src/cli/cli-repo-gate.js +2 -2
  50. package/dist/src/cli/cli-start.js +91 -58
  51. package/dist/src/cli/completion.js +82 -46
  52. package/dist/src/cli/config-commands.js +82 -71
  53. package/dist/src/cli/help.js +4 -6
  54. package/dist/src/cli/log-commands.js +43 -23
  55. package/dist/src/cli/notify-command.js +2 -2
  56. package/dist/src/cli/question-commands.js +86 -60
  57. package/dist/src/cli.js +181 -135
  58. package/dist/src/collections.js +31 -16
  59. package/dist/src/concurrency/check-slots.js +70 -0
  60. package/dist/src/concurrency/lock.js +22 -0
  61. package/dist/src/concurrency/run-permit.js +49 -0
  62. package/dist/src/concurrency/semaphore.js +46 -250
  63. package/dist/src/config/config-editable-keys.js +4 -7
  64. package/dist/src/config/config-fallbacks.js +81 -101
  65. package/dist/src/config/config-field-checks.js +66 -37
  66. package/dist/src/config/config-keys.js +5 -20
  67. package/dist/src/config/config-live.js +4 -28
  68. package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
  69. package/dist/src/config/config-request.js +153 -102
  70. package/dist/src/config/config-schema.js +9 -11
  71. package/dist/src/config/config-selector-checks.js +1 -1
  72. package/dist/src/config/config-validation-agents.js +2 -2
  73. package/dist/src/config/config-validation-roles.js +4 -52
  74. package/dist/src/config/config-validation.js +4 -38
  75. package/dist/src/config/config-views.js +9 -34
  76. package/dist/src/config/config-write.js +6 -23
  77. package/dist/src/config/config.js +4 -5
  78. package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
  79. package/dist/src/doctor/doctor-backlog-plans.js +24 -24
  80. package/dist/src/doctor/doctor-backlog.js +7 -5
  81. package/dist/src/doctor/doctor-check-names.js +3 -6
  82. package/dist/src/doctor/doctor-checks.js +5 -5
  83. package/dist/src/doctor/doctor-claude-checks.js +44 -28
  84. package/dist/src/doctor/doctor-codex-checks.js +6 -6
  85. package/dist/src/doctor/doctor-contention-checks.js +44 -112
  86. package/dist/src/doctor/doctor-fallback-checks.js +2 -2
  87. package/dist/src/doctor/doctor-holds.js +6 -45
  88. package/dist/src/doctor/doctor-install.js +1 -1
  89. package/dist/src/doctor/doctor-launch-services.js +1 -1
  90. package/dist/src/doctor/doctor-model-checks.js +178 -94
  91. package/dist/src/doctor/doctor-notify.js +2 -2
  92. package/dist/src/doctor/doctor-opencode-checks.js +5 -4
  93. package/dist/src/doctor/doctor-orphans.js +120 -20
  94. package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
  95. package/dist/src/doctor/doctor-pi-checks.js +2 -2
  96. package/dist/src/doctor/doctor-principles.js +3 -3
  97. package/dist/src/doctor/doctor-project-checks.js +3 -3
  98. package/dist/src/doctor/doctor-role-notes.js +1 -1
  99. package/dist/src/doctor/doctor.js +8 -11
  100. package/dist/src/events/event-format.js +30 -54
  101. package/dist/src/events/event-labels.js +1 -5
  102. package/dist/src/events/event-legacy.js +1 -3
  103. package/dist/src/events/{notify.js → event-notify.js} +2 -2
  104. package/dist/src/events/event-read.js +1 -1
  105. package/dist/src/failure/failure-cluster.js +6 -6
  106. package/dist/src/failure/failure-data.js +8 -4
  107. package/dist/src/failure/failure-render.js +44 -34
  108. package/dist/src/failure/failure-state-change.js +4 -34
  109. package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
  110. package/dist/src/files/files.js +30 -0
  111. package/dist/src/files/json-object.js +8 -8
  112. package/dist/src/files/stat-cache.js +12 -8
  113. package/dist/src/gates/budget-gates.js +85 -84
  114. package/dist/src/gates/gate-polls.js +27 -68
  115. package/dist/src/gates/pause-gates.js +16 -2
  116. package/dist/src/gates/readiness.js +1 -1
  117. package/dist/src/gates/role-cap-gates.js +44 -35
  118. package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
  119. package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
  120. package/dist/src/git/git.js +75 -45
  121. package/dist/src/git/worktree-pool.js +3 -6
  122. package/dist/src/gui/gui-admin-endpoints.js +3 -3
  123. package/dist/src/gui/gui-backlog-commands.js +4 -4
  124. package/dist/src/gui/gui-backlog-endpoints.js +61 -41
  125. package/dist/src/gui/gui-command.js +43 -24
  126. package/dist/src/gui/gui-history-endpoints.js +100 -52
  127. package/dist/src/gui/gui-operator-commands.js +22 -23
  128. package/dist/src/gui/gui-prompt-commands.js +23 -23
  129. package/dist/src/gui/gui-report-endpoints.js +1 -1
  130. package/dist/src/history/history-data.js +71 -44
  131. package/dist/src/history/history-queued.js +49 -27
  132. package/dist/src/history/history.js +51 -33
  133. package/dist/src/inbox/prompt-commands.js +59 -42
  134. package/dist/src/init/init.js +88 -40
  135. package/dist/src/merge/merge-backlog-conflicts.js +1 -1
  136. package/dist/src/merge/merge-core.js +1 -1
  137. package/dist/src/merge/merge-drain.js +8 -11
  138. package/dist/src/merge/merge-pipeline.js +6 -7
  139. package/dist/src/merge/merge-post-review-drain.js +10 -13
  140. package/dist/src/merge/merge-requeue.js +1 -1
  141. package/dist/src/merge/merge-slot.js +0 -8
  142. package/dist/src/merge/merge-verify.js +8 -18
  143. package/dist/src/notes-cli.js +52 -42
  144. package/dist/src/operator/operator-commands.js +76 -44
  145. package/dist/src/operator/operator-intent.js +3 -3
  146. package/dist/src/orchestrator/orchestrator-launch.js +31 -63
  147. package/dist/src/orchestrator/orchestrator-poll.js +27 -32
  148. package/dist/src/orchestrator/orchestrator.js +8 -21
  149. package/dist/src/orchestrator/scheduling-admit.js +101 -68
  150. package/dist/src/orchestrator/scheduling-scope.js +1 -1
  151. package/dist/src/orchestrator/scheduling-skip.js +1 -1
  152. package/dist/src/orchestrator/startup-migrations.js +1 -1
  153. package/dist/src/paths.js +8 -7
  154. package/dist/src/pi/pi-args.js +5 -2
  155. package/dist/src/pi/pi-backend.js +2 -2
  156. package/dist/src/pi/pi-bin.js +3 -3
  157. package/dist/src/pi/pi-event-line.js +1 -1
  158. package/dist/src/pi/pi-models.js +43 -29
  159. package/dist/src/pi/pi-settings.js +43 -20
  160. package/dist/src/pi/pi-stream.js +32 -6
  161. package/dist/src/pi/pi-watchdogs.js +12 -5
  162. package/dist/src/pi/pi.js +2 -2
  163. package/dist/src/pi-extension/context-shake.js +32 -20
  164. package/dist/src/pi-extension/kill-guard.js +69 -0
  165. package/dist/src/process/process-group.js +49 -5
  166. package/dist/src/process/process.js +8 -8
  167. package/dist/src/process/run-marker.js +31 -13
  168. package/dist/src/process/supervisor.js +40 -28
  169. package/dist/src/prompt/dialect.js +1 -1
  170. package/dist/src/prompt/gate-notes.js +2 -2
  171. package/dist/src/prompt/notes-backlog.js +66 -37
  172. package/dist/src/prompt/principles.js +6 -4
  173. package/dist/src/prompt/prompt-followup.js +12 -4
  174. package/dist/src/prompt/prompt.js +88 -38
  175. package/dist/src/report/report-data.js +53 -37
  176. package/dist/src/report/report-fold.js +16 -9
  177. package/dist/src/report/report-render.js +21 -194
  178. package/dist/src/report/report-since-data.js +4 -3
  179. package/dist/src/report/report-since-render.js +173 -0
  180. package/dist/src/report/report-usage-fold.js +8 -8
  181. package/dist/src/report/report.js +46 -22
  182. package/dist/src/review/post-merge-review-render.js +9 -3
  183. package/dist/src/review/post-merge-review.js +1 -1
  184. package/dist/src/review/review-precheck.js +11 -1
  185. package/dist/src/review/review.js +40 -153
  186. package/dist/src/review/secret-leaks.js +26 -16
  187. package/dist/src/roles/role-catalog.js +11 -3
  188. package/dist/src/roles/role-guidance.js +16 -12
  189. package/dist/src/roles/role-render.js +1 -14
  190. package/dist/src/roles/role-view.js +9 -21
  191. package/dist/src/run/acceptance-greps.js +88 -45
  192. package/dist/src/run/run-apply.js +12 -6
  193. package/dist/src/run/run-detail-data.js +121 -68
  194. package/dist/src/run/run-detail-render.js +92 -35
  195. package/dist/src/run/run-detail.js +72 -41
  196. package/dist/src/run/run-prompt.js +63 -50
  197. package/dist/src/run/run-resume.js +76 -29
  198. package/dist/src/run/run-stage.js +135 -84
  199. package/dist/src/run/run-timing.js +8 -2
  200. package/dist/src/run/run-usage.js +31 -1
  201. package/dist/src/run/run-verdict.js +119 -50
  202. package/dist/src/run/stage-check.js +49 -32
  203. package/dist/src/run/stage-checks-shared.js +28 -0
  204. package/dist/src/run/stage-name-checks.js +111 -92
  205. package/dist/src/run/stage-path-checks.js +69 -42
  206. package/dist/src/run/stage-prose-checks.js +55 -40
  207. package/dist/src/runner/runner-outcome.js +6 -1
  208. package/dist/src/runner/runner-pi.js +3 -10
  209. package/dist/src/runner/runner-recovery.js +1 -1
  210. package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
  211. package/dist/src/runner/runner-run.js +97 -35
  212. package/dist/src/runner/runner-state-load.js +12 -9
  213. package/dist/src/runner/runner.js +41 -25
  214. package/dist/src/scheduling/claims.js +0 -4
  215. package/dist/src/scheduling/scheduling.js +53 -66
  216. package/dist/src/status/status-data.js +2 -2
  217. package/dist/src/status/status-role-notes.js +2 -2
  218. package/dist/src/status/status-roles.js +9 -17
  219. package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
  220. package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
  221. package/dist/src/team/team-hold.js +98 -65
  222. package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
  223. package/dist/src/team/team-polls.js +28 -126
  224. package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
  225. package/dist/src/team/team-state.js +4 -13
  226. package/dist/src/text/markdown.js +1 -1
  227. package/dist/src/text/phrases.js +20 -15
  228. package/dist/src/text/text.js +3 -3
  229. package/dist/src/ui/alert-feeds.js +49 -73
  230. package/dist/src/ui/badges.js +3 -13
  231. package/dist/src/ui/gui/gui-client-boot.js +2 -2
  232. package/dist/src/ui/gui/gui-client-composer.js +1 -1
  233. package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
  234. package/dist/src/ui/gui/gui-client-drawer.js +1 -1
  235. package/dist/src/ui/gui/gui-client-history.js +28 -14
  236. package/dist/src/ui/gui/gui-client-model.js +0 -12
  237. package/dist/src/ui/gui/gui-client-pending.js +7 -6
  238. package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
  239. package/dist/src/ui/gui/gui-client-report.js +1 -1
  240. package/dist/src/ui/gui/gui-client-roles.js +0 -1
  241. package/dist/src/ui/gui/gui-client-settings.js +43 -84
  242. package/dist/src/ui/gui/gui-client-team.js +4 -4
  243. package/dist/src/ui/gui/gui-client.js +7 -1
  244. package/dist/src/ui/progress-data.js +53 -40
  245. package/dist/src/ui/run-progress-model.js +9 -2
  246. package/dist/src/ui/status-model.js +2 -24
  247. package/dist/src/ui/status-payload.js +3 -7
  248. package/dist/src/ui/status-render.js +5 -9
  249. package/dist/src/ui/team-alert-backlog.js +6 -6
  250. package/dist/src/ui/team-alert-status.js +9 -170
  251. package/dist/src/ui/team-alert-tail.js +19 -215
  252. package/dist/src/ui/team-alerts.js +7 -8
  253. package/dist/src/ui/transcript-run.js +27 -16
  254. package/dist/src/ui/tui/tui-editor-answer.js +12 -10
  255. package/dist/src/ui/tui/tui-editor-attach.js +4 -3
  256. package/dist/src/ui/tui/tui-editor-backends.js +4 -3
  257. package/dist/src/ui/tui/tui-editor-brief.js +6 -9
  258. package/dist/src/ui/tui/tui-editor-budget.js +4 -3
  259. package/dist/src/ui/tui/tui-editor-config.js +80 -68
  260. package/dist/src/ui/tui/tui-editor-entry.js +12 -20
  261. package/dist/src/ui/tui/tui-editor-pause.js +51 -57
  262. package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
  263. package/dist/src/ui/tui/tui-editor-queued.js +11 -14
  264. package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
  265. package/dist/src/ui/tui/tui-editor-release.js +4 -3
  266. package/dist/src/ui/tui/tui-editor-retire.js +7 -7
  267. package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
  268. package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
  269. package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
  270. package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
  271. package/dist/src/ui/tui/tui-editor-search.js +4 -3
  272. package/dist/src/ui/tui/tui-editor-session.js +17 -4
  273. package/dist/src/ui/tui/tui-editors.js +1 -7
  274. package/dist/src/ui/tui/tui-frame.js +182 -134
  275. package/dist/src/ui/tui/tui-guard.js +20 -0
  276. package/dist/src/ui/tui/tui-input.js +159 -92
  277. package/dist/src/ui/tui/tui-keymap.js +51 -29
  278. package/dist/src/ui/tui/tui-keys.js +1 -1
  279. package/dist/src/ui/tui/tui-pane-activity.js +3 -3
  280. package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
  281. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  282. package/dist/src/ui/tui/tui-pane-health.js +252 -228
  283. package/dist/src/ui/tui/tui-pane-history.js +61 -70
  284. package/dist/src/ui/tui/tui-pane-pending.js +284 -247
  285. package/dist/src/ui/tui/tui-pane-queued.js +25 -27
  286. package/dist/src/ui/tui/tui-pane-report.js +2 -1
  287. package/dist/src/ui/tui/tui-pane-session.js +11 -7
  288. package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
  289. package/dist/src/ui/tui/tui-pane.js +14 -11
  290. package/dist/src/ui/tui/tui-panes.js +29 -32
  291. package/dist/src/ui/tui/tui.js +3 -4
  292. package/dist/src/verdict/fix-claim.js +76 -54
  293. package/dist/src/verdict/refusal.js +1 -1
  294. package/package.json +5 -5
  295. package/dist/src/concurrency/provider-lease.js +0 -105
  296. package/dist/src/runner/model-fallback.js +0 -76
  297. package/dist/src/runner/runner-fallback.js +0 -66
  298. package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
  299. package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
  300. /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
  301. /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
  302. /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
  303. /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
  304. /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
  305. /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
@@ -1,5 +1,6 @@
1
1
  import { setConfigKey, unsetConfigKey } from "../../config/config-write.js";
2
2
  import { isExponentSpelling } from "../../text/text.js";
3
+ import { savedPromptDraft } from "./tui-editor-session.js";
3
4
  /** Build a team-scope config-key editor. The editor owns its mode flag and saved-draft pair
4
5
  * in a closure; it may take the line only on the Activity view (view 0, the team scope) or
5
6
  * when it already holds it (the toggle-out arm), and only while no other editor holds the
@@ -8,86 +9,97 @@ import { isExponentSpelling } from "../../text/text.js";
8
9
  * (the file is untouched), success restores the saved prompt text. */
9
10
  export function createTeamConfigKeyEditor(session, spec) {
10
11
  let mode = false;
11
- let restoreDraft = () => { };
12
+ const draft = savedPromptDraft(session);
12
13
  // Leave the editor (Esc, the key again, or Ctrl+T): restore the saved prompt text.
13
14
  const exit = () => {
14
15
  if (!mode)
15
16
  return;
16
17
  mode = false;
17
18
  session.held[spec.mode] = false;
18
- restoreDraft();
19
+ draft.restore();
19
20
  };
20
- const handleKey = (_str, key, ctx) => {
21
- if (key.ctrl && key.name === spec.openCtrl) {
22
- // The key opens this team-scope editor only on the Activity view (view 0, the team
23
- // scope): on the other views the key keeps its own meaning and this arm falls through
24
- // so the later guards run. While open, the key toggles the editor out. Entering saves
25
- // the prompt draft; leaving restores it.
26
- if (mode) {
27
- exit();
28
- return true;
29
- }
30
- if (ctx.view !== 0)
31
- return false;
32
- // A check-cap editor with text on the line must fall through to applyKey's kill before the
33
- // hold-guard runs: while another editor holds the (non-empty) line, Ctrl+K should still
34
- // kill from the cursor to the line end rather than be consumed by the mutual-exclusion
35
- // flash, and a typed draft must never be swallowed. Only Ctrl+K sets `requireEmptyPrompt`,
36
- // so only it takes this fall-through.
37
- if (spec.requireEmptyPrompt && session.input !== "")
38
- return false;
39
- if (session.editorEntryDenied(spec.mode))
40
- return true;
41
- const existing = spec.prefill(session);
42
- restoreDraft = session.takeDraft();
43
- mode = true;
44
- session.held[spec.mode] = true;
45
- session.input = existing;
46
- session.cursor = session.input.length;
47
- session.flashMessage(spec.openFlash);
48
- return true;
49
- }
50
- if (key.name === "escape" && mode) {
51
- // Esc cancels the editor, restoring the previous prompt text.
21
+ // The key opens this team-scope editor only on the Activity view (view 0, the team
22
+ // scope): on the other views the key keeps its own meaning and this arm falls through
23
+ // so the later guards run. While open, the key toggles the editor out. Entering saves
24
+ // the prompt draft; leaving restores it.
25
+ const openOrToggle = (ctx) => {
26
+ if (mode) {
52
27
  exit();
53
28
  return true;
54
29
  }
55
- if (key.name === "return" && mode) {
56
- // Enter commits through the shared setter family the config CLI and the dashboard
57
- // Settings use: a whitespace-trimmed empty line clears the key (`unsetConfigKey`),
58
- // otherwise `setConfigKey` writes the trimmed line — it JSON-parses the text, so a
59
- // bare command or typed value stays the literal while a typed number becomes a number
60
- // — and validateConfig owns the value's truth (naming `configKey`) on the way out. An
61
- // invalid value flashes and STAYS in edit mode so the operator can fix it (the file is
62
- // untouched); success restores the saved prompt text.
63
- const text = session.input.trim();
64
- // Shape, before the write, for the plain-integer team keys: an exponent spelling is
65
- // itself a JSON number that setConfigKey's JSON.parse would read past validateConfig
66
- // and silently write, so refuse it here and stay in edit mode (the twin of the per-role
67
- // instances/interval guards); a non-exponent invalid falls through to validateConfig.
68
- if (spec.plainIntegerKey && text !== "" && isExponentSpelling(text)) {
69
- session.flashMessage(`${spec.configKey} must be a plain integer — no exponent notation`);
70
- return true;
71
- }
72
- const result = text === ""
73
- ? unsetConfigKey(session.root, spec.configKey)
74
- : setConfigKey(session.root, spec.configKey, text);
75
- if (result.ok) {
76
- exit();
77
- const noop = text === "" ? result.oldValue === undefined : "unchanged" in result;
78
- session.flashMessage(noop
79
- ? text === ""
80
- ? `${spec.configKey} is not set in tumwater.json, so nothing was cleared`
81
- : `${spec.configKey} is already ${text}, so tumwater.json was not written`
82
- : text === ""
83
- ? spec.clearedFlash
84
- : spec.setFlash(text));
85
- }
86
- else {
87
- session.flashMessage(result.error); // broken config or write failure — stay in edit mode
88
- }
30
+ if (ctx.view !== 0)
31
+ return false;
32
+ // A check-cap editor with text on the line must fall through to applyKey's kill before the
33
+ // hold-guard runs: while another editor holds the (non-empty) line, Ctrl+K should still
34
+ // kill from the cursor to the line end rather than be consumed by the mutual-exclusion
35
+ // flash, and a typed draft must never be swallowed. Only Ctrl+K sets `requireEmptyPrompt`,
36
+ // so only it takes this fall-through.
37
+ if (spec.requireEmptyPrompt && session.input !== "")
38
+ return false;
39
+ if (session.editorEntryDenied(spec.mode))
40
+ return true;
41
+ const existing = spec.prefill(session);
42
+ draft.save();
43
+ mode = true;
44
+ session.held[spec.mode] = true;
45
+ session.input = existing;
46
+ session.cursor = session.input.length;
47
+ session.flashMessage(spec.openFlash);
48
+ return true;
49
+ };
50
+ // Esc cancels the editor, restoring the previous prompt text.
51
+ const cancel = () => {
52
+ exit();
53
+ return true;
54
+ };
55
+ // The four-way message a successful commit flashes: an empty-line no-op clear, a real
56
+ // clear, an unchanged set, and a real set.
57
+ const commitFlash = (result, text) => {
58
+ const noop = text === "" ? result.oldValue === undefined : "unchanged" in result;
59
+ return noop
60
+ ? text === ""
61
+ ? `${spec.configKey} is not set in tumwater.json, so nothing was cleared`
62
+ : `${spec.configKey} is already ${text}, so tumwater.json was not written`
63
+ : text === ""
64
+ ? spec.clearedFlash
65
+ : spec.setFlash(text);
66
+ };
67
+ // Enter commits through the shared setter family the config CLI and the dashboard
68
+ // Settings use: a whitespace-trimmed empty line clears the key (`unsetConfigKey`),
69
+ // otherwise `setConfigKey` writes the trimmed line — it JSON-parses the text, so a
70
+ // bare command or typed value stays the literal while a typed number becomes a number
71
+ // — and validateConfig owns the value's truth (naming `configKey`) on the way out. An
72
+ // invalid value flashes and STAYS in edit mode so the operator can fix it (the file is
73
+ // untouched); success restores the saved prompt text.
74
+ const commit = () => {
75
+ const text = session.input.trim();
76
+ // Shape, before the write, for the plain-integer team keys: an exponent spelling is
77
+ // itself a JSON number that setConfigKey's JSON.parse would read past validateConfig
78
+ // and silently write, so refuse it here and stay in edit mode (the twin of the per-role
79
+ // instances/interval guards); a non-exponent invalid falls through to validateConfig.
80
+ if (spec.plainIntegerKey && text !== "" && isExponentSpelling(text)) {
81
+ session.flashMessage(`${spec.configKey} must be a plain integer — no exponent notation`);
89
82
  return true;
90
83
  }
84
+ const result = text === ""
85
+ ? unsetConfigKey(session.root, spec.configKey)
86
+ : setConfigKey(session.root, spec.configKey, text);
87
+ if (result.ok) {
88
+ exit();
89
+ session.flashMessage(commitFlash(result, text));
90
+ }
91
+ else {
92
+ session.flashMessage(result.error); // broken config or write failure — stay in edit mode
93
+ }
94
+ return true;
95
+ };
96
+ const handleKey = (_str, key, ctx) => {
97
+ if (key.ctrl && key.name === spec.openCtrl)
98
+ return openOrToggle(ctx);
99
+ if (key.name === "escape" && mode)
100
+ return cancel();
101
+ if (key.name === "return" && mode)
102
+ return commit();
91
103
  return false;
92
104
  };
93
105
  return {
@@ -5,7 +5,8 @@ import { submitRolePromptAndWake } from "../../operator/operator-intent.js";
5
5
  import { promptLengthProblem } from "../../inbox/inbox-submit.js";
6
6
  import { collapseWhitespace } from "../../text/text.js";
7
7
  import { selectedEditableEntry } from "./tui-pane-backlog.js";
8
- import { readEditorPath } from "./tui-editor-session.js";
8
+ import { guardError } from "./tui-guard.js";
9
+ import { readEditorPath, savedPromptDraft } from "./tui-editor-session.js";
9
10
  /** The backlog pane's title editor (Ctrl+N new entry, Ctrl+E on a selected plan or bug) on the
10
11
  * shared prompt line. It owns its target (`entryEditFor`), its plan↔bug flip, its saved-draft
11
12
  * pair, its exit helper, and the fileEntryWithBody / tryEditEntry / commit arms, plus the
@@ -13,7 +14,7 @@ import { readEditorPath } from "./tui-editor-session.js";
13
14
  * the session's held registry. */
14
15
  export function createEntryEditor(session) {
15
16
  let entryEditFor = null;
16
- let restoreDraft = () => { };
17
+ const draft = savedPromptDraft(session);
17
18
  // The triple state of the entry editor's prompt line: false (the title field), true (the
18
19
  // file-path field whose contents replace the whole entry), and the draft the toggle parks
19
20
  // when it hands the line to a different field.
@@ -29,7 +30,7 @@ export function createEntryEditor(session) {
29
30
  entryEditFor = null;
30
31
  entryBodyFile = false;
31
32
  session.held.entry = null;
32
- restoreDraft();
33
+ draft.restore();
33
34
  };
34
35
  // On the Backlog view Ctrl+N files a new entry: it opens the title editor with a null
35
36
  // position (new plan), and pressed again inside that editor flips plan <-> bug.
@@ -44,7 +45,7 @@ export function createEntryEditor(session) {
44
45
  // already holding the line) is the notes/change/trail cycle, a pane arm the outer
45
46
  // dispatch handles — this factory returns false so it runs.
46
47
  const openNewEntry = () => {
47
- restoreDraft = session.takeDraft();
48
+ draft.save();
48
49
  entryEditFor = { kind: "plan", position: null, title: "" };
49
50
  session.held.entry = entryEditFor;
50
51
  session.input = "";
@@ -76,22 +77,19 @@ export function createEntryEditor(session) {
76
77
  // saves through tryEditEntry, the non-exiting core the dashboard drawer shares, keeping
77
78
  // the body and stamp. The prompt line is single-line, so only the title is editable here.
78
79
  const openSelectedEntry = (ctx) => {
79
- try {
80
+ guardError(session, () => {
80
81
  const selected = selectedEditableEntry(session, ctx.selectedEntry, "edited");
81
82
  if (selected !== null) {
82
83
  const kind = selected.kind;
83
84
  const position = selected.position;
84
- restoreDraft = session.takeDraft();
85
+ draft.save();
85
86
  entryEditFor = { kind, position, title: selected.picked.title };
86
87
  session.held.entry = entryEditFor;
87
88
  session.input = stripEntryStamp(selected.picked.title);
88
89
  session.cursor = session.input.length;
89
90
  session.flashMessage(`editing ${kind} #${position} — Enter to save, Esc to cancel`);
90
91
  }
91
- }
92
- catch (err) {
93
- session.flashError(err);
94
- }
92
+ });
95
93
  return true;
96
94
  };
97
95
  // Esc cancels the title editor, restoring the previous prompt text; nothing is written.
@@ -107,7 +105,7 @@ export function createEntryEditor(session) {
107
105
  // the two arms pass differently is the (title, body) pair taking the entry's place;
108
106
  // an omitted body preserves the matched entry's own body (the title edit arm).
109
107
  const applyEntryEdit = (target, newTitle, newBody) => {
110
- try {
108
+ guardError(session, () => {
111
109
  // Both call sites (the file arm's and the title arm's Entry-save paths) run only
112
110
  // when `target.position` is non-null, so the position math never sees a null.
113
111
  const position = target.position;
@@ -131,10 +129,7 @@ export function createEntryEditor(session) {
131
129
  session.clearBacklogSelection();
132
130
  session.flashMessage(`that ${target.kind} was already removed or moved — nothing edited`);
133
131
  }
134
- }
135
- catch (err) {
136
- session.flashError(err);
137
- }
132
+ });
138
133
  };
139
134
  // File mode: the line holds a path whose contents replace title and body through the
140
135
  // same title-checked tryEditEntry core the title edit uses, keeping the entry's
@@ -160,7 +155,7 @@ export function createEntryEditor(session) {
160
155
  // New entry (Ctrl+N): file by title with an empty body and wake its role, the same
161
156
  // steps handleBacklogFile takes; a length problem or a throw stays in the editor.
162
157
  const fileNewEntry = (target) => {
163
- try {
158
+ guardError(session, () => {
164
159
  const { role, wakeText } = OPERATOR_FILE_KINDS[target.kind];
165
160
  const problem = promptLengthProblem(wakeText(collapseWhitespace(session.input)), role);
166
161
  if (problem) {
@@ -172,10 +167,7 @@ export function createEntryEditor(session) {
172
167
  submitRolePromptAndWake(session.root, role, wakeText(filed.title));
173
168
  exitEntryEdit();
174
169
  session.flashMessage(`filed ${target.kind}: ${filed.title} — woke ${role}`);
175
- }
176
- catch (err) {
177
- session.flashError(err);
178
- }
170
+ });
179
171
  };
180
172
  const commitEntryTitle = (target) => {
181
173
  applyEntryEdit(target, session.input);
@@ -3,7 +3,9 @@ import { isTeamPaused, pausedRoles, pauseTeam, pauseRole, resumeTeam, resumeRole
3
3
  import { durationLabel } from "../../cli/cli-args.js";
4
4
  import { teamPausePhrase, teamResumePhrase } from "../../text/phrases.js";
5
5
  import { parsePauseInput, parseRolePauseInput, parseRoleWakeInput, parseStopInInput, } from "./tui-input.js";
6
+ import { guardError } from "./tui-guard.js";
6
7
  import { viewedRole } from "./tui-editor-ctx.js";
8
+ import { savedPromptDraft } from "./tui-editor-session.js";
7
9
  /** The pause/stop/wake editor on the shared prompt line: the whole-team Ctrl+F pause and Ctrl+Q
8
10
  * stop editors, the per-role Ctrl+P pause / Ctrl+A abort / Ctrl+W wake controls, and the
9
11
  * parse/dispatch leaves those write. It owns its mode flags (pauseEditMode/`pauseEditRole`/
@@ -14,7 +16,7 @@ export function createPauseEditor(session) {
14
16
  let pauseEditRole = null;
15
17
  let pauseEditWake = false;
16
18
  let pauseEditStop = false;
17
- let restoreDraft = () => { };
19
+ const draft = savedPromptDraft(session);
18
20
  // Leave pause-edit mode (Esc, Ctrl+F again, a written pause, or Ctrl+T): restore the draft.
19
21
  const exitPauseEditMode = () => {
20
22
  if (!pauseEditMode)
@@ -27,10 +29,10 @@ export function createPauseEditor(session) {
27
29
  session.held.pauseEditRole = null;
28
30
  session.held.pauseEditWake = false;
29
31
  session.held.pauseEditStop = false;
30
- restoreDraft();
32
+ draft.restore();
31
33
  };
32
34
  const openPauseEditor = (role, wake, stop, hint) => {
33
- restoreDraft = session.takeDraft();
35
+ draft.save();
34
36
  pauseEditMode = true;
35
37
  pauseEditRole = role;
36
38
  pauseEditWake = wake;
@@ -57,7 +59,7 @@ export function createPauseEditor(session) {
57
59
  exitPauseEditMode();
58
60
  return true;
59
61
  }
60
- try {
62
+ guardError(session, () => {
61
63
  if (isTeamPaused(session.root)) {
62
64
  session.flashMessage(resumeTeam(session.root) ? teamResumePhrase() : "team was not paused");
63
65
  }
@@ -66,10 +68,7 @@ export function createPauseEditor(session) {
66
68
  // until resumed. A paused team resumes at once.
67
69
  openPauseEditor(null, false, false, 'pause the team: [duration] [reason], e.g. "2h deploy freeze" (empty pauses until resumed) — Enter to pause, Esc to cancel');
68
70
  }
69
- }
70
- catch (err) {
71
- session.flashError(err);
72
- }
71
+ });
73
72
  return true;
74
73
  };
75
74
  // Ctrl+Q (mnemonic: **q**uit later) schedules or cancels a graceful team stop through
@@ -81,7 +80,7 @@ export function createPauseEditor(session) {
81
80
  exitPauseEditMode();
82
81
  return true;
83
82
  }
84
- try {
83
+ guardError(session, () => {
85
84
  if (scheduledStopAt(session.root) !== undefined) {
86
85
  session.flashMessage(cancelScheduledStopMessage(session.root));
87
86
  }
@@ -90,10 +89,7 @@ export function createPauseEditor(session) {
90
89
  // stop intent (no role pinned, stop mode on): nothing is written until Enter.
91
90
  openPauseEditor(null, false, true, 'stop the team later: a duration, e.g. "2h" — current runs finish, then the team stops — Enter to schedule, Esc to cancel');
92
91
  }
93
- }
94
- catch (err) {
95
- session.flashError(err);
96
- }
92
+ });
97
93
  return true;
98
94
  };
99
95
  // Ctrl+R on the Activity view applies the pending self-redeploy now — the terminal twin
@@ -106,13 +102,10 @@ export function createPauseEditor(session) {
106
102
  // out of pause-edit mode, whose prompt line the restart must not disturb. The write can
107
103
  // fail like any marker write, so flash the reason instead of letting the throw escape.
108
104
  const restartNow = (_ctx) => {
109
- try {
105
+ guardError(session, () => {
110
106
  const result = requestRestart(session.root);
111
107
  session.flashMessage(result.ok ? result.message : result.error);
112
- }
113
- catch (err) {
114
- session.flashError(err);
115
- }
108
+ });
116
109
  return true;
117
110
  };
118
111
  // A second Ctrl+P or Ctrl+W closes the per-role pause/wake editor, mirroring Ctrl+F's
@@ -132,7 +125,7 @@ export function createPauseEditor(session) {
132
125
  // throw would escape this keypress handler and kill the TUI — flash the reason instead.
133
126
  const roleControlArm = (name, ctx) => {
134
127
  const role = viewedRole(ctx); // defined: view is clamped inside the transcript range
135
- try {
128
+ guardError(session, () => {
136
129
  if (name === "p") {
137
130
  // Toggle by the marker's current state, read fresh: pauseRole/resumeRole's false
138
131
  // return means another window raced us to the same state, and the wording
@@ -163,10 +156,7 @@ export function createPauseEditor(session) {
163
156
  // Ctrl+W opens the one-line wake editor; nothing is written until Enter. Empty wakes now.
164
157
  openPauseEditor(role, true, false, `wake ${role}: [duration], e.g. "2h", or "cancel" (empty wakes now) — Enter to apply, Esc to cancel`);
165
158
  }
166
- }
167
- catch (err) {
168
- session.flashError(err);
169
- }
159
+ });
170
160
  return true;
171
161
  };
172
162
  // Esc cancels the pause editor without writing, restoring the previous prompt text.
@@ -208,43 +198,34 @@ export function createPauseEditor(session) {
208
198
  session.flashMessage(stopParsed.error);
209
199
  return;
210
200
  }
211
- try {
201
+ guardError(session, () => {
212
202
  const result = requestScheduledStop(session.root, stopParsed.inMs);
213
203
  exitPauseEditMode();
214
204
  session.flashMessage(result.ok ? result.message : `error: ${result.error}`);
215
- }
216
- catch (err) {
217
- session.flashError(err);
218
- }
205
+ });
219
206
  };
220
207
  const applyWakeEntry = (wakeParsed, target) => {
221
208
  if (!wakeParsed.ok) {
222
209
  session.flashMessage(wakeParsed.error);
223
210
  return;
224
211
  }
225
- try {
212
+ guardError(session, () => {
226
213
  const message = wakeParsed.cancel ? cancelWakes(session.root, [target]) : requestWake(session.root, [target], wakeParsed.inMs);
227
214
  exitPauseEditMode();
228
215
  session.flashMessage(message);
229
- }
230
- catch (err) {
231
- session.flashError(err);
232
- }
216
+ });
233
217
  };
234
218
  const applyRolePauseEntry = (parsed, target) => {
235
- try {
219
+ guardError(session, () => {
236
220
  const at = session.now();
237
221
  const until = parsed.forMs === undefined ? undefined : at + parsed.forMs;
238
222
  const changed = pauseRole(session.root, target, until);
239
223
  exitPauseEditMode();
240
224
  session.flashMessage(rolePauseMessage(session.root, target, changed, until === undefined ? undefined : { ms: parsed.forMs, untilMs: until }, at));
241
- }
242
- catch (err) {
243
- session.flashError(err);
244
- }
225
+ });
245
226
  };
246
227
  const applyTeamPauseEntry = (parsed) => {
247
- try {
228
+ guardError(session, () => {
248
229
  const until = parsed.forMs === undefined ? undefined : session.now() + parsed.forMs;
249
230
  const changed = pauseTeam(session.root, until, parsed.reason);
250
231
  exitPauseEditMode();
@@ -252,31 +233,44 @@ export function createPauseEditor(session) {
252
233
  session.flashMessage(changed || until !== undefined
253
234
  ? `${teamPausePhrase()}${forSuffix}${changed && parsed.reason ? `: ${parsed.reason}` : ""}`
254
235
  : "team is already paused");
236
+ });
237
+ };
238
+ const pauseLineFree = () => !session.anyEditorHeld("pause");
239
+ const restartAllowed = (ctx) => ctx.view === 0 && !pauseEditMode && pauseLineFree();
240
+ const roleViewEditable = (ctx) => !session.anyEditorHeld("rolePromptFor", "search") &&
241
+ ctx.roleIds.length > 0 &&
242
+ ctx.view >= 1 && ctx.view <= ctx.roleIds.length;
243
+ const ctrlRoutes = [
244
+ { name: "f", should: pauseLineFree, run: teamPauseToggle },
245
+ { name: "q", should: pauseLineFree, run: stopLaterToggle },
246
+ { name: "r", should: restartAllowed, run: restartNow },
247
+ { name: "p", should: () => pauseEditRole !== null, run: closeRolePauseEdit },
248
+ { name: "w", should: () => pauseEditRole !== null, run: closeRolePauseEdit },
249
+ { name: "p", should: roleViewEditable, run: (ctx) => roleControlArm("p", ctx) },
250
+ { name: "a", should: roleViewEditable, run: (ctx) => roleControlArm("a", ctx) },
251
+ { name: "w", should: roleViewEditable, run: (ctx) => roleControlArm("w", ctx) },
252
+ { name: "v", should: roleViewEditable, run: (ctx) => roleControlArm("v", ctx) },
253
+ ];
254
+ const routeCtrl = (key, ctx) => {
255
+ for (const route of ctrlRoutes) {
256
+ if (route.name === key.name && route.should(ctx))
257
+ return route.run(ctx);
255
258
  }
256
- catch (err) {
257
- session.flashError(err);
258
- }
259
+ return null;
259
260
  };
260
- const handleKey = (_str, key, ctx) => {
261
- // Ordered dispatch: a guard miss falls through to the next arm, then to applyKey.
262
- if (key.ctrl && key.name === "f" && !session.anyEditorHeld("pause"))
263
- return teamPauseToggle(ctx);
264
- if (key.ctrl && key.name === "q" && !session.anyEditorHeld("pause"))
265
- return stopLaterToggle(ctx);
266
- if (key.ctrl && key.name === "r" && ctx.view === 0 && !pauseEditMode && !session.anyEditorHeld("pause"))
267
- return restartNow(ctx);
268
- if (key.ctrl && (key.name === "p" || key.name === "w") && pauseEditRole !== null)
269
- return closeRolePauseEdit();
270
- if (!session.anyEditorHeld("rolePromptFor", "search") &&
271
- ctx.roleIds.length > 0 &&
272
- ctx.view >= 1 && ctx.view <= ctx.roleIds.length &&
273
- key.ctrl && (key.name === "p" || key.name === "a" || key.name === "w" || key.name === "v"))
274
- return roleControlArm(key.name, ctx);
261
+ const editEntryArm = (key) => {
275
262
  if (key.name === "escape" && pauseEditMode)
276
263
  return cancelPauseEdit();
277
264
  if (key.name === "return" && pauseEditMode)
278
265
  return commitPauseEntry();
279
266
  return false;
280
267
  };
268
+ const handleKey = (_str, key, ctx) => {
269
+ // Ordered dispatch: a guard miss falls through to the next route, then to editEntryArm.
270
+ const ctrl = key.ctrl ? routeCtrl(key, ctx) : null;
271
+ if (ctrl !== null)
272
+ return ctrl;
273
+ return editEntryArm(key);
274
+ };
281
275
  return { handleKey, exit: exitPauseEditMode };
282
276
  }
@@ -1,14 +1,16 @@
1
1
  import { submitRolePromptAndWake } from "../../operator/operator-intent.js";
2
2
  import { pushPromptHistory } from "./tui-prompt-history.js";
3
3
  import { parseRolePromptInput } from "./tui-input.js";
4
+ import { guardError } from "./tui-guard.js";
4
5
  import { viewedRole } from "./tui-editor-ctx.js";
6
+ import { savedPromptDraft } from "./tui-editor-session.js";
5
7
  /** The per-role prompt Ctrl+R editor on the shared prompt line. It owns its mode flag (the
6
8
  * viewed role's role id), its saved-draft pair, its exit helper, and the handleKey arms for its
7
9
  * key: the per-role toggle (guarded out of the other editors' prompt line) and its Esc/Enter
8
10
  * leaves. Mutually exclusive with the other editors via the session's held registry. */
9
11
  export function createPromptEditor(session) {
10
12
  let rolePromptFor = null;
11
- let restoreDraft = () => { };
13
+ const draft = savedPromptDraft(session);
12
14
  // Leave role-prompt mode (Esc, Ctrl+R again, or Ctrl+T): restore the saved prompt text,
13
15
  // and reset the recall state with it — the saved draft was saved settled (see the Ctrl+R
14
16
  // handler), so resuming a browse entered in role mode would restore role-mode text onto
@@ -18,7 +20,7 @@ export function createPromptEditor(session) {
18
20
  return;
19
21
  rolePromptFor = null;
20
22
  session.held.rolePromptFor = null;
21
- restoreDraft();
23
+ draft.restore();
22
24
  };
23
25
  // Ctrl+R toggles role-prompt mode for the viewed role (mnemonic: p**R**ompt — Ctrl+P is
24
26
  // taken by pause): entering saves the director draft in this mode's own pair and blanks
@@ -30,7 +32,7 @@ export function createPromptEditor(session) {
30
32
  ctx.roleIds.length > 0 &&
31
33
  ctx.view >= 1 && ctx.view <= ctx.roleIds.length) {
32
34
  const role = viewedRole(ctx); // defined: view is clamped inside the transcript range
33
- try {
35
+ guardError(session, () => {
34
36
  if (rolePromptFor) {
35
37
  exitRolePromptMode();
36
38
  session.flashMessage("role prompt cancelled");
@@ -40,17 +42,14 @@ export function createPromptEditor(session) {
40
42
  // recall state: settle first, so the role editor saves (and later restores) the
41
43
  // draft the operator was writing, and no stale index/draft crosses modes —
42
44
  // otherwise Esc here would surface role-mode recalls on the director line.
43
- restoreDraft = session.takeDraft();
45
+ draft.save();
44
46
  rolePromptFor = role;
45
47
  session.held.rolePromptFor = role;
46
48
  session.input = "";
47
49
  session.cursor = 0;
48
50
  session.flashMessage(`prompt for ${role}: Enter to send, Esc to cancel`);
49
51
  }
50
- }
51
- catch (err) {
52
- session.flashError(err);
53
- }
52
+ });
54
53
  return true;
55
54
  }
56
55
  return false;
@@ -76,15 +75,12 @@ export function createPromptEditor(session) {
76
75
  session.flashMessage(parsed.error);
77
76
  }
78
77
  else {
79
- try {
78
+ guardError(session, () => {
80
79
  submitRolePromptAndWake(session.root, role, parsed.value, undefined);
81
80
  session.promptHistory = pushPromptHistory(session.promptHistory, parsed.value);
82
81
  exitRolePromptMode();
83
82
  session.flashMessage(`added to ${role}'s inbox`);
84
- }
85
- catch (err) {
86
- session.flashError(err);
87
- }
83
+ });
88
84
  }
89
85
  return true;
90
86
  };
@@ -1,12 +1,14 @@
1
1
  import { editQueuedFile, readQueuedPromptBody } from "../../inbox/inbox-edit.js";
2
2
  import { parseRolePromptInput } from "./tui-input.js";
3
+ import { guardError } from "./tui-guard.js";
4
+ import { savedPromptDraft } from "./tui-editor-session.js";
3
5
  /** The Queued pane's in-place row editor (Ctrl+E) on the shared prompt line. It owns its target
4
6
  * (`queuedEditFor`), its saved-draft pair, its exit helper, and the readQueuedPromptBody /
5
7
  * editQueuedFile leaves that pre-fill the line and write it back. Mutually exclusive with the
6
8
  * other editors via the session's held registry. */
7
9
  export function createQueuedEditor(session) {
8
10
  let queuedEditFor = null;
9
- let restoreDraft = () => { };
11
+ const draft = savedPromptDraft(session);
10
12
  // Leave the Queued pane's in-place editor (Esc, a saved edit, or Ctrl+T): restore the saved
11
13
  // prompt text, and reset the recall state with it — the same settle contract the other
12
14
  // editors honor.
@@ -15,7 +17,7 @@ export function createQueuedEditor(session) {
15
17
  return;
16
18
  queuedEditFor = null;
17
19
  session.held.queued = null;
18
- restoreDraft();
20
+ draft.restore();
19
21
  };
20
22
  // Ctrl+E opens the Queued pane's in-place editor on the selected row (mnemonic:
21
23
  // **E**dit) through the same readQueuedPromptBody/editQueuedFile pair the dashboard's
@@ -31,8 +33,9 @@ export function createQueuedEditor(session) {
31
33
  ctx.selectedQueued === null) {
32
34
  return false;
33
35
  }
34
- try {
35
- const position = Math.min(ctx.selectedQueued, ctx.queuedRows.length - 1);
36
+ const selected = ctx.selectedQueued; // narrowed to number by the guard above
37
+ guardError(session, () => {
38
+ const position = Math.min(selected, ctx.queuedRows.length - 1);
36
39
  const row = ctx.queuedRows[position];
37
40
  if (row !== undefined) {
38
41
  const body = readQueuedPromptBody(session.root, row.role, row.file);
@@ -44,7 +47,7 @@ export function createQueuedEditor(session) {
44
47
  // Mid-recall, the line holds a history entry while the real draft sits in the
45
48
  // recall state: settle first, so the queued editor saves (and later restores)
46
49
  // the draft the operator was writing.
47
- restoreDraft = session.takeDraft();
50
+ draft.save();
48
51
  queuedEditFor = { role: row.role, file: row.file };
49
52
  session.held.queued = queuedEditFor;
50
53
  session.input = body;
@@ -52,10 +55,7 @@ export function createQueuedEditor(session) {
52
55
  session.flashMessage(`editing inbox message #${row.position} for ${row.role} — Enter to save, Esc to cancel`);
53
56
  }
54
57
  }
55
- }
56
- catch (err) {
57
- session.flashError(err);
58
- }
58
+ });
59
59
  return true;
60
60
  };
61
61
  // Esc cancels the Queued pane's in-place editor, restoring the previous prompt text.
@@ -77,17 +77,14 @@ export function createQueuedEditor(session) {
77
77
  else {
78
78
  // The dispatch guards on `queuedEditFor`, so it is never null when this arm runs.
79
79
  const target = queuedEditFor;
80
- try {
80
+ guardError(session, () => {
81
81
  const outcome = editQueuedFile(session.root, target.role, target.file, parsed.value);
82
82
  exitQueuedEdit();
83
83
  session.refreshQueued();
84
84
  session.flashMessage(outcome.status === "edited"
85
85
  ? `edited inbox message for ${target.role}`
86
86
  : "the inbox message was already gone");
87
- }
88
- catch (err) {
89
- session.flashError(err);
90
- }
87
+ });
91
88
  }
92
89
  return true;
93
90
  };