tumwater 0.1.2 → 0.2.0

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