tumwater 0.2.0 → 0.3.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 (446) hide show
  1. package/README.md +27 -13
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-args.js +8 -9
  4. package/dist/src/agent/agent-backend.js +8 -6
  5. package/dist/src/agent/backend-traits.js +42 -25
  6. package/dist/src/agent/claude/claude-account.js +71 -204
  7. package/dist/src/agent/claude/claude-args.js +9 -6
  8. package/dist/src/agent/claude/claude-backend.js +10 -10
  9. package/dist/src/agent/claude/claude-env.js +1 -1
  10. package/dist/src/agent/claude/claude-stream.js +4 -3
  11. package/dist/src/agent/claude/claude-write-guard.js +49 -0
  12. package/dist/src/agent/claude/write-guard-hook.js +13 -0
  13. package/dist/src/agent/codex/codex-account.js +119 -0
  14. package/dist/src/agent/codex/codex-args.js +181 -0
  15. package/dist/src/agent/codex/codex-backend.js +279 -0
  16. package/dist/src/agent/codex/codex-catalog.js +97 -0
  17. package/dist/src/agent/codex/codex-env.js +18 -0
  18. package/dist/src/agent/codex/codex-hook-trust.js +246 -0
  19. package/dist/src/agent/codex/codex-patch-paths.js +36 -0
  20. package/dist/src/agent/codex/codex-rollout.js +179 -0
  21. package/dist/src/agent/codex/codex-stream.js +402 -0
  22. package/dist/src/agent/codex/codex-write-guard.js +86 -0
  23. package/dist/src/agent/codex/write-guard-hook.js +12 -0
  24. package/dist/src/agent/image-input.js +29 -24
  25. package/dist/src/agent/notes-shim.js +11 -12
  26. package/dist/src/agent/opencode/opencode-args.js +0 -1
  27. package/dist/src/agent/opencode/opencode-backend.js +12 -9
  28. package/dist/src/agent/opencode/opencode-catalog.js +55 -29
  29. package/dist/src/agent/opencode/opencode-config.js +19 -14
  30. package/dist/src/agent/opencode/opencode-env.js +0 -1
  31. package/dist/src/agent/opencode/opencode-session-id.js +1 -2
  32. package/dist/src/agent/opencode/opencode-stream.js +1 -1
  33. package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
  34. package/dist/src/agent/role-notes-write.js +5 -6
  35. package/dist/src/agent/run-agent.js +465 -448
  36. package/dist/src/agent/run-settle.js +214 -0
  37. package/dist/src/agent/session-ledger.js +86 -16
  38. package/dist/src/agent/subscription-account.js +220 -0
  39. package/dist/src/agent/write-guard.js +148 -0
  40. package/dist/src/backlog/backlog-archive.js +246 -55
  41. package/dist/src/backlog/backlog-cli-view.js +280 -0
  42. package/dist/src/backlog/backlog-cli.js +85 -274
  43. package/dist/src/backlog/backlog-commit.js +4 -5
  44. package/dist/src/backlog/backlog-eligibility.js +175 -242
  45. package/dist/src/backlog/backlog-index.js +149 -0
  46. package/dist/src/backlog/backlog-md.js +54 -6
  47. package/dist/src/backlog/backlog-parts.js +254 -0
  48. package/dist/src/backlog/backlog-release.js +117 -0
  49. package/dist/src/backlog/backlog-render.js +27 -0
  50. package/dist/src/backlog/backlog-structure.js +99 -156
  51. package/dist/src/backlog/backlog-write.js +40 -127
  52. package/dist/src/backlog/backlog.js +21 -6
  53. package/dist/src/backlog/question-answer.js +21 -15
  54. package/dist/src/baseline/main-baseline.js +89 -20
  55. package/dist/src/baseline/main-culprit.js +94 -0
  56. package/dist/src/baseline/main-guardrails.js +209 -0
  57. package/dist/src/baseline/main-red.js +139 -32
  58. package/dist/src/baseline/main-revert.js +133 -0
  59. package/dist/src/baseline/main-watch.js +184 -0
  60. package/dist/src/brief/brief-edit.js +9 -11
  61. package/dist/src/brief/brief.js +31 -51
  62. package/dist/src/budget/budget.js +30 -20
  63. package/dist/src/budget/fallback-breaker.js +26 -27
  64. package/dist/src/build/build-check-detect.js +23 -15
  65. package/dist/src/build/build-check-events.js +25 -6
  66. package/dist/src/build/build-check-report.js +49 -9
  67. package/dist/src/build/build-check-scoped.js +18 -11
  68. package/dist/src/build/build-check.js +2 -1
  69. package/dist/src/build/build-info.js +2 -3
  70. package/dist/src/build/build-stage.js +9 -14
  71. package/dist/src/build/dep-install.js +1 -2
  72. package/dist/src/change/change-data.js +123 -31
  73. package/dist/src/change/change-render.js +46 -5
  74. package/dist/src/cli/brief-command.js +3 -4
  75. package/dist/src/cli/cli-args.js +118 -33
  76. package/dist/src/cli/cli-command-args.js +46 -18
  77. package/dist/src/cli/cli-editor.js +2 -1
  78. package/dist/src/cli/cli-flag-specs.js +150 -45
  79. package/dist/src/cli/cli-init.js +51 -0
  80. package/dist/src/cli/cli-marker-commands.js +4 -4
  81. package/dist/src/cli/cli-query-commands.js +308 -590
  82. package/dist/src/cli/cli-repo-gate.js +11 -1
  83. package/dist/src/cli/cli-role-command.js +420 -0
  84. package/dist/src/cli/cli-start-summary.js +160 -0
  85. package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
  86. package/dist/src/cli/completion.js +149 -22
  87. package/dist/src/cli/config-commands.js +41 -18
  88. package/dist/src/cli/follow-view.js +75 -0
  89. package/dist/src/cli/help.js +189 -73
  90. package/dist/src/cli/log-commands.js +148 -82
  91. package/dist/src/cli/question-commands.js +205 -10
  92. package/dist/src/cli.js +106 -43
  93. package/dist/src/collections.js +26 -10
  94. package/dist/src/concurrency/check-permit.js +20 -3
  95. package/dist/src/concurrency/lock.js +17 -13
  96. package/dist/src/concurrency/provider-lease.js +44 -8
  97. package/dist/src/concurrency/semaphore.js +102 -26
  98. package/dist/src/config/config-aliases.js +0 -1
  99. package/dist/src/config/config-editable-keys.js +13 -1
  100. package/dist/src/config/config-fallbacks.js +93 -56
  101. package/dist/src/config/config-field-checks.js +40 -10
  102. package/dist/src/config/config-keys.js +30 -8
  103. package/dist/src/config/config-live.js +63 -24
  104. package/dist/src/config/config-request.js +137 -0
  105. package/dist/src/config/config-schema.js +31 -24
  106. package/dist/src/config/config-selector-checks.js +123 -0
  107. package/dist/src/config/config-validation-agents.js +80 -0
  108. package/dist/src/config/config-validation-model.js +116 -0
  109. package/dist/src/config/config-validation.js +141 -199
  110. package/dist/src/config/config-views.js +127 -53
  111. package/dist/src/config/config-write-core.js +135 -0
  112. package/dist/src/config/config-write.js +131 -264
  113. package/dist/src/config/config.js +48 -38
  114. package/dist/src/config/model-selector.js +34 -34
  115. package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
  116. package/dist/src/doctor/doctor-backlog-plans.js +263 -0
  117. package/dist/src/doctor/doctor-backlog.js +100 -496
  118. package/dist/src/doctor/doctor-check-names.js +81 -0
  119. package/dist/src/doctor/doctor-checks.js +52 -579
  120. package/dist/src/doctor/doctor-claude-checks.js +145 -0
  121. package/dist/src/doctor/doctor-codex-checks.js +279 -0
  122. package/dist/src/doctor/doctor-contention-checks.js +380 -0
  123. package/dist/src/doctor/doctor-holds.js +82 -66
  124. package/dist/src/doctor/doctor-install.js +2 -1
  125. package/dist/src/doctor/doctor-model-checks.js +236 -402
  126. package/dist/src/doctor/doctor-notify.js +3 -4
  127. package/dist/src/doctor/doctor-opencode-checks.js +162 -0
  128. package/dist/src/doctor/doctor-orphans.js +182 -63
  129. package/dist/src/doctor/doctor-project-checks.js +303 -0
  130. package/dist/src/doctor/doctor.js +121 -77
  131. package/dist/src/doctor/image-probe.js +21 -8
  132. package/dist/src/doctor/outcome.js +3 -2
  133. package/dist/src/doctor/probe.js +7 -11
  134. package/dist/src/events/event-format.js +181 -86
  135. package/dist/src/events/event-labels.js +12 -5
  136. package/dist/src/events/event-legacy.js +39 -15
  137. package/dist/src/events/event-read.js +39 -39
  138. package/dist/src/events/event-tally.js +16 -0
  139. package/dist/src/events/event-window.js +60 -24
  140. package/dist/src/events/events.js +30 -13
  141. package/dist/src/events/notify.js +7 -7
  142. package/dist/src/failure/failure-cluster.js +12 -12
  143. package/dist/src/failure/failure-data.js +56 -56
  144. package/dist/src/failure/failure-render.js +31 -23
  145. package/dist/src/failure/failure-state-change.js +17 -12
  146. package/dist/src/failure/time-spend.js +99 -35
  147. package/dist/src/files/files.js +111 -35
  148. package/dist/src/files/json-files.js +3 -3
  149. package/dist/src/files/json-object.js +28 -7
  150. package/dist/src/files/stat-cache.js +2 -2
  151. package/dist/src/files/tail.js +1 -1
  152. package/dist/src/gates/bootstrap-gates.js +4 -5
  153. package/dist/src/gates/budget-gates.js +177 -84
  154. package/dist/src/gates/disk-gate.js +1 -1
  155. package/dist/src/gates/gate-polls.js +108 -58
  156. package/dist/src/gates/maintenance-quota.js +10 -13
  157. package/dist/src/gates/pause-gates.js +28 -10
  158. package/dist/src/gates/readiness.js +7 -3
  159. package/dist/src/gates/role-cap-gates.js +8 -8
  160. package/dist/src/gates/startup-gate.js +94 -32
  161. package/dist/src/gates/streak-gate.js +54 -30
  162. package/dist/src/git/commit-message.js +77 -46
  163. package/dist/src/git/git-diff.js +16 -10
  164. package/dist/src/git/git-run.js +79 -38
  165. package/dist/src/git/git.js +36 -34
  166. package/dist/src/git/slot-salvage.js +253 -0
  167. package/dist/src/git/slots-state.js +6 -6
  168. package/dist/src/git/worktree-pool.js +132 -38
  169. package/dist/src/git/worktree-use.js +4 -11
  170. package/dist/src/git/worktree.js +94 -37
  171. package/dist/src/gui/gui-admin-endpoints.js +96 -0
  172. package/dist/src/gui/gui-args.js +79 -8
  173. package/dist/src/gui/gui-backlog-commands.js +42 -18
  174. package/dist/src/gui/gui-backlog-endpoints.js +155 -0
  175. package/dist/src/gui/gui-command.js +8 -13
  176. package/dist/src/gui/gui-config-commands.js +94 -72
  177. package/dist/src/gui/gui-endpoint-commands.js +4 -3
  178. package/dist/src/gui/gui-endpoint-common.js +4 -4
  179. package/dist/src/gui/gui-endpoints.js +11 -380
  180. package/dist/src/gui/gui-history-endpoints.js +301 -0
  181. package/dist/src/gui/gui-operator-commands.js +108 -13
  182. package/dist/src/gui/gui-prompt-commands.js +11 -14
  183. package/dist/src/gui/gui-report-endpoints.js +90 -0
  184. package/dist/src/gui/gui-server.js +11 -4
  185. package/dist/src/gui/gui-team-commands.js +7 -7
  186. package/dist/src/gui/http-body.js +1 -1
  187. package/dist/src/history/history-data.js +122 -234
  188. package/dist/src/history/history-queued.js +147 -0
  189. package/dist/src/history/history-render.js +42 -22
  190. package/dist/src/history/history.js +193 -49
  191. package/dist/src/inbox/inbox-attachments.js +29 -30
  192. package/dist/src/inbox/inbox-cancel.js +10 -11
  193. package/dist/src/inbox/inbox-edit.js +9 -9
  194. package/dist/src/inbox/inbox-submit.js +18 -18
  195. package/dist/src/inbox/inbox-top.js +25 -9
  196. package/dist/src/inbox/inbox.js +42 -43
  197. package/dist/src/inbox/pending-prompt.js +26 -26
  198. package/dist/src/inbox/prompt-commands.js +62 -49
  199. package/dist/src/inbox/prompt-not-before.js +2 -2
  200. package/dist/src/init/init-templates.js +1 -2
  201. package/dist/src/init/init.js +28 -23
  202. package/dist/src/merge/backlog-conflicts.js +215 -35
  203. package/dist/src/merge/merge-batch.js +14 -8
  204. package/dist/src/merge/merge-cancel.js +53 -0
  205. package/dist/src/merge/merge-check-failures.js +44 -22
  206. package/dist/src/merge/merge-core.js +81 -32
  207. package/dist/src/merge/merge-diff.js +5 -4
  208. package/dist/src/merge/merge-drain.js +150 -11
  209. package/dist/src/merge/merge-git.js +177 -38
  210. package/dist/src/merge/merge-pipeline.js +15 -3
  211. package/dist/src/merge/merge-post-review.js +90 -0
  212. package/dist/src/merge/merge-queue-render.js +49 -11
  213. package/dist/src/merge/merge-queue.js +31 -15
  214. package/dist/src/merge/merge-requeue.js +102 -0
  215. package/dist/src/merge/merge-slot.js +61 -16
  216. package/dist/src/merge/merge-stack.js +33 -11
  217. package/dist/src/merge/merge-to-main.js +86 -27
  218. package/dist/src/merge/merge-top.js +69 -0
  219. package/dist/src/merge/merge-verify.js +12 -10
  220. package/dist/src/notes-cli.js +30 -15
  221. package/dist/src/operator/operator-commands.js +192 -81
  222. package/dist/src/operator/operator-intent.js +69 -42
  223. package/dist/src/operator/operator-requests.js +35 -40
  224. package/dist/src/operator/retire.js +5 -6
  225. package/dist/src/operator/revert.js +142 -34
  226. package/dist/src/orchestrator/orchestrator-launch.js +60 -52
  227. package/dist/src/orchestrator/orchestrator-poll.js +320 -0
  228. package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
  229. package/dist/src/orchestrator/orchestrator.js +128 -369
  230. package/dist/src/orchestrator/retention.js +42 -8
  231. package/dist/src/orchestrator/scheduling-admit.js +116 -0
  232. package/dist/src/orchestrator/scheduling-scope.js +201 -0
  233. package/dist/src/orchestrator/scheduling-skip.js +185 -0
  234. package/dist/src/orchestrator/startup-migration-steps.js +8 -7
  235. package/dist/src/orchestrator/startup-migrations.js +11 -7
  236. package/dist/src/paths.js +116 -49
  237. package/dist/src/pi/command-shape.js +12 -1
  238. package/dist/src/pi/pi-args.js +6 -4
  239. package/dist/src/pi/pi-backend.js +6 -6
  240. package/dist/src/pi/pi-bin.js +54 -63
  241. package/dist/src/pi/pi-listed-models.js +53 -0
  242. package/dist/src/pi/pi-models.js +63 -30
  243. package/dist/src/pi/pi-run-result.js +1 -1
  244. package/dist/src/pi/pi-settings.js +1 -1
  245. package/dist/src/pi/pi-stream.js +6 -6
  246. package/dist/src/pi/pi-watchdogs.js +18 -19
  247. package/dist/src/pi-extension/bounded-output.js +5 -3
  248. package/dist/src/pi-extension/context-budget.js +2 -2
  249. package/dist/src/pi-extension/context-shake.js +1 -1
  250. package/dist/src/pi-extension/context-window.js +0 -1
  251. package/dist/src/pi-extension/full-output.js +3 -10
  252. package/dist/src/pi-extension/strip-thinking.js +0 -1
  253. package/dist/src/pi-extension/trim-system-prompt.js +3 -4
  254. package/dist/src/pi-extension/write-guard.js +37 -0
  255. package/dist/src/process/launch-services.js +1 -2
  256. package/dist/src/process/node-bin.js +16 -0
  257. package/dist/src/process/process-group.js +8 -18
  258. package/dist/src/process/process.js +41 -2
  259. package/dist/src/process/run-marker.js +18 -6
  260. package/dist/src/process/supervisor.js +22 -21
  261. package/dist/src/prompt/dialect.js +17 -2
  262. package/dist/src/prompt/gate-notes.js +203 -0
  263. package/dist/src/prompt/gate-prompts.js +121 -211
  264. package/dist/src/prompt/notes-backlog.js +181 -0
  265. package/dist/src/prompt/principles.js +1 -2
  266. package/dist/src/prompt/prompt-followup.js +19 -14
  267. package/dist/src/prompt/prompt.js +129 -204
  268. package/dist/src/redeploy/redeploy-policy.js +24 -19
  269. package/dist/src/redeploy/redeploy-probes.js +1 -1
  270. package/dist/src/redeploy/redeploy.js +2 -3
  271. package/dist/src/redeploy/redeployer.js +20 -20
  272. package/dist/src/redeploy/self-reload.js +2 -1
  273. package/dist/src/report/report-data.js +161 -77
  274. package/dist/src/report/report-fold.js +129 -52
  275. package/dist/src/report/report-main-red.js +55 -0
  276. package/dist/src/report/report-render.js +133 -64
  277. package/dist/src/report/report-types.js +1 -0
  278. package/dist/src/report/report.js +26 -9
  279. package/dist/src/request-timeouts.js +2 -2
  280. package/dist/src/review/known-flakes.js +100 -1
  281. package/dist/src/review/post-merge-review.js +106 -0
  282. package/dist/src/review/protected-edits.js +4 -5
  283. package/dist/src/review/review-followup.js +8 -8
  284. package/dist/src/review/review-precheck.js +66 -23
  285. package/dist/src/review/review-run.js +395 -0
  286. package/dist/src/review/review-verdict.js +2 -2
  287. package/dist/src/review/review.js +155 -360
  288. package/dist/src/review/secret-leaks.js +36 -16
  289. package/dist/src/review/suite-rerun.js +0 -1
  290. package/dist/src/roles/legacy-role-migration.js +80 -7
  291. package/dist/src/roles/role-catalog.js +69 -12
  292. package/dist/src/roles/role-guidance.js +55 -20
  293. package/dist/src/roles/role-labels.js +1 -2
  294. package/dist/src/roles/role-render.js +67 -50
  295. package/dist/src/roles/role-view.js +59 -47
  296. package/dist/src/roles/roles.js +31 -31
  297. package/dist/src/roles/runner-ids.js +97 -0
  298. package/dist/src/run/acceptance-greps.js +119 -0
  299. package/dist/src/run/feedback-filings.js +51 -0
  300. package/dist/src/{tick → run}/qa-coverage.js +9 -27
  301. package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
  302. package/dist/src/run/run-detail-data.js +179 -0
  303. package/dist/src/run/run-detail-render.js +98 -0
  304. package/dist/src/run/run-detail.js +168 -0
  305. package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
  306. package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
  307. package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
  308. package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
  309. package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
  310. package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
  311. package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
  312. package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
  313. package/dist/src/run/selfcheck-digest.js +40 -0
  314. package/dist/src/run/stage-backlog-checks.js +249 -0
  315. package/dist/src/run/stage-check.js +172 -0
  316. package/dist/src/run/stage-path-checks.js +730 -0
  317. package/dist/src/{loop → runner}/leftover.js +80 -25
  318. package/dist/src/{loop → runner}/model-fallback.js +12 -12
  319. package/dist/src/runner/revision.js +202 -0
  320. package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
  321. package/dist/src/runner/runner-outcome.js +107 -0
  322. package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
  323. package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
  324. package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
  325. package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
  326. package/dist/src/runner/runner-state.js +102 -0
  327. package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
  328. package/dist/src/scheduling/backoff.js +38 -38
  329. package/dist/src/scheduling/claims.js +35 -22
  330. package/dist/src/scheduling/once-round.js +22 -22
  331. package/dist/src/scheduling/quiet-hours.js +5 -6
  332. package/dist/src/scheduling/scheduling.js +74 -64
  333. package/dist/src/scheduling/work-merged-cache.js +2 -2
  334. package/dist/src/status/status-budget.js +55 -47
  335. package/dist/src/status/status-data.js +43 -16
  336. package/dist/src/status/status-inbox.js +2 -2
  337. package/dist/src/status/status-install.js +0 -1
  338. package/dist/src/status/status-polls.js +28 -28
  339. package/dist/src/status/status-roles.js +39 -13
  340. package/dist/src/team/error-storm.js +8 -8
  341. package/dist/src/team/failure-spread.js +2 -2
  342. package/dist/src/team/orchestrator-info.js +15 -9
  343. package/dist/src/team/reclaim.js +5 -6
  344. package/dist/src/team/team-hold.js +7 -7
  345. package/dist/src/team/team-polls.js +91 -50
  346. package/dist/src/team/team-state.js +8 -2
  347. package/dist/src/text/datetime.js +63 -17
  348. package/dist/src/text/format.js +48 -11
  349. package/dist/src/text/markdown.js +8 -5
  350. package/dist/src/text/phrases.js +57 -23
  351. package/dist/src/text/suggest.js +1 -1
  352. package/dist/src/text/text.js +67 -13
  353. package/dist/src/ui/badges.js +65 -37
  354. package/dist/src/ui/gui/gui-client-backlog.js +292 -0
  355. package/dist/src/ui/gui/gui-client-boot.js +32 -26
  356. package/dist/src/ui/gui/gui-client-composer.js +8 -9
  357. package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
  358. package/dist/src/ui/gui/gui-client-drawer.js +126 -285
  359. package/dist/src/ui/gui/gui-client-feed.js +271 -0
  360. package/dist/src/ui/gui/gui-client-history.js +434 -67
  361. package/dist/src/ui/gui/gui-client-labels.js +1 -2
  362. package/dist/src/ui/gui/gui-client-markdown.js +1 -2
  363. package/dist/src/ui/gui/gui-client-model.js +27 -36
  364. package/dist/src/ui/gui/gui-client-operator.js +55 -51
  365. package/dist/src/ui/gui/gui-client-pending.js +227 -23
  366. package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
  367. package/dist/src/ui/gui/gui-client-report.js +87 -427
  368. package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
  369. package/dist/src/ui/gui/gui-client-settings.js +56 -4
  370. package/dist/src/ui/gui/gui-client-team.js +25 -465
  371. package/dist/src/ui/gui/gui-client.js +27 -25
  372. package/dist/src/ui/gui/gui-icons.js +0 -2
  373. package/dist/src/ui/gui/gui-page.js +9 -7
  374. package/dist/src/ui/gui/gui-styles.js +34 -34
  375. package/dist/src/ui/progress-data.js +9 -11
  376. package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
  377. package/dist/src/ui/status-model.js +133 -125
  378. package/dist/src/ui/status-payload.js +136 -49
  379. package/dist/src/ui/status-render.js +101 -82
  380. package/dist/src/ui/team-alerts.js +248 -57
  381. package/dist/src/ui/tone.js +5 -6
  382. package/dist/src/ui/transcript-run.js +58 -0
  383. package/dist/src/ui/transcript-tail.js +28 -20
  384. package/dist/src/ui/transcript.js +63 -23
  385. package/dist/src/ui/tui/tui-app.js +0 -1
  386. package/dist/src/ui/tui/tui-backlog.js +51 -14
  387. package/dist/src/ui/tui/tui-editor-answer.js +27 -31
  388. package/dist/src/ui/tui/tui-editor-attach.js +3 -5
  389. package/dist/src/ui/tui/tui-editor-backends.js +132 -0
  390. package/dist/src/ui/tui/tui-editor-brief.js +10 -12
  391. package/dist/src/ui/tui/tui-editor-budget.js +35 -31
  392. package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
  393. package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
  394. package/dist/src/ui/tui/tui-editor-config.js +86 -0
  395. package/dist/src/ui/tui/tui-editor-entry.js +36 -47
  396. package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
  397. package/dist/src/ui/tui/tui-editor-instances.js +37 -0
  398. package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
  399. package/dist/src/ui/tui/tui-editor-interval.js +11 -9
  400. package/dist/src/ui/tui/tui-editor-model.js +29 -0
  401. package/dist/src/ui/tui/tui-editor-notes.js +3 -5
  402. package/dist/src/ui/tui/tui-editor-notify.js +25 -0
  403. package/dist/src/ui/tui/tui-editor-pause.js +48 -59
  404. package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
  405. package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
  406. package/dist/src/ui/tui/tui-editor-queued.js +5 -9
  407. package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
  408. package/dist/src/ui/tui/tui-editor-retire.js +4 -6
  409. package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
  410. package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
  411. package/dist/src/ui/tui/tui-editor-search.js +45 -33
  412. package/dist/src/ui/tui/tui-editor-session.js +60 -34
  413. package/dist/src/ui/tui/tui-editor-slots.js +23 -0
  414. package/dist/src/ui/tui/tui-editors.js +35 -2
  415. package/dist/src/ui/tui/tui-frame.js +56 -11
  416. package/dist/src/ui/tui/tui-history.js +8 -9
  417. package/dist/src/ui/tui/tui-input.js +13 -11
  418. package/dist/src/ui/tui/tui-keymap.js +0 -1
  419. package/dist/src/ui/tui/tui-keys.js +24 -9
  420. package/dist/src/ui/tui/tui-pane-activity.js +14 -11
  421. package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
  422. package/dist/src/ui/tui/tui-pane-brief.js +0 -1
  423. package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
  424. package/dist/src/ui/tui/tui-pane-health.js +58 -8
  425. package/dist/src/ui/tui/tui-pane-history.js +402 -82
  426. package/dist/src/ui/tui/tui-pane-pending.js +308 -13
  427. package/dist/src/ui/tui/tui-pane-queued.js +71 -55
  428. package/dist/src/ui/tui/tui-pane-report.js +46 -14
  429. package/dist/src/ui/tui/tui-pane-session.js +25 -6
  430. package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
  431. package/dist/src/ui/tui/tui-pane.js +80 -23
  432. package/dist/src/ui/tui/tui-panes.js +100 -30
  433. package/dist/src/ui/tui/tui.js +14 -14
  434. package/dist/src/verdict/fix-claim.js +82 -32
  435. package/dist/src/verdict/no-change.js +4 -4
  436. package/dist/src/verdict/refusal.js +40 -16
  437. package/dist/src/verdict/reply-contract.js +42 -31
  438. package/dist/src/version.js +0 -1
  439. package/package.json +2 -1
  440. package/dist/src/loop/revision.js +0 -70
  441. package/dist/src/roles/loop-ids.js +0 -77
  442. package/dist/src/tick/stage-check.js +0 -419
  443. package/dist/src/tick/telemetry-digest.js +0 -25
  444. package/dist/src/tick/tick-detail-data.js +0 -108
  445. package/dist/src/tick/tick-detail.js +0 -163
  446. package/dist/src/ui/gui/gui-client-sound.js +0 -53
@@ -1,25 +1,26 @@
1
- // vocabulary: plain
2
1
  /** The dashboard's detail drawer, browser-side: a sheet on the right that opens over the page
3
- * without losing it. For a loop it shows the state, the loop's controls (prompt, wake now or later,
2
+ * without losing it. For a role it shows the state, the role's controls (prompt, wake now or later,
4
3
  * pause/resume, abort), what it is working on, its last result, the metrics the team table
5
- * leaves out (tokens, context, cost, timings), its recent ticks (/api/history filtered to the
6
- * loop), and its live transcript (/api/transcript, re-fetched on the 1 s poll and kept pinned
7
- * to the bottom while the reader is there). For a backlog entry it shows the entry in full
8
- * (/api/backlog), rendered as Markdown, with a shortcut into the composer — answering a
4
+ * leaves out (tokens, context, cost, timings), its recent runs (/api/history filtered to the
5
+ * role), and its live transcript (/api/transcript, re-fetched on the 1 s poll and kept pinned
6
+ * to the bottom while the reader is there; a "Show prompts" button adds each agent run's prompt
7
+ * with `&prompt=1`; a search box narrows it with `&grep=`). For a backlog entry it shows the entry
8
+ * in full (/api/backlog), rendered as Markdown, with a shortcut into the composer — answering a
9
9
  * question, or asking the director about a plan or bug — and, for a plan or bug, a two-click
10
10
  * Cancel that removes the entry (/api/backlog-cancel). Esc, the close button, or clicking the
11
11
  * same row again closes it. Spliced into gui-client.ts's script, reaching its helpers through
12
12
  * that concatenation. */
13
- export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind: "loop", role } | { kind: "entry", file, index } while open
14
- let drawerTicks = null; // the open loop's recent history rows (null until fetched)
15
- let drawerTicksAt = 0;
16
- let drawerChange = null; // the open loop's unmerged change: { role, state: "loading"|"ok"|"error", view, message }
17
- let drawerRole = null; // the open loop's /api/role payload: { role, state: "loading"|"ok"|"error", view, message }
13
+ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind: "role", role } | { kind: "entry", file, index } while open
14
+ let drawerRuns = null; // the open role's recent history rows (null until fetched)
15
+ let drawerRunsAt = 0;
16
+ let drawerChangeStat = false;
17
+ let drawerChange = null; // the open role's unmerged change: { role, state: "loading"|"ok"|"error", view, message }
18
+ let drawerRole = null; // the open role's /api/role payload: { role, state: "loading"|"ok"|"error", view, message }
18
19
  const openEntryKey = () => (drawer && drawer.kind === "entry" ? drawer.file + ":" + drawer.index : null);
19
- const openLoopRole = () => (drawer && drawer.kind === "loop" ? drawer.role : null); // vocabulary-ok: the drawer kind stays "loop"
20
- // One key/value row builder shared by the loop-metrics table (renderLoopDrawer) and the
21
- // Configuration section (drawerConfigHtml): identical markup, formerly copy-pasted into two
22
- // local consts (kv and row) so the row shape could drift between the two sections.
20
+ const openRoleId = () => (drawer && drawer.kind === "role" ? drawer.role : null);
21
+ // One key/value row builder shared by the role-metrics table (renderRoleDrawer) and the
22
+ // Configuration section (drawerConfigHtml, in gui-client-drawer-config.ts): identical markup,
23
+ // formerly copy-pasted into two local consts (kv and row) so the row shape could drift.
23
24
  const kvRow = (k, v, title) => "<div><dt>" + esc(k) + "</dt><dd title='" + esc(title || v) + "'>" + esc(v) + "</dd></div>";
24
25
  // A diff block in the Change section (the committed change and the dirty-tree uncommitted
25
26
  // diff, drawerChangeHtml's two arms): trim the trailing newline the diff carriage-returned,
@@ -35,7 +36,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
35
36
  $("scrim").hidden = false;
36
37
  }
37
38
  function closeDrawer() {
38
- const wasLoop = openLoopRole();
39
+ const wasRole = openRoleId();
39
40
  drawer = null;
40
41
  entryEditing = null;
41
42
  noteEditing = null;
@@ -46,8 +47,8 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
46
47
  instancesEditing = null;
47
48
  modelEditing = null;
48
49
  disarmNoteClear();
49
- if (wasLoop && (location.hash.startsWith("#role/") || location.hash.startsWith("#loop/"))) history.replaceState(null, "", "#" + activeView); // vocabulary-ok: legacy dashboard hash, still routed
50
- drawerTicks = null;
50
+ if (wasRole && (location.hash.startsWith("#role/") || location.hash.startsWith("#loop/"))) history.replaceState(null, "", "#" + activeView); // vocabulary-ok: legacy dashboard hash, still routed
51
+ drawerRuns = null;
51
52
  drawerChange = null;
52
53
  drawerRole = null;
53
54
  $("drawer").hidden = true;
@@ -58,29 +59,36 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
58
59
  if (lastStatus) renderTeam(lastStatus); // drop the row highlight
59
60
  }
60
61
 
61
- function openLoop(role) {
62
- drawer = { kind: "loop", role }; // vocabulary-ok: the drawer kind stays "loop"
62
+ let transcriptPrompts = false;
63
+ let transcriptGrep = "";
64
+ function openRole(role) {
65
+ drawer = { kind: "role", role };
66
+ transcriptPrompts = false;
67
+ drawerChangeStat = false;
68
+ transcriptGrep = "";
63
69
  const hash = "#role/" + encodeURIComponent(role);
64
70
  if (location.hash !== hash) history.replaceState(null, "", hash);
65
- drawerTicks = null;
66
- drawerTicksAt = 0;
71
+ drawerRuns = null;
72
+ drawerRunsAt = 0;
67
73
  drawerChange = null;
68
74
  drawerRole = null;
69
75
  showDrawer("<div id='draweractions' class='drawer-actions'></div><div id='drawermeta'></div><div id='drawerrole'></div><section class='transcript-sec'><div class='sec-head'><h3>Live transcript</h3>" +
70
- "<span class='muted' style='margin-left:auto;font-size:13px'>last 200 lines</span></div>" +
76
+ "<input type='search' id='transcriptgrep' class='field' placeholder='Search transcript' aria-label='Search transcript' autocomplete='off' style='margin-left:auto'>" +
77
+ "<button type='button' class='btn btn-sm' id='transcriptprompts' data-transcript-prompts style='margin-left:8px'>Show prompts</button>" +
78
+ "<span class='muted' style='margin-left:12px;font-size:13px'>last 200 lines</span></div>" +
71
79
  "<pre id='transcript' class='transcript'><span class='muted'>Loading…</span></pre></section>");
72
80
  if (lastStatus) {
73
- renderLoopDrawer(lastStatus);
81
+ renderRoleDrawer(lastStatus);
74
82
  renderTeam(lastStatus);
75
83
  }
76
- loadLoopTicks(true);
84
+ loadRoleRuns(true);
77
85
  loadDrawerChange(role);
78
86
  loadDrawerRole(role);
79
87
  refreshTranscript(true);
80
88
  }
81
- function toggleLoop(role) {
82
- if (openLoopRole() === role) closeDrawer();
83
- else openLoop(role);
89
+ function toggleRole(role) {
90
+ if (openRoleId() === role) closeDrawer();
91
+ else openRole(role);
84
92
  }
85
93
  function openEntry(file, index) {
86
94
  drawer = { kind: "entry", file, index: Number(index) };
@@ -101,8 +109,8 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
101
109
  return change && change.summary ? change.summary : "";
102
110
  }
103
111
 
104
- function renderLoopDrawer(d) {
105
- const role = openLoopRole();
112
+ function renderRoleDrawer(d) {
113
+ const role = openRoleId();
106
114
  if (!role) return;
107
115
  const l = (d.roles || []).find((x) => x.role === role);
108
116
  if (!l) {
@@ -114,7 +122,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
114
122
  return;
115
123
  }
116
124
  const info = phaseInfo(l.phase);
117
- const paused = (d.pausedLoops || []).includes(l.role);
125
+ const paused = (d.pausedLoops || []).includes(l.role); // retired-name-ok: serialized /api key
118
126
  const detail = phaseDetail(l, info, d);
119
127
  paintPanel("drawerhead", "<div class='kicker'>Role" + (l.custom ? " · user-defined" : "") + "</div>" +
120
128
  "<h2 id='drawertitle' class='mono'>" + esc(roleLabel(l.role)) + "</h2><div class='chips'>" + pill(info) +
@@ -124,7 +132,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
124
132
  "' data-role='" + esc(l.role) + "'>" + icon(ic) + esc(label) + "</button>";
125
133
  let actions = btn("prompt", "chat", "Message this role");
126
134
  // The running set (info.live), not the permit-holder inFlight: the exempt running director
127
- // has an abortable tick without holding a maxConcurrent permit (BUGS.md 2026-10-06).
135
+ // has an abortable run without holding a maxConcurrent permit (BUGS.md 2026-10-06).
128
136
  if (info.live) actions += abortConfirming(l.role) ? btn("abort", "stop", "Click again to stop", "btn-danger confirming") : btn("abort", "stop", "Stop this run", "btn-danger");
129
137
  else {
130
138
  actions += btn("wake", "bolt", "Wake now");
@@ -163,17 +171,17 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
163
171
  : next === "now" || next.startsWith("now ") ? next
164
172
  : next.startsWith("sleeping ") ? "in " + next.slice(9) + " (sleeping)"
165
173
  : "in " + next;
166
- const tickWord = info.live ? "this run" : "last run";
174
+ const runWord = info.live ? "this run" : "last run";
167
175
  const stats = "<dl class='kv'>" + kvRow("Commits", String(l.commits)) + kvRow("Runs", String(l.runs)) +
168
- kvRow("Tokens, " + tickWord, fmtTokens(l.generated)) + kvRow("Peak context, " + tickWord, fmtTokens(l.peakCtx)) +
176
+ kvRow("Tokens, " + runWord, fmtTokens(l.generated)) + kvRow("Peak context, " + runWord, fmtTokens(l.peakCtx)) +
169
177
  kvRow("Spent today", fmtUsd(l.todayUsd)) + kvRow("Spent in total", fmtUsd(l.costUsd)) +
170
- kvRow("Last run ended", l.lastRunEndedAt ? fmtAgo(l.lastRunEndedAt) : "never", fmtLastTick(l.lastRunEndedAt)) +
178
+ kvRow("Last run ended", l.lastRunEndedAt ? fmtAgo(l.lastRunEndedAt) : "never", fmtLastRun(l.lastRunEndedAt)) +
171
179
  kvRow("Last merged", l.lastMergedAt ? fmtAgo(l.lastMergedAt) : "never") +
172
180
  kvRow("Next run", nextNote) + "</dl>";
173
- const ticks = drawerTicks === null
181
+ const runs = drawerRuns === null
174
182
  ? "<p class='muted'>Loading…</p>"
175
- : drawerTicks.length
176
- ? "<ul class='ticks'>" + drawerTicks.map((t) => { // vocabulary-ok: the ticks class stays
183
+ : drawerRuns.length
184
+ ? "<ul class='run-list'>" + drawerRuns.map((t) => {
177
185
  const res = resultInfo(t.result);
178
186
  return "<li><span class='when' title='" + esc(t.time) + "'>#" + esc(t.run) + " · " + esc(fmtAgo(t.ts)) + "</span>" +
179
187
  "<span class='clamp1' title='" + esc(t.detail) + "'><span class='res t-" + res.tone + "'>" + esc(res.label) + "</span> " + esc(t.detail) + "</span>" +
@@ -181,16 +189,17 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
181
189
  }).join("") + "</ul>"
182
190
  : "<p class='muted'>No completed runs in the log yet.</p>";
183
191
  paintPanel("draweractions", actions);
184
- paintPanel("drawermeta", now + last + stats +
185
- "<section><div class='sec-head'><h3>Recent runs</h3></div>" + ticks + "</section>" + drawerChangeSection());
186
- // The notebook/prompt live in their own panel, outside drawermeta: the poll rewrites
187
- // drawermeta whenever its countdown or tick-age text changes, and an innerHTML rewrite
188
- // would collapse an opened Next tick prompt. This panel's markup is stable while its
192
+ const queued = drawerQueueHtml(d.mergeQueue, l.role, Date.now(), d.mergeQueueLabels);
193
+ paintPanel("drawermeta", now + last + queued + stats +
194
+ "<section><div class='sec-head'><h3>Recent runs</h3></div>" + runs + "</section>" + drawerChangeSection());
195
+ // The notes/prompt live in their own panel, outside drawermeta: the poll rewrites
196
+ // drawermeta whenever its countdown or run-age text changes, and an innerHTML rewrite
197
+ // would collapse an opened Next run prompt. This panel's markup is stable while its
189
198
  // payload is unchanged, so paintPanel leaves it (and the disclosure's open state) alone.
190
199
  paintPanel("drawerrole", drawerRoleSection());
191
200
  }
192
201
 
193
- // The open loop's unmerged change, as /api/diff?role serves it: the full patch the CLI's
202
+ // The open role's unmerged change, as /api/diff?role serves it: the full patch the CLI's
194
203
  // tumwater diff --role prints. Degrades like the collector — a missing worktree, a missing
195
204
  // base branch, or no work each get a plain statement, and an unreadable payload says so
196
205
  // rather than rendering a half-view. Pure of side effects; the view paints what it returns.
@@ -200,10 +209,15 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
200
209
  const dirty = Array.isArray(view.dirtyFiles) ? view.dirtyFiles : [];
201
210
  if (view.state === "absent") return "<p class='muted'>No workspace yet — this role has not run.</p>";
202
211
  if (view.state === "no-base") return "<p class='muted'>Main branch " + esc(view.mainBranch) + " does not exist.</p>";
212
+ const scoped = typeof view.path === "string";
213
+ if (scoped && view.state === "ready" && commits.length === 0 && dirty.length === 0) {
214
+ return "<p class='muted'>No pending change to " + esc(view.path) + ".</p>";
215
+ }
203
216
  if (view.state !== "ready" || (view.ahead === 0 && dirty.length === 0)) return "<p class='muted'>No unlanded work.</p>";
204
217
  let html = "<p class='note'>" + esc(view.branch) + " · " + esc(plural(view.ahead, "commit")) +
205
218
  " ahead of " + esc(view.mainBranch) +
206
- (dirty.length ? " · " + esc(plural(dirty.length, "uncommitted file")) : "") + "</p>";
219
+ (dirty.length ? " · " + esc(plural(dirty.length, "uncommitted file")) : "") +
220
+ (scoped ? " · limited to " + esc(view.path) : "") + "</p>";
207
221
  html += commitRowsHtml(commits);
208
222
  if (view.diff) html += diffPre(view.diff);
209
223
  if (dirty.length) {
@@ -218,37 +232,43 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
218
232
  if (!c || c.state === "loading") body = "<p class='muted'>Loading…</p>";
219
233
  else if (c.state === "error") body = "<p class='muted'>" + esc(c.message || "Pending change unavailable") + "</p>";
220
234
  else body = drawerChangeHtml(c.view);
221
- return "<section><div class='sec-head'><h3>Pending change</h3></div>" + body + "</section>";
235
+ return "<section><div class='sec-head'><h3>Pending change</h3>" +
236
+ "<button type='button' class='btn btn-sm' data-change-stat style='margin-left:auto'>" + (drawerChangeStat ? "Full patch" : "Stat only") + "</button></div>" + body + "</section>";
222
237
  }
223
238
  // Fetch one drawer section's payload once when the drawer opens, storing it in the
224
- // caller's state cell. A stale answer (the drawer moved to another loop) is dropped;
239
+ // caller's state cell. A stale answer (the drawer moved to another role) is dropped;
225
240
  // later status polls repaint from the stored view. The assign callback writes that cell
226
241
  // (drawerChange / drawerRole), so the loading/ok/error contract lives here once for both
227
- // the pending-change and notebook sections.
242
+ // the pending-change and notes sections.
228
243
  async function loadDrawerSection(role, url, fallbackMessage, assign) {
229
244
  if (!role) return;
230
245
  assign({ role, state: "loading" });
231
246
  try {
232
247
  const d = await getJson(url + encodeURIComponent(role), pollSignal());
233
- if (openLoopRole() !== role) return;
248
+ if (openRoleId() !== role) return;
234
249
  assign({ role, state: "ok", view: d });
235
250
  } catch (err) {
236
- if (openLoopRole() !== role) return;
251
+ if (openRoleId() !== role) return;
237
252
  assign({ role, state: "error", message: err && err.message ? err.message : fallbackMessage });
238
253
  }
239
- if (lastStatus) renderLoopDrawer(lastStatus);
254
+ if (lastStatus) renderRoleDrawer(lastStatus);
240
255
  }
241
- // The open loop's unmerged change, as /api/diff?role serves it.
256
+ // The open role's unmerged change, as /api/diff?role serves it.
242
257
  function loadDrawerChange(role) {
243
- return loadDrawerSection(role, "/api/diff?role=", "Pending change unavailable", (v) => { drawerChange = v; });
258
+ return loadDrawerSection(role, "/api/diff?" + (drawerChangeStat ? "stat=1&" : "") + "role=", "Pending change unavailable", (v) => { drawerChange = v; });
259
+ }
260
+
261
+ function toggleDrawerChangeStat() {
262
+ drawerChangeStat = !drawerChangeStat;
263
+ return loadDrawerChange(openRoleId());
244
264
  }
245
265
 
246
- // The open loop's notebook and next-tick prompt, as /api/role serves them (rolePayload):
266
+ // The open role's notes and next-run prompt, as /api/role serves them (rolePayload):
247
267
  // the same note and assembled prompt the CLI's role command shows, so the operator can
248
- // diagnose a loop from the page. The note renders in a <pre>; the next prompt sits in a
268
+ // diagnose a role from the page. The note renders in a <pre>; the next prompt sits in a
249
269
  // <details> so it does not push the rest of the drawer down. Both empty cases say so in a
250
270
  // muted line. It paints into its own #drawerrole panel, not #drawermeta: the poll rewrites
251
- // drawermeta whenever its countdown or tick-age text changes, and an innerHTML rewrite would
271
+ // drawermeta whenever its countdown or run-age text changes, and an innerHTML rewrite would
252
272
  // collapse an opened <details>. This panel's string is stable while the payload is unchanged,
253
273
  // so paintPanel skips it and the disclosure stays open.
254
274
  function drawerRoleSection() {
@@ -272,14 +292,14 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
272
292
  : note ? "<pre class='mono'>" + esc(note) + "</pre>" : "<p class='muted'>No notes yet.</p>";
273
293
  body = "<div class='sec-head'><h3>Notes</h3>" + (editing ? "" : buttons) + "</div>" + noteBody +
274
294
  "<details><summary>Next run's prompt</summary>" + (nextPrompt ? "<pre class='mono'>" + esc(nextPrompt) + "</pre>" : "<p class='muted'>Nothing to run (empty inbox).</p>") + "</details>" +
275
- // The loop's recent tick outcomes (rolePayload's recentOutcomes, read from the event
276
- // log), rendered like the Recent ticks list above: each with its relative age, result
277
- // label, summary, and duration. Mirrors the Recent tick outcomes section of
295
+ // The role's recent run outcomes (rolePayload's recentOutcomes, read from the event
296
+ // log), rendered like the Recent runs list above: each with its relative age, result
297
+ // label, summary, and duration. Mirrors the Recent run outcomes section of
278
298
  // tumwater role <id> — no backticks allowed here (this whole script is one big
279
- // String.raw template), so the page and the CLI read one loop's outcomes identically.
299
+ // String.raw template), so the page and the CLI read one role's outcomes identically.
280
300
  (outcomes.length ? "<section><div class='sec-head'><h3>Recent outcomes" +
281
301
  "<span class='muted' style='font-size:13px'>· " + outcomes.length + "</span></h3></div>" +
282
- "<ul class='ticks'>" + outcomes.map((o) => { // vocabulary-ok: the ticks class stays
302
+ "<ul class='run-list'>" + outcomes.map((o) => {
283
303
  const info = resultInfo(o.result);
284
304
  const detail = typeof o.summary === "string" && o.summary.trim() !== "" ? o.summary : "";
285
305
  return "<li><span class='when' title='" + esc(new Date(Number(o.ts)).toLocaleString()) + "'>" + esc(fmtAgo(Number(o.ts))) + "</span>" +
@@ -290,211 +310,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
290
310
  }
291
311
  return "<section>" + body + "</section>";
292
312
  }
293
- // A Configuration section built from the same /api/role payload the CLI's tumwater role
294
- // <id> renders (rolePayload), so the drawer's and the terminal's rows read one document and
295
- // cannot drift: what the loop RUNS ON (model/tier, provider cap, image reading, the
296
- // fallback pairs, billing, min tick interval) and CARRIES (queued prompts, the instructions
297
- // override, find text). Each row is guarded by a present/own check so an absent field
298
- // renders no row — a cap-less team shows no Provider row, no fallback shows no fallback
299
- // rows, and a notes-only payload adds an empty (omitted) section. Labels and wording mirror
300
- // role-render.ts's family; no backticks allowed here (this whole script is one big
301
- // String.raw template).
302
- function drawerConfigHtml(view, role) {
303
- const pair = (p, m) => [p, m].filter(Boolean).join("/");
304
- const singleLine = (s) => String(s).replace(/\s+/g, " ").trim();
305
- // A verbatim value's single-line clamped rendering (80 chars + ellipsis) with the full
306
- // text riding the title tooltip — the same clamp+tooltip idiom the tick summaries use.
307
- const clamp = (t) => { const one = singleLine(t); return one.length > 80 ? one.slice(0, 80) + "…" : one; };
308
- const rows = [];
309
- // A role that runs on pi's own saved default carries no provider/model but a piDefault
310
- // selector (rolePayload's resolvedModelFields leaves both undefined), and the CLI's
311
- // Model line names that default (<agent> default (<selector>), agentDefault). Mirror
312
- // that branch so the drawer names the model a default pi team actually runs on.
313
- const defaultLine = typeof view.piDefault === "string"
314
- ? (view.backend && view.backend !== "pi" ? view.backend : "pi") + " default (" + view.piDefault + ")"
315
- : "";
316
- const modelLine = defaultLine !== "" ? defaultLine : pair(view.provider, view.model);
317
- if (modelLine !== "" || view.modelTier !== undefined) {
318
- rows.push(kvRow("Model", (modelLine !== "" ? modelLine + " " : "") + (view.modelTier !== undefined ? "(" + view.modelTier + " tier)" : "").trim()));
319
- }
320
- // The per-role model triple (roles.<base>.provider/.model/.thinking, the file's own raw
321
- // overrides rolePayload's providerOverride/modelOverride/thinkingOverride carry) — the
322
- // one per-role write the Model row above still renders read-only, the dashboard half of
323
- // the CLI's --provider/--model/--thinking. An always-shown row (every role, the director
324
- // included, since the CLI writes its triple too) whose body names the payload's raw
325
- // overrides, or no overrides, and whose control is a model-edit button (Edit/Set model);
326
- // while the editor is open for this role, three text inputs prefilled with the raw
327
- // overrides (empty when unset) plus Save/Discard replace the body, mirroring the interval
328
- // editor's markup.
329
- const rawTriple = [];
330
- if (typeof view.providerOverride === "string" && view.providerOverride !== "") rawTriple.push("provider: " + view.providerOverride);
331
- if (typeof view.modelOverride === "string" && view.modelOverride !== "") rawTriple.push("model: " + view.modelOverride);
332
- if (typeof view.thinkingOverride === "string" && view.thinkingOverride !== "") rawTriple.push("thinking: " + view.thinkingOverride);
333
- const modelBody = rawTriple.length ? rawTriple.join(" · ") : "no overrides";
334
- const editModel = modelEditing && modelEditing.role === role;
335
- const modelControl = editModel
336
- ? "<input id='modeleditprovider' type='text' placeholder='provider' value='" + esc(modelEditing.provider) + "'>" +
337
- "<input id='modeleditmodel' type='text' placeholder='model' value='" + esc(modelEditing.model) + "'>" +
338
- "<input id='modeleditthinking' type='text' placeholder='thinking' value='" + esc(modelEditing.thinking) + "'>" +
339
- "<div><button type='button' class='btn btn-sm btn-primary' data-act='model-save' data-arg='" + esc(role) + "'>Save</button> " +
340
- "<button type='button' class='btn btn-sm' data-act='model-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
341
- : "<button type='button' class='btn btn-sm' data-act='model-edit' data-arg='" + esc(role) + "'>" + (rawTriple.length ? "Edit" : "Set model") + "</button>";
342
- rows.push("<div><dt>Model overrides</dt><dd title='" + esc(modelBody) + "'>" + esc(modelBody) + modelControl + "</dd></div>");
343
- // Context window: the resolved primary seam's declared window - the same seam '- Next run:'
344
- // previews. Guard optional to keep an older payload that lacks the field rendering as before.
345
- if (view.contextWindow !== undefined) {
346
- rows.push(kvRow("Context window", view.contextWindow === null ? "unknown" : fmtTokens(view.contextWindow) + " tokens"));
347
- }
348
- if (view.providerKey !== undefined && view.providerCap !== undefined) {
349
- rows.push(kvRow("Provider", view.providerKey + " (cap " + view.providerCap + ")"));
350
- }
351
- if (view.imageInput !== undefined) {
352
- const images = view.imageInput === true ? "yes"
353
- : view.imageInput === false ? (view.imageReroute ? "no — reruns on " + view.imageReroute : "no")
354
- : "unknown";
355
- rows.push(kvRow("Images", images));
356
- }
357
- if (view.modelFallback && typeof view.modelFallback === "object" && view.modelFallback.model !== undefined) {
358
- const fb = view.modelFallback;
359
- const when = typeof fb.since === "number" ? " since " + fmtAgo(fb.since) : "";
360
- rows.push(kvRow("Model fallback", pair(fb.provider, fb.model) + " — on fallback" + when + " (primary failing: " + (fb.reason ?? "") + ")"));
361
- }
362
- if (view.fallback && typeof view.fallback === "object" && view.fallback.model !== undefined) {
363
- rows.push(kvRow("Budget fallback", pair(view.fallback.provider, view.fallback.model) + " (" + (view.fallbackFree ? "free" : "priced") + ")"));
364
- }
365
- if (view.billing !== undefined) {
366
- const window = view.billing === "subscription" && view.billingWindow ? " (" + view.billingWindow + " window)" : "";
367
- rows.push(kvRow("Billing", (view.billing === "api" ? "API" : view.billing) + window));
368
- }
369
- // The min run interval (rolePayload's minRunIntervalSeconds), worded like role-render.ts's
370
- // Min run interval line, plus a control to set/clear the per-role override — the last
371
- // per-role hold the drawer edits. Shown whenever the payload names an interval (always —
372
- // the effective value is the role's override or the team default); the body tags whether
373
- // the shown value is the role's own override or the team default. While the editor is open
374
- // for this role, an input prefilled with the override (empty when none) and Save/Discard
375
- // replace the sentence, mirroring the daily-cap editor's markup; a role with no override
376
- // still seeds an empty input, so an empty buffer on Save clears.
377
- if (typeof view.minRunIntervalSeconds === "number") {
378
- const editing = intervalEditing && intervalEditing.role === role;
379
- const intervalBody = String(view.minRunIntervalSeconds) + "s" +
380
- (typeof view.minRunIntervalOverride === "number" ? " (override)" : " (team default)");
381
- const seed = intervalEditing ? intervalEditing.value : null;
382
- const intervalControl = editing
383
- ? "<input id='intervaleditinput' type='number' min='0' step='1' value='" + (seed === "" || seed == null ? "" : String(seed)) + "' placeholder='team default'>" +
384
- "<div><button type='button' class='btn btn-sm btn-primary' data-act='interval-save' data-arg='" + esc(role) + "'>Save</button> " +
385
- "<button type='button' class='btn btn-sm' data-act='interval-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
386
- : "<button type='button' class='btn btn-sm' data-act='interval-edit' data-arg='" + esc(role) + "'>" + (typeof view.minRunIntervalOverride === "number" ? "Edit" : "Set interval") + "</button>";
387
- rows.push("<div><dt>Min run interval</dt><dd title='" + esc(intervalBody) + "'>" + esc(intervalBody) + intervalControl + "</dd></div>");
388
- }
389
- // The parallel-runner count (roles.<base>.instances), worded like role-render.ts's
390
- // Instances line — the last per-role hold the drawer edits. Rendered only for a role that
391
- // may split (its base, an instance strip, is feature or bugfix): the count is meaningless
392
- // for any other role, which role-view never carries. The body reads the payload's numeric
393
- // count (roles.<base>.instances) tagged (set) or falls back to 1 tagged (default) when the
394
- // file leaves it unset — role-view carries instances only when the file sets it. While the
395
- // editor is open for this role, an input prefilled with the count (empty when none) and
396
- // Save/Discard
397
- // replace the sentence, mirroring the interval editor's markup; a role with no count still
398
- // seeds an empty input, so an empty buffer on Save clears.
399
- const splitBase = role.replace(/-\d+$/, "");
400
- if (splitBase === "feature" || splitBase === "bugfix") {
401
- const hasInstances = typeof view.instances === "number";
402
- const instCount = hasInstances ? view.instances : 1;
403
- const instBody = String(instCount) + (hasInstances ? " (set)" : " (default)");
404
- const editingInst = instancesEditing && instancesEditing.role === role;
405
- const seed = instancesEditing ? instancesEditing.value : null;
406
- const instControl = editingInst
407
- ? "<input id='instanceseditinput' type='number' min='1' max='8' step='1' value='" + (seed === "" || seed == null ? "" : String(seed)) + "' placeholder='default 1'>" +
408
- "<div><button type='button' class='btn btn-sm btn-primary' data-act='instances-save' data-arg='" + esc(role) + "'>Save</button> " +
409
- "<button type='button' class='btn btn-sm' data-act='instances-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
410
- : "<button type='button' class='btn btn-sm' data-act='instances-edit' data-arg='" + esc(role) + "'>" + (hasInstances ? "Edit" : "Set instances") + "</button>";
411
- rows.push("<div><dt>Instances</dt><dd title='" + esc(instBody) + "'>" + esc(instBody) + instControl + "</dd></div>");
412
- }
413
- // The windows governing the loop, worded like role-render.ts's Quiet hours line; the end time
414
- // mirrors quietWindowEnd (browser code cannot import it). A non-director role's own window
415
- // is an always-shown editable row (the Daily cap row's rationale: the control sits where
416
- // the hold is shown) — including when the payload carries no role-scope window ("no own
417
- // window"); any other window stays a read-only row worded exactly as before ("team window").
418
- const own = Array.isArray(view.quietHours) ? view.quietHours.find(function (w) { return w && w.scope === "role"; }) : undefined;
419
- const ownWindow = own && typeof own.window === "string" ? own.window : "";
420
- if (role !== "director") {
421
- const ownEnd = String(ownWindow).split("-")[1];
422
- const ownState = own && own.inWindow
423
- ? "holding now — quiet until " + (ownEnd ? ownEnd.trim() : "")
424
- : "not holding now";
425
- const editQuiet = quietEditing && quietEditing.role === role;
426
- const quietBody = ownWindow !== "" ? ownWindow + " (own window; " + ownState + ")" : "no own window";
427
- const quietControl = editQuiet
428
- ? "<input id='quieteditinput' type='text' placeholder='23:00-07:00' value='" + esc(quietEditing.window) + "'>" +
429
- "<div><button type='button' class='btn btn-sm btn-primary' data-act='quiet-save' data-arg='" + esc(role) + "'>Save</button> " +
430
- "<button type='button' class='btn btn-sm' data-act='quiet-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
431
- : "<button type='button' class='btn btn-sm' data-act='quiet-edit' data-arg='" + esc(role) + "'>" + (ownWindow !== "" ? "Edit" : "Set quiet hours") + "</button>";
432
- rows.push("<div><dt>Quiet hours</dt><dd title='" + esc(quietBody) + "'>" + esc(quietBody) + quietControl + "</dd></div>");
433
- }
434
- if (Array.isArray(view.quietHours)) view.quietHours.forEach(function (w) {
435
- if (!w || w.scope === "role" || typeof w.window !== "string") return;
436
- const end = String(w.window).split("-")[1];
437
- const state = w.inWindow ? "holding now — quiet until " + (end ? end.trim() : "") : "not holding now";
438
- rows.push(kvRow("Quiet hours", w.window + " (team window; " + state + ")"));
439
- });
440
- if (typeof view.inboxCount === "number") rows.push(kvRow("Queued prompts", String(view.inboxCount)));
441
- // The loop's own spend over its retained event window (rolePayload's usage, summed from the
442
- // same run_end events recentOutcomes reads): how much the loop burned on tokens and cost,
443
- // with a notional suffix only when a claude-subscription seam burned non-zero notional spend
444
- // — mirroring role-render.ts's Spend line, so the drawer and tumwater role <id> read one
445
- // loop's spend identically. Each numeric field is coerced (foreign shapes read as 0) and an
446
- // all-zero usage is absent on the payload, so this row never renders for a never-costly loop.
447
- if (view.usage && typeof view.usage === "object") {
448
- const u = view.usage;
449
- const real = Number(u.costUsd) || 0;
450
- const tokens = Number(u.tokens) || 0;
451
- const notional = Number(u.notionalCostUsd) || 0;
452
- let spend = fmtUsd(real) + " · " + fmtTokens(tokens) + " tokens";
453
- if (notional !== 0) spend += " · " + fmtUsd(notional) + " notional";
454
- rows.push(kvRow("Spend", spend));
455
- }
456
- // The per-role daily cap (rolePayload's dailyCap), worded like role-render.ts's Daily cap line.
457
- // The per-role daily cap (rolePayload's dailyCap), worded like role-render.ts's Daily cap
458
- // line. Shown for every non-director role — always, cap or not — because the "Set a cap"/
459
- // "Edit cap" control is the end of the amber role-cap alert's path: the alert's remedy
460
- // leads here. A director shows no row (role-view.ts's guard exempts it from a cap). While
461
- // the cap editor is open for this role, an input prefilled with the current cap (empty for
462
- // none) and Save/Discard replace the sentence, mirroring the notebook editor's markup.
463
- if (role !== "director") {
464
- const dc = view.dailyCap && typeof view.dailyCap === "object" ? view.dailyCap : null;
465
- const editing = capEditing && capEditing.role === role;
466
- const capBody = dc
467
- ? fmtUsd(Number(dc.spentUsd) || 0) + " of " + fmtUsd(Number(dc.capUsd) || 0) + " spent today (" + (dc.over ? "holding now — no new runs until local midnight" : "not holding") + ")" +
468
- (Array.isArray(dc.roleIds) && dc.roleIds.length > 1 ? " · shared by " + dc.roleIds.join(", ") : "")
469
- : "no cap";
470
- const capControl = editing
471
- ? "<input id='capeditinput' type='number' min='0' step='0.01' value='" + (capEditing.value ? String(capEditing.value) : "") + "' placeholder='no cap'>" +
472
- "<div><button type='button' class='btn btn-sm btn-primary' data-act='cap-save' data-arg='" + esc(role) + "'>Save</button> " +
473
- "<button type='button' class='btn btn-sm' data-act='cap-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
474
- : "<button type='button' class='btn btn-sm' data-act='cap-edit' data-arg='" + esc(role) + "'>" + (dc ? "Edit cap" : "Set a cap") + "</button>";
475
- rows.push("<div><dt>Daily cap</dt><dd title='" + esc(capBody) + "'>" + esc(capBody) + capControl + "</dd></div>");
476
- }
477
- // The standing instructions (roles.<base>.instructions, resolved through the base role),
478
- // worded like role-render.ts's Instructions override. Shown for every role — always,
479
- // override or not — because the Edit/Save control is how the director's and instance
480
- // runs' instructions get written from the dashboard: the same always-shown rationale as
481
- // the Daily cap row above. While the editor is open for this role, a textarea prefilled
482
- // with the current text and Save/Discard replace the sentence, mirroring the notebook
483
- // editor's markup; a role with no override still seeds an empty textarea, so an empty
484
- // buffer on Save clears.
485
- const instEditing = instructionsEditing && instructionsEditing.role === role;
486
- const instText = typeof view.instructions === "string" ? view.instructions : "";
487
- const instBody = instText.trim() !== "" ? clamp(instText) : "No standing instructions.";
488
- const instControl = instEditing
489
- ? "<textarea id='instructionsedittext' rows='10' class='mono' style='width:100%'>" + esc(instructionsEditing.text) + "</textarea>" +
490
- "<div><button type='button' class='btn btn-sm btn-primary' data-act='instructions-save' data-arg='" + esc(role) + "'>Save</button> " +
491
- "<button type='button' class='btn btn-sm' data-act='instructions-discard' data-arg='" + esc(role) + "'>Discard</button></div>"
492
- : "<button type='button' class='btn btn-sm' data-act='instructions-edit' data-arg='" + esc(role) + "'>Edit</button>";
493
- rows.push("<div><dt>Instructions override</dt><dd title='" + esc(instText) + "'>" + esc(instBody) + instControl + "</dd></div>");
494
- if (typeof view.find === "string" && view.find.trim() !== "") rows.push(kvRow("Find text", clamp(view.find), view.find));
495
- return rows.length ? "<section><div class='sec-head'><h3>Configuration</h3></div><dl class='kv'>" + rows.join("") + "</dl></section>" : "";
496
- }
497
- // An open notebook edit holds the draft's starting text, not the live payload, so a poll
313
+ // An open notes edit holds the draft's starting text, not the live payload, so a poll
498
314
  // refetch neither wipes the textarea nor changes the painted panel string. Clear waits for a
499
315
  // confirming click like entry cancel: the first click arms it for 5 s, the second posts.
500
316
  let noteEditing = null; // { role, text }
@@ -504,9 +320,9 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
504
320
  let intervalEditing = null; // { role, value } — value is the seeded override (number) or "" when none
505
321
  let instancesEditing = null; // { role, value } — value is the seeded count (number) or "" when none
506
322
  let modelEditing = null; // { role, provider, model, thinking } — the seeded raw overrides ("" when unset)
507
- function repaintDrawerRole() { if (lastStatus) renderLoopDrawer(lastStatus); }
508
- // The notebook clear and the backlog entry cancel each build a two-click confirm slot from
509
- // the shared confirmSlot factory (defined in the loops splice, an earlier one), so the arm,
323
+ function repaintDrawerRole() { if (lastStatus) renderRoleDrawer(lastStatus); }
324
+ // The notes clear and the backlog entry cancel each build a two-click confirm slot from
325
+ // the shared confirmSlot factory (defined in the roles splice, an earlier one), so the arm,
510
326
  // guard, and disarm logic lives once.
511
327
  const noteClear = confirmSlot(repaintDrawerRole);
512
328
  const noteClearArmed = (role) => noteClear.armed(role);
@@ -526,24 +342,24 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
526
342
  function stopModelEdit() { modelEditing = null; repaintDrawerRole(); }
527
343
  function startQuietEdit(role, window) { quietEditing = { role, window: String(window ?? "") }; repaintDrawerRole(); }
528
344
  function stopQuietEdit() { quietEditing = null; repaintDrawerRole(); }
529
- // The notebook fetch shares loadDrawerSection; closing the drawer clears drawerRole, so
345
+ // The notes fetch shares loadDrawerSection; closing the drawer clears drawerRole, so
530
346
  // reopening refetches.
531
347
  function loadDrawerRole(role) {
532
348
  return loadDrawerSection(role, "/api/role?role=", "Notes and prompt unavailable", (v) => { drawerRole = v; });
533
349
  }
534
350
 
535
- async function loadLoopTicks(force) {
536
- const role = openLoopRole();
537
- if (!role || (!force && Date.now() - drawerTicksAt < 5000)) return;
538
- drawerTicksAt = Date.now();
351
+ async function loadRoleRuns(force) {
352
+ const role = openRoleId();
353
+ if (!role || (!force && Date.now() - drawerRunsAt < 5000)) return;
354
+ drawerRunsAt = Date.now();
539
355
  try {
540
356
  const d = await getJson("/api/history?n=8&role=" + encodeURIComponent(role), pollSignal());
541
- if (openLoopRole() !== role) return;
542
- drawerTicks = d && Array.isArray(d.rows) ? d.rows : [];
357
+ if (openRoleId() !== role) return;
358
+ drawerRuns = d && Array.isArray(d.rows) ? d.rows : [];
543
359
  } catch {
544
- if (drawerTicks === null) drawerTicks = [];
360
+ if (drawerRuns === null) drawerRuns = [];
545
361
  }
546
- if (lastStatus) renderLoopDrawer(lastStatus);
362
+ if (lastStatus) renderRoleDrawer(lastStatus);
547
363
  }
548
364
 
549
365
  // Transcript lines by kind (transcript.ts's rendering): run separators, tool calls, thinking,
@@ -555,24 +371,38 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
555
371
  }).join("\n");
556
372
  }
557
373
  async function refreshTranscript(initial) {
558
- const role = openLoopRole();
374
+ const role = openRoleId();
559
375
  if (!role) return;
376
+ const prompts = transcriptPrompts;
377
+ const grep = transcriptGrep;
560
378
  let d;
561
379
  try {
562
- d = await getJson("/api/transcript?role=" + encodeURIComponent(role) + "&n=200", pollSignal());
380
+ d = await getJson("/api/transcript?role=" + encodeURIComponent(role) + "&n=200" + (prompts ? "&prompt=1" : "") + (grep ? "&grep=" + encodeURIComponent(grep) : ""), pollSignal());
563
381
  } catch {
564
382
  return; // keep the previous content on a failed poll
565
383
  }
566
384
  const pre = $("transcript");
567
- if (openLoopRole() !== role || !pre) return;
385
+ if (openRoleId() !== role || !pre || transcriptPrompts !== prompts || transcriptGrep !== grep) return;
568
386
  const lines = Array.isArray(d.lines) ? d.lines : [];
569
- const html = lines.length ? transcriptHtml(lines) : "<span class='muted'>No transcript yet — it fills in once this role's next run starts.</span>";
387
+ const html = lines.length ? transcriptHtml(lines) : grep ? "<span class='muted'>No transcript entries match \u201c" + esc(grep) + "\u201d.</span>" : "<span class='muted'>No transcript yet — it fills in once this role's next run starts.</span>";
570
388
  // Stay pinned to the newest line while the reader is at the bottom; leave them be if they
571
389
  // scrolled up to read.
572
390
  const atBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 12;
573
391
  if (paintPanel("transcript", html) && (initial || atBottom)) pre.scrollTop = pre.scrollHeight;
574
392
  }
575
393
 
394
+ function toggleTranscriptPrompts() {
395
+ transcriptPrompts = !transcriptPrompts;
396
+ const b = $("transcriptprompts");
397
+ if (b) b.textContent = transcriptPrompts ? "Hide prompts" : "Show prompts";
398
+ refreshTranscript(true);
399
+ }
400
+
401
+ function setTranscriptGrep(value) {
402
+ transcriptGrep = String(value ?? "").trim();
403
+ refreshTranscript(true);
404
+ }
405
+
576
406
  // A plan/bug cancel waits for its confirming click, the confirmAbort shape: the first click
577
407
  // arms it for 5 s, a second inside the window fires, and a timeout disarms. The slot records
578
408
  // the title too: a list that shifted between the clicks must re-arm, not cancel whatever
@@ -621,7 +451,7 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
621
451
  "<button type='button' class='btn btn-sm btn-primary' data-act='answer-record' data-arg='" + index + "'>" + icon("chat") + "Record answer</button>" +
622
452
  "<button type='button' class='btn btn-sm' data-act='answer' data-arg='" + index + "'>Ask the director instead</button>"
623
453
  : "<button type='button' class='btn btn-sm' data-act='mention' data-arg='" + esc(file + ":" + index) + "'>" + icon("chat") + "Ask the director about this</button>" +
624
- (d.refused || d.waiting ? "<button type='button' class='btn btn-sm btn-primary' data-act='entry-release' data-arg='" + esc(file + ":" + index) + "'>Release this entry</button>" : "") +
454
+ (d.refused || d.waiting || d.heldForOperator ? "<button type='button' class='btn btn-sm btn-primary' data-act='entry-release' data-arg='" + esc(file + ":" + index) + "'>Release this entry</button>" : "") +
625
455
  (index > 0 ? "<button type='button' class='btn btn-sm' data-act='entry-top' data-arg='" + esc(file + ":" + index) + "'>Move to top</button>" : "") +
626
456
  "<button type='button' class='btn btn-sm' data-act='entry-edit' data-arg='" + esc(file + ":" + index) + "'>Edit this entry</button>" +
627
457
  "<button type='button' class='btn btn-sm btn-danger' data-act='entry-cancel' data-arg='" + esc(file + ":" + index) + "'>" + (entryCancelArmed(file + ":" + index) ? "Click again to cancel" : "Cancel this entry") + "</button>";
@@ -632,15 +462,26 @@ export const GUI_CLIENT_DRAWER_JS = String.raw ` let drawer = null; // { kind:
632
462
 
633
463
  async function refreshDrawer() {
634
464
  if (!drawer) return;
635
- if (drawer.kind === "loop") { // vocabulary-ok: the drawer kind stays "loop"
636
- if (lastStatus) renderLoopDrawer(lastStatus);
465
+ if (drawer.kind === "role") {
466
+ if (lastStatus) renderRoleDrawer(lastStatus);
637
467
  await refreshTranscript(false);
638
468
  } else {
639
469
  await refreshEntry();
640
470
  }
641
471
  }
642
472
  $("scrim").addEventListener("click", closeDrawer);
473
+ $("drawer").addEventListener("input", (ev) => {
474
+ if (ev.target && ev.target.id === "transcriptgrep") setTranscriptGrep(ev.target.value);
475
+ });
643
476
  $("drawer").addEventListener("click", (ev) => {
477
+ if (clickClosest(ev, "button[data-transcript-prompts]")) {
478
+ toggleTranscriptPrompts();
479
+ return;
480
+ }
481
+ if (clickClosest(ev, "button[data-change-stat]")) {
482
+ toggleDrawerChangeStat();
483
+ return;
484
+ }
644
485
  const b = clickClosest(ev, "button.rowaction");
645
486
  if (b) rowAction(b.dataset.action, b.dataset.role, b.dataset.for === undefined ? undefined : Number(b.dataset.for));
646
487
  });