tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -47,207 +47,234 @@ export function roleInspectMarkdown(root, role) {
47
47
  * the head with the selected entry's full body in entry mode, usage/failures re-window
48
48
  * the cached Markdown set on view entry, and the activity pane keeps the event tail. */
49
49
  export function paneBody(pane, opts) {
50
- const { root, width, eventBudget, entryBudget, state } = opts;
51
- if (pane.kind === "transcript") {
52
- // The notes view (Ctrl+N) windows the cached inspection Markdown exactly like the
53
- // usage/failures panes, headings bold; the live transcript keeps the tail.
54
- if (pane.note) {
55
- const win = entryBodyWindow(state.noteCache ?? "", state.paneScroll, eventBudget, width);
56
- return {
57
- body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
58
- emptyNote: "(nothing to inspect yet)",
59
- };
60
- }
61
- // The change view (the second Ctrl+N step) windows the collected unmerged-change text the
62
- // same way; its async collector leaves the cache null for the first frames, so the pane
63
- // paints the loading note rather than entryBodyWindow's "no details" placeholder.
64
- if (pane.change) {
65
- if (state.changeCache === null)
66
- return { body: [], emptyNote: "(loading pending change…)" };
67
- const win = entryBodyWindow(state.changeCache, state.paneScroll, eventBudget, width);
68
- return {
69
- body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
70
- emptyNote: "(loading pending change…)",
71
- };
72
- }
73
- if (pane.grep) {
74
- // Filtered: scan the drawer's 200 entries with the shared keep rule (a lowercased needle).
75
- return {
76
- body: readTranscript(root, pane.role, 200, { grep: pane.grep.toLowerCase() }).slice(-eventBudget).map((l) => toneLine(l, width, transcriptTone(l))),
77
- emptyNote: `(no transcript entries match "${pane.grep}")`,
78
- };
79
- }
50
+ if (pane.kind === "transcript")
51
+ return transcriptBody(pane, opts);
52
+ if (pane.kind === "backlog")
53
+ return backlogBody(pane, opts);
54
+ if (pane.kind === "history")
55
+ return historyBody(pane, opts);
56
+ if (pane.kind === "pending")
57
+ return pendingBody(pane, opts);
58
+ if (pane.kind === "health")
59
+ return healthBody(pane, opts);
60
+ if (pane.kind === "brief")
61
+ return briefBody(pane, opts);
62
+ if (pane.kind === "queued")
63
+ return queuedBody(pane, opts);
64
+ if (pane.kind === "usage" || pane.kind === "failures")
65
+ return reportBody(pane, opts);
66
+ return activityBody(pane, opts);
67
+ }
68
+ function transcriptBody(pane, opts) {
69
+ const { root, width, eventBudget, state } = opts;
70
+ // The notes view (Ctrl+N) windows the cached inspection Markdown exactly like the
71
+ // usage/failures panes, headings bold; the live transcript keeps the tail.
72
+ if (pane.note) {
73
+ const win = entryBodyWindow(state.noteCache ?? "", state.paneScroll, eventBudget, width);
80
74
  return {
81
- body: readTranscript(root, pane.role, eventBudget).slice(-eventBudget).map((l) => toneLine(l, width, transcriptTone(l))),
82
- emptyNote: "(no transcript yet)",
75
+ body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
76
+ emptyNote: "(nothing to inspect yet)",
83
77
  };
84
78
  }
85
- if (pane.kind === "backlog") {
86
- // The flat, labeled entry list is the one backlog read this frame needs: it carries every
87
- // planned feature, open bug, and open question (then the newest answered questions, done
88
- // plans and fixed bugs)
89
- // from PLANS.md/BUGS.md/QUESTIONS.md with its section label and eligibility mark (the same
90
- // entryHold verdict `tumwater backlog` renders). List and entry mode share this one pass;
91
- // entry mode used to read the three sections and derive the holds directly and
92
- // then again inside the flat list, so every render paid twice for the same reads and verdicts.
93
- const flat = browsableBacklogEntries(root);
94
- // A `/` search narrows the browsable list to the matching open entries (the answered/done/
95
- // fixed rows are read-only and hidden while a search is applied); entry mode still opens
96
- // `flat[sel]`, the unfiltered index the selection holds.
97
- const grep = state.backlogGrep ?? null;
98
- const shown = flat.filter((e) => {
99
- if (grep !== null && !(isOpenBacklogLabel(e.label) && entryMatches(e, grep)))
100
- return false;
101
- if (state.backlogReadyOnly && isOpenBacklogLabel(e.label) && e.hold !== null)
102
- return false;
103
- return true;
104
- });
105
- // A stale selection (an entry removed since the last render) clamps to the last; no
106
- // entries at all falls back to list mode.
107
- const sel = state.selectedEntry === null || flat.length === 0 ? null : Math.min(state.selectedEntry, flat.length - 1);
108
- if (sel === null) {
109
- // List mode: section headings stand out, empty sections read dim. Keeps the HEAD of the
110
- // list when it overflows — file order is newest-first, unlike events which keep the tail.
111
- const listed = (label) => shown.filter((e) => e.label === label).map((e) => e.title + e.mark);
112
- // Plans list a series' blocked parts as one line (as `plan --list` does); the selection
113
- // below still walks the flat list, one entry per part.
114
- const plans = shown.filter((e) => e.label === "plan");
115
- // A `/` search active: the answered/done/fixed sections are hidden, the sections build
116
- // only from the matching open entries under a `search: "<grep>"` header, and nothing
117
- // matching paints a self-explanatory line.
118
- if (grep !== null) {
119
- const prefix = [`search: "${grep}"`];
120
- const rest = shown.length === 0
121
- ? [`(no entries match "${grep}")`]
122
- : backlogLines(planListLines(plans), listed("bug"), listed("question"), plans.length);
123
- return {
124
- body: [...prefix, ...rest]
125
- .slice(0, eventBudget)
126
- .map((l) => toneLine(l, width, /^(?:plans|open bugs|open questions) \(\d+\):$/.test(l) ? "bold" : l.startsWith("(") ? "dim" : undefined)),
127
- emptyNote: "(no events yet)",
128
- };
129
- }
130
- // The newest answered questions follow the open ones: they are in `flat` (up/down open
131
- // their bodies), but no edit key acts on them.
132
- const answered = answeredQuestionSummaries(root, DASHBOARD_ANSWERED_QUESTIONS).map((q) => q.decision ? `${q.title} — ${q.decision}` : q.title);
133
- // Done plans and fixed bugs follow, browsable and read-only the same way.
134
- const finishedRows = (rows, verb) => rows.map((r) => (r.completedOn ? `${r.title} — ${verb} ${r.completedOn}` : r.title));
135
- const finished = {
136
- done: finishedRows(recentFinished(donePlanEntries(root), DONE_DATE_RE), "done"),
137
- fixed: finishedRows(recentFinished(fixedBugEntries(root), FIXED_DATE_RE), "fixed"),
138
- };
139
- return {
140
- body: backlogLines(planListLines(plans), listed("bug"), listed("question"), plans.length, answered, finished)
141
- .slice(0, eventBudget)
142
- .map((l) => toneLine(l, width, /^(?:plans|open bugs|open questions|answered recently|done recently|fixed recently) \(\d+\):$/.test(l) ? "bold" : l.startsWith("(") ? "dim" : undefined)),
143
- emptyNote: "(no events yet)",
144
- };
145
- }
146
- // Entry browsing: the selected entry's full body under a line naming its section and
147
- // title.
148
- const e = flat[sel];
149
- const win = entryBodyWindow(e.body, state.entryScroll, entryBudget, width);
79
+ // The change view (the second Ctrl+N step) windows the collected unmerged-change text the
80
+ // same way; its async collector leaves the cache null for the first frames, so the pane
81
+ // paints the loading note rather than entryBodyWindow's "no details" placeholder.
82
+ if (pane.change) {
83
+ if (state.changeCache === null)
84
+ return { body: [], emptyNote: "(loading pending change…)" };
85
+ const win = entryBodyWindow(state.changeCache, state.paneScroll, eventBudget, width);
150
86
  return {
151
- body: [toneLine(`${e.label}: ${e.title}${e.mark}`, width, "bold"), ...win.lines.map((l) => toneLine(l, width))],
152
- emptyNote: "(no events yet)",
87
+ body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
88
+ emptyNote: "(loading pending change…)",
153
89
  };
154
90
  }
155
- if (pane.kind === "history") {
156
- // The run trail (Ctrl+N) windows the cached renderRunDetail text; a null cache (the
157
- // read found no run) paints the pane's own note rather than entryBodyWindow's placeholder.
158
- if (pane.trail) {
159
- if (state.trailCache === null)
160
- return { body: [], emptyNote: "(no run trail)" };
161
- const win = entryBodyWindow(state.trailCache, state.paneScroll, eventBudget, width);
162
- return {
163
- body: win.lines.map((l) => toneLine(l, width)),
164
- emptyNote: "(no run trail)",
165
- };
166
- }
167
- // historyPaneText returns "" when the log holds no completed runs; return no lines
168
- // (rather than windowing an empty string, which entryBodyWindow replaces with its
169
- // "no details" placeholder) so the frame prints the pane's own empty note instead.
170
- const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
91
+ if (pane.grep) {
92
+ // Filtered: scan the drawer's 200 entries with the shared keep rule (a lowercased needle).
171
93
  return {
172
- body: win.lines.map((l) => toneLine(l, width)),
173
- emptyNote: "(no runs yet)",
94
+ body: readTranscript(root, pane.role, 200, { grep: pane.grep.toLowerCase() }).slice(-eventBudget).map((l) => toneLine(l, width, transcriptTone(l))),
95
+ emptyNote: `(no transcript entries match "${pane.grep}")`,
174
96
  };
175
97
  }
176
- if (pane.kind === "pending") {
177
- // A selected queued merge shows its detail in place of the queue list; otherwise paneCache
178
- // stays null until the async collector resolves (the Ctrl+T handler kicks it off
179
- // fire-and-forget); like history, return no lines while null so the frame prints the
180
- // loading note rather than windowing an empty string into the "no details" placeholder.
181
- if (state.queueDetail) {
182
- const detail = state.queueDetail;
183
- const sel = entryBodyWindow(detail, state.paneScroll, eventBudget, width);
98
+ return {
99
+ body: readTranscript(root, pane.role, eventBudget).slice(-eventBudget).map((l) => toneLine(l, width, transcriptTone(l))),
100
+ emptyNote: "(no transcript yet)",
101
+ };
102
+ }
103
+ function backlogBody(_pane, opts) {
104
+ const { root, width, eventBudget, entryBudget, state } = opts;
105
+ // The flat, labeled entry list is the one backlog read this frame needs: it carries every
106
+ // planned feature, open bug, and open question (then the newest answered questions, done
107
+ // plans and fixed bugs)
108
+ // from PLANS.md/BUGS.md/QUESTIONS.md with its section label and eligibility mark (the same
109
+ // entryHold verdict `tumwater backlog` renders). List and entry mode share this one pass;
110
+ // entry mode used to read the three sections and derive the holds directly and
111
+ // then again inside the flat list, so every render paid twice for the same reads and verdicts.
112
+ const flat = browsableBacklogEntries(root);
113
+ // A `/` search narrows the browsable list to the matching open entries (the answered/done/
114
+ // fixed rows are read-only and hidden while a search is applied); entry mode still opens
115
+ // `flat[sel]`, the unfiltered index the selection holds.
116
+ const grep = state.backlogGrep ?? null;
117
+ const shown = flat.filter((e) => {
118
+ if (grep !== null && !(isOpenBacklogLabel(e.label) && entryMatches(e, grep)))
119
+ return false;
120
+ if (state.backlogReadyOnly && isOpenBacklogLabel(e.label) && e.hold !== null)
121
+ return false;
122
+ return true;
123
+ });
124
+ // A stale selection (an entry removed since the last render) clamps to the last; no
125
+ // entries at all falls back to list mode.
126
+ const sel = state.selectedEntry === null || flat.length === 0 ? null : Math.min(state.selectedEntry, flat.length - 1);
127
+ if (sel === null) {
128
+ // List mode: section headings stand out, empty sections read dim. Keeps the HEAD of the
129
+ // list when it overflows — file order is newest-first, unlike events which keep the tail.
130
+ const listed = (label) => shown.filter((e) => e.label === label).map((e) => e.title + e.mark);
131
+ // Plans list a series' blocked parts as one line (as `plan --list` does); the selection
132
+ // below still walks the flat list, one entry per part.
133
+ const plans = shown.filter((e) => e.label === "plan");
134
+ // A `/` search active: the answered/done/fixed sections are hidden, the sections build
135
+ // only from the matching open entries under a `search: "<grep>"` header, and nothing
136
+ // matching paints a self-explanatory line.
137
+ if (grep !== null) {
138
+ const prefix = [`search: "${grep}"`];
139
+ const rest = shown.length === 0
140
+ ? [`(no entries match "${grep}")`]
141
+ : backlogLines(planListLines(plans), listed("bug"), listed("question"), plans.length);
184
142
  return {
185
- body: sel.lines.map((l) => toneLine(l, width)),
186
- emptyNote: "(loading pending changes…)",
143
+ body: [...prefix, ...rest]
144
+ .slice(0, eventBudget)
145
+ .map((l) => toneLine(l, width, /^(?:plans|open bugs|open questions) \(\d+\):$/.test(l) ? "bold" : l.startsWith("(") ? "dim" : undefined)),
146
+ emptyNote: "(no events yet)",
187
147
  };
188
148
  }
189
- const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
149
+ // The newest answered questions follow the open ones: they are in `flat` (up/down open
150
+ // their bodies), but no edit key acts on them.
151
+ const answered = answeredQuestionSummaries(root, DASHBOARD_ANSWERED_QUESTIONS).map((q) => q.decision ? `${q.title} — ${q.decision}` : q.title);
152
+ // Done plans and fixed bugs follow, browsable and read-only the same way.
153
+ const finishedRows = (rows, verb) => rows.map((r) => (r.completedOn ? `${r.title} — ${verb} ${r.completedOn}` : r.title));
154
+ const finished = {
155
+ done: finishedRows(recentFinished(donePlanEntries(root), DONE_DATE_RE), "done"),
156
+ fixed: finishedRows(recentFinished(fixedBugEntries(root), FIXED_DATE_RE), "fixed"),
157
+ };
190
158
  return {
191
- body: win.lines.map((l) => toneLine(l, width)),
192
- emptyNote: "(loading pending changes…)",
159
+ body: backlogLines(planListLines(plans), listed("bug"), listed("question"), plans.length, answered, finished)
160
+ .slice(0, eventBudget)
161
+ .map((l) => toneLine(l, width, /^(?:plans|open bugs|open questions|answered recently|done recently|fixed recently) \(\d+\):$/.test(l) ? "bold" : l.startsWith("(") ? "dim" : undefined)),
162
+ emptyNote: "(no events yet)",
193
163
  };
194
164
  }
195
- if (pane.kind === "health") {
196
- const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
165
+ // Entry browsing: the selected entry's full body under a line naming its section and
166
+ // title.
167
+ const e = flat[sel];
168
+ const win = entryBodyWindow(e.body, state.entryScroll, entryBudget, width);
169
+ return {
170
+ body: [toneLine(`${e.label}: ${e.title}${e.mark}`, width, "bold"), ...win.lines.map((l) => toneLine(l, width))],
171
+ emptyNote: "(no events yet)",
172
+ };
173
+ }
174
+ function historyBody(pane, opts) {
175
+ const { width, eventBudget, state } = opts;
176
+ // The run trail (Ctrl+N) windows the cached renderRunDetail text; a null cache (the
177
+ // read found no run) paints the pane's own note rather than entryBodyWindow's placeholder.
178
+ if (pane.trail) {
179
+ if (state.trailCache === null)
180
+ return { body: [], emptyNote: "(no run trail)" };
181
+ const win = entryBodyWindow(state.trailCache, state.paneScroll, eventBudget, width);
197
182
  return {
198
183
  body: win.lines.map((l) => toneLine(l, width)),
199
- emptyNote: "(running health checks…)",
184
+ emptyNote: "(no run trail)",
200
185
  };
201
186
  }
202
- if (pane.kind === "brief") {
203
- const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
187
+ // historyPaneText returns "" when the log holds no completed runs; return no lines
188
+ // (rather than windowing an empty string, which entryBodyWindow replaces with its
189
+ // "no details" placeholder) so the frame prints the pane's own empty note instead.
190
+ const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
191
+ return {
192
+ body: win.lines.map((l) => toneLine(l, width)),
193
+ emptyNote: "(no runs yet)",
194
+ };
195
+ }
196
+ function pendingBody(_pane, opts) {
197
+ const { width, eventBudget, state } = opts;
198
+ // A selected queued merge shows its detail in place of the queue list; otherwise paneCache
199
+ // stays null until the async collector resolves (the Ctrl+T handler kicks it off
200
+ // fire-and-forget); like history, return no lines while null so the frame prints the
201
+ // loading note rather than windowing an empty string into the "no details" placeholder.
202
+ if (state.queueDetail) {
203
+ const detail = state.queueDetail;
204
+ const sel = entryBodyWindow(detail, state.paneScroll, eventBudget, width);
204
205
  return {
205
- body: win.lines.map((l) => toneLine(l, width)),
206
- emptyNote: "(no project brief with a prompt block — run `tumwater init` first)",
206
+ body: sel.lines.map((l) => toneLine(l, width)),
207
+ emptyNote: "(loading pending changes…)",
207
208
  };
208
209
  }
209
- if (pane.kind === "queued") {
210
- // A set selection shows the row's full prompt under a cancel header (the numbered list
211
- // clips long prompts); list mode windows the cached renderQueuedList string, which is
212
- // computed synchronously on Ctrl+T entry over the same queue `tumwater prompt --list`
213
- // prints, so paneCache is already set this frame. An empty queue falls to the pane's
214
- // own note either way.
215
- const sel = state.selectedQueued === null || state.queuedRows.length === 0
216
- ? null
217
- : Math.min(state.selectedQueued, state.queuedRows.length - 1);
218
- if (sel !== null) {
219
- const row = state.queuedRows[sel];
220
- const win = entryBodyWindow(row.text, state.paneScroll, entryBudget, width);
221
- return {
222
- body: [
223
- toneLine(`${row.role}'s inbox #${row.position} (Ctrl+X to cancel)`, width, "bold"),
224
- ...win.lines.map((l) => toneLine(l, width)),
225
- ],
226
- emptyNote: "(inbox empty)",
227
- };
228
- }
229
- // An applied search names itself above the list; one that matched nothing says so instead
230
- // of claiming the inbox is empty.
231
- const searchLine = pane.grep ? [toneLine(`search: "${pane.grep}"`, width, "bold")] : [];
232
- if (pane.grep && state.queuedRows.length === 0) {
233
- return { body: [...searchLine, toneLine(`(no queued prompt matches "${pane.grep}")`, width)], emptyNote: "(inbox empty)" };
234
- }
235
- const listBudget = Math.max(1, eventBudget - searchLine.length);
236
- const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, listBudget, width) : { lines: [] };
210
+ const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
211
+ return {
212
+ body: win.lines.map((l) => toneLine(l, width)),
213
+ emptyNote: "(loading pending changes…)",
214
+ };
215
+ }
216
+ function healthBody(_pane, opts) {
217
+ const { width, eventBudget, state } = opts;
218
+ const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
219
+ return {
220
+ body: win.lines.map((l) => toneLine(l, width)),
221
+ emptyNote: "(running health checks…)",
222
+ };
223
+ }
224
+ function briefBody(_pane, opts) {
225
+ const { width, eventBudget, state } = opts;
226
+ const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, eventBudget, width) : { lines: [] };
227
+ return {
228
+ body: win.lines.map((l) => toneLine(l, width)),
229
+ emptyNote: "(no project brief with a prompt block — run `tumwater init` first)",
230
+ };
231
+ }
232
+ function queuedBody(pane, opts) {
233
+ const { width, eventBudget, entryBudget, state } = opts;
234
+ // A set selection shows the row's full prompt under a cancel header (the numbered list
235
+ // clips long prompts); list mode windows the cached renderQueuedList string, which is
236
+ // computed synchronously on Ctrl+T entry over the same queue `tumwater prompt --list`
237
+ // prints, so paneCache is already set this frame. An empty queue falls to the pane's
238
+ // own note either way.
239
+ const sel = state.selectedQueued === null || state.queuedRows.length === 0
240
+ ? null
241
+ : Math.min(state.selectedQueued, state.queuedRows.length - 1);
242
+ if (sel !== null) {
243
+ const row = state.queuedRows[sel];
244
+ const win = entryBodyWindow(row.text, state.paneScroll, entryBudget, width);
237
245
  return {
238
- body: [...searchLine, ...win.lines.map((l) => toneLine(l, width))],
246
+ body: [
247
+ toneLine(`${row.role}'s inbox #${row.position} (Ctrl+X to cancel)`, width, "bold"),
248
+ ...win.lines.map((l) => toneLine(l, width)),
249
+ ],
239
250
  emptyNote: "(inbox empty)",
240
251
  };
241
252
  }
242
- if (pane.kind === "usage" || pane.kind === "failures") {
243
- // The Markdown `tumwater report`/`report --failures` prints, windowed like backlog entry
244
- // mode. The cache is set on view entry by the Ctrl+T handler, so this only re-windows a string.
245
- const win = entryBodyWindow(state.paneCache ?? "", state.paneScroll, eventBudget, width);
246
- return {
247
- body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
248
- emptyNote: "(no events yet)",
249
- };
253
+ // An applied search names itself above the list; one that matched nothing says so instead
254
+ // of claiming the inbox is empty.
255
+ const searchLine = pane.grep ? [toneLine(`search: "${pane.grep}"`, width, "bold")] : [];
256
+ if (pane.grep && state.queuedRows.length === 0) {
257
+ return { body: [...searchLine, toneLine(`(no queued prompt matches "${pane.grep}")`, width)], emptyNote: "(inbox empty)" };
250
258
  }
259
+ const listBudget = Math.max(1, eventBudget - searchLine.length);
260
+ const win = state.paneCache ? entryBodyWindow(state.paneCache, state.paneScroll, listBudget, width) : { lines: [] };
261
+ return {
262
+ body: [...searchLine, ...win.lines.map((l) => toneLine(l, width))],
263
+ emptyNote: "(inbox empty)",
264
+ };
265
+ }
266
+ function reportBody(_pane, opts) {
267
+ const { width, eventBudget, state } = opts;
268
+ // The Markdown `tumwater report`/`report --failures` prints, windowed like backlog entry
269
+ // mode. The cache is set on view entry by the Ctrl+T handler, so this only re-windows a string.
270
+ const win = entryBodyWindow(state.paneCache ?? "", state.paneScroll, eventBudget, width);
271
+ return {
272
+ body: win.lines.map((l) => toneLine(l, width, l.startsWith("#") ? "bold" : undefined)),
273
+ emptyNote: "(no events yet)",
274
+ };
275
+ }
276
+ function activityBody(pane, opts) {
277
+ const { root, width, eventBudget, state } = opts;
251
278
  // The Activity pane loads a fixed tail once per read, narrows it by the active role scope
252
279
  // and text search, and windows it: null follows the live tail (the newest eventBudget
253
280
  // events), a stored offset pages back through the loaded events. Both clamp to