toolroll 0.9.53 → 0.9.55

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 (254) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +5 -4
  3. package/dist/agent-onboard.js +4 -1
  4. package/dist/approval-rules-ui.d.ts +3 -2
  5. package/dist/approval-rules-ui.js +15 -20
  6. package/dist/assignment-ui.d.ts +3 -2
  7. package/dist/assignment-ui.js +4 -11
  8. package/dist/backup-ui.d.ts +2 -1
  9. package/dist/backup-ui.js +16 -27
  10. package/dist/brand-mark.d.ts +3 -2
  11. package/dist/brand-mark.js +4 -4
  12. package/dist/browser/workspace.css +1 -1
  13. package/dist/browser/workspace.js +25 -25
  14. package/dist/browser-shell.d.ts +2 -1
  15. package/dist/browser-shell.js +9 -9
  16. package/dist/browser-workspace.d.ts +2 -3
  17. package/dist/browser-workspace.js +0 -3
  18. package/dist/chat-ask.js +8 -8
  19. package/dist/chat-core.d.ts +171 -0
  20. package/dist/chat-core.js +288 -0
  21. package/dist/chat-decide.d.ts +10 -8
  22. package/dist/chat-decide.js +26 -21
  23. package/dist/chat-delivery-state.d.ts +51 -75
  24. package/dist/chat-delivery-state.js +140 -170
  25. package/dist/chat-delivery.d.ts +21 -2
  26. package/dist/chat-delivery.js +313 -194
  27. package/dist/chat-flow.d.ts +29 -1
  28. package/dist/chat-flow.js +24 -50
  29. package/dist/chat-messages.d.ts +319 -0
  30. package/dist/chat-messages.js +841 -0
  31. package/dist/chat-migration.d.ts +30 -0
  32. package/dist/chat-migration.js +802 -0
  33. package/dist/chat-polish.d.ts +5 -4
  34. package/dist/chat-polish.js +9 -10
  35. package/dist/chat-quiet.d.ts +3 -0
  36. package/dist/chat-quiet.js +5 -0
  37. package/dist/check-levels-ui.d.ts +2 -1
  38. package/dist/check-levels-ui.js +26 -53
  39. package/dist/cli.d.ts +3 -1
  40. package/dist/cli.js +9 -3
  41. package/dist/coding-shipping-ui.d.ts +2 -1
  42. package/dist/coding-shipping-ui.js +6 -6
  43. package/dist/coding-ui.d.ts +3 -1
  44. package/dist/coding-ui.js +30 -29
  45. package/dist/contracts/chat-content.d.ts +71 -2
  46. package/dist/contracts/chat-content.js +22 -3
  47. package/dist/contracts/chat-tables.d.ts +11 -4
  48. package/dist/contracts/chat-tables.js +165 -87
  49. package/dist/contracts/cli.d.ts +1 -3
  50. package/dist/contracts/console-api.d.ts +1 -1
  51. package/dist/contracts/console-api.js +6 -5
  52. package/dist/contracts/discord-callback.js +2 -1
  53. package/dist/contracts/health.d.ts +1 -3
  54. package/dist/contracts/slack-callback.js +3 -2
  55. package/dist/contracts/store-json.d.ts +1 -5
  56. package/dist/contracts/store-json.js +1 -4
  57. package/dist/contracts/teams-callback.js +2 -1
  58. package/dist/control-ui.d.ts +5 -4
  59. package/dist/control-ui.js +9 -34
  60. package/dist/credentials-ui.d.ts +5 -4
  61. package/dist/credentials-ui.js +23 -24
  62. package/dist/demo-chat.d.ts +3 -3
  63. package/dist/demo-chat.js +29 -76
  64. package/dist/discord-chat.js +19 -38
  65. package/dist/discord-settings.d.ts +3 -2
  66. package/dist/discord-settings.js +21 -55
  67. package/dist/discord.js +4 -14
  68. package/dist/envelope.js +10 -0
  69. package/dist/evidence-pack.d.ts +2 -1
  70. package/dist/evidence-pack.js +45 -49
  71. package/dist/export-ui.d.ts +7 -2
  72. package/dist/export-ui.js +4 -10
  73. package/dist/export.js +4 -4
  74. package/dist/flow-gallery-ui.d.ts +4 -3
  75. package/dist/flow-gallery-ui.js +28 -43
  76. package/dist/flow-starters-ui.d.ts +2 -1
  77. package/dist/flow-starters-ui.js +12 -15
  78. package/dist/flow-triggers.d.ts +1 -0
  79. package/dist/flow-triggers.js +16 -14
  80. package/dist/flows-ui.d.ts +4 -3
  81. package/dist/flows-ui.js +19 -30
  82. package/dist/flows.js +2 -1
  83. package/dist/guides.js +108 -3
  84. package/dist/html.d.ts +76 -0
  85. package/dist/html.js +173 -0
  86. package/dist/integrations-ui.d.ts +2 -1
  87. package/dist/integrations-ui.js +19 -23
  88. package/dist/kits-ui.d.ts +5 -4
  89. package/dist/kits-ui.js +18 -20
  90. package/dist/knowledge-ui.d.ts +6 -5
  91. package/dist/knowledge-ui.js +26 -25
  92. package/dist/lead-context.d.ts +11 -1
  93. package/dist/lead-context.js +6 -8
  94. package/dist/ledger-view.d.ts +2 -1
  95. package/dist/ledger-view.js +27 -29
  96. package/dist/limits-ui.d.ts +2 -1
  97. package/dist/limits-ui.js +7 -7
  98. package/dist/live-bus.d.ts +50 -8
  99. package/dist/live-bus.js +133 -50
  100. package/dist/mate-tools.js +3 -2
  101. package/dist/mcp-oauth.js +35 -36
  102. package/dist/models-ui.d.ts +2 -1
  103. package/dist/models-ui.js +25 -35
  104. package/dist/modes.d.ts +13 -1
  105. package/dist/modes.js +17 -1
  106. package/dist/monitoring-ui.d.ts +4 -3
  107. package/dist/monitoring-ui.js +13 -26
  108. package/dist/notification-rows.d.ts +30 -0
  109. package/dist/notification-rows.js +102 -0
  110. package/dist/openrouter-models.d.ts +2 -1
  111. package/dist/openrouter-models.js +5 -9
  112. package/dist/operate.js +16 -9
  113. package/dist/people-audit-ui.d.ts +2 -1
  114. package/dist/people-audit-ui.js +13 -13
  115. package/dist/plane-review.js +5 -11
  116. package/dist/policy-ui.d.ts +3 -2
  117. package/dist/policy-ui.js +17 -23
  118. package/dist/project-delete-ui.d.ts +4 -3
  119. package/dist/project-delete-ui.js +15 -27
  120. package/dist/project-delete.js +21 -26
  121. package/dist/pull-request-ui.d.ts +2 -1
  122. package/dist/pull-request-ui.js +17 -32
  123. package/dist/recipe-ui.d.ts +8 -7
  124. package/dist/recipe-ui.js +39 -32
  125. package/dist/release-adapters.d.ts +60 -0
  126. package/dist/release-adapters.js +435 -0
  127. package/dist/release-cli.d.ts +24 -0
  128. package/dist/release-cli.js +181 -0
  129. package/dist/release-gate.d.ts +80 -0
  130. package/dist/release-gate.js +150 -0
  131. package/dist/release-links.d.ts +29 -0
  132. package/dist/release-links.js +102 -0
  133. package/dist/release.d.ts +331 -0
  134. package/dist/release.js +550 -0
  135. package/dist/reply-shape.d.ts +2 -1
  136. package/dist/reply-shape.js +5 -5
  137. package/dist/repository-context-ui.d.ts +2 -1
  138. package/dist/repository-context-ui.js +2 -7
  139. package/dist/request-budget-ui.d.ts +2 -1
  140. package/dist/request-budget-ui.js +16 -16
  141. package/dist/result-review.d.ts +2 -3
  142. package/dist/result-review.js +3 -9
  143. package/dist/result-shots.js +2 -2
  144. package/dist/retention-ui.d.ts +7 -1
  145. package/dist/retention-ui.js +7 -11
  146. package/dist/retention.js +1 -1
  147. package/dist/serve.d.ts +11 -2
  148. package/dist/serve.js +69 -58
  149. package/dist/server/chat.d.ts +3 -6
  150. package/dist/server/chat.js +969 -1006
  151. package/dist/server/chrome.d.ts +291 -0
  152. package/dist/server/chrome.js +747 -0
  153. package/dist/server/console.css +2819 -0
  154. package/dist/server/flows.d.ts +2 -6
  155. package/dist/server/flows.js +840 -766
  156. package/dist/server/guards.d.ts +1 -1
  157. package/dist/server/guards.js +5 -2
  158. package/dist/server/handler-context.d.ts +1 -1
  159. package/dist/server/handler-context.js +1 -1
  160. package/dist/server/handler-registry.d.ts +8 -6
  161. package/dist/server/handler-registry.js +14 -2
  162. package/dist/server/http.d.ts +274 -0
  163. package/dist/server/http.js +239 -0
  164. package/dist/server/live.d.ts +34 -0
  165. package/dist/server/live.js +309 -0
  166. package/dist/server/pages.d.ts +2 -6
  167. package/dist/server/pages.js +808 -789
  168. package/dist/server/people-tokens.d.ts +1 -6
  169. package/dist/server/people-tokens.js +580 -572
  170. package/dist/server/remote.d.ts +4 -7
  171. package/dist/server/remote.js +37 -70
  172. package/dist/server/render-chat.d.ts +402 -0
  173. package/dist/server/render-chat.js +927 -0
  174. package/dist/server/render-pages.d.ts +535 -0
  175. package/dist/server/render-pages.js +1277 -0
  176. package/dist/server/render-people.d.ts +32 -0
  177. package/dist/server/render-people.js +121 -0
  178. package/dist/server/render-results.d.ts +744 -0
  179. package/dist/server/render-results.js +2237 -0
  180. package/dist/server/render-settings.d.ts +147 -0
  181. package/dist/server/render-settings.js +577 -0
  182. package/dist/server/render-tasks.d.ts +632 -0
  183. package/dist/server/render-tasks.js +2345 -0
  184. package/dist/server/request-context.d.ts +46 -0
  185. package/dist/server/request-context.js +17 -0
  186. package/dist/server/route-table.d.ts +25 -3
  187. package/dist/server/route-table.js +163 -75
  188. package/dist/server/runtime.d.ts +15 -7
  189. package/dist/server/runtime.js +7 -1
  190. package/dist/server/session.d.ts +138 -0
  191. package/dist/server/session.js +94 -0
  192. package/dist/server/settings.d.ts +1 -5
  193. package/dist/server/settings.js +2182 -2117
  194. package/dist/server/task-views.d.ts +11 -4
  195. package/dist/server/task-views.js +26 -22
  196. package/dist/server/tasks.d.ts +9 -9
  197. package/dist/server/tasks.js +1486 -1418
  198. package/dist/server-telemetry.d.ts +4 -3
  199. package/dist/server-telemetry.js +4 -8
  200. package/dist/skills-ui.d.ts +4 -3
  201. package/dist/skills-ui.js +14 -17
  202. package/dist/skills.d.ts +1 -1
  203. package/dist/skills.js +2 -1
  204. package/dist/slack-api.d.ts +5 -1
  205. package/dist/slack-api.js +2 -2
  206. package/dist/slack-chat.d.ts +9 -4
  207. package/dist/slack-chat.js +29 -52
  208. package/dist/slack-settings.d.ts +3 -2
  209. package/dist/slack-settings.js +23 -57
  210. package/dist/slack.js +6 -16
  211. package/dist/spend-ui.d.ts +2 -1
  212. package/dist/spend-ui.js +9 -32
  213. package/dist/sso-ui.d.ts +3 -2
  214. package/dist/sso-ui.js +26 -27
  215. package/dist/storage-ui.d.ts +2 -1
  216. package/dist/storage-ui.js +14 -33
  217. package/dist/store.d.ts +97 -364
  218. package/dist/store.js +207 -1531
  219. package/dist/surface.js +14 -3
  220. package/dist/task-composer.d.ts +2 -1
  221. package/dist/task-composer.js +7 -7
  222. package/dist/task-status.d.ts +4 -3
  223. package/dist/task-status.js +15 -16
  224. package/dist/team-http.d.ts +5 -9
  225. package/dist/team-http.js +5 -63
  226. package/dist/team-ui.d.ts +2 -1
  227. package/dist/team-ui.js +2 -5
  228. package/dist/teammate-question.js +12 -12
  229. package/dist/teammates-ui.d.ts +5 -4
  230. package/dist/teammates-ui.js +61 -90
  231. package/dist/teams-chat.js +12 -9
  232. package/dist/teams-settings.d.ts +3 -2
  233. package/dist/teams-settings.js +22 -33
  234. package/dist/teams.js +4 -5
  235. package/dist/telegram-flow.js +5 -5
  236. package/dist/telegram-settings.d.ts +3 -2
  237. package/dist/telegram-settings.js +14 -25
  238. package/dist/telegram.js +4 -4
  239. package/dist/toolroll-update-ui.d.ts +3 -2
  240. package/dist/toolroll-update-ui.js +35 -39
  241. package/dist/toolroll-update.d.ts +10 -0
  242. package/dist/toolroll-update.js +50 -3
  243. package/dist/tools-ui.d.ts +5 -2
  244. package/dist/tools-ui.js +31 -31
  245. package/dist/when-html.d.ts +11 -3
  246. package/dist/when-html.js +5 -6
  247. package/dist/workspace-revision.js +3 -1
  248. package/dist/workspace-ui.d.ts +4 -2
  249. package/dist/workspace-ui.js +10 -10
  250. package/package.json +1 -1
  251. package/dist/server/shared.d.ts +0 -3151
  252. package/dist/server/shared.js +0 -12001
  253. package/dist/slack-state.d.ts +0 -13
  254. package/dist/slack-state.js +0 -10
@@ -1,5 +1,6 @@
1
1
  /** Live discovery for the UI. Prices are USD, separate from rounded budget pins. */
2
2
  import { createHash } from "node:crypto";
3
+ import { html } from "./html.js";
3
4
  import { readCappedBody } from "./converse.js";
4
5
  import { validModelId } from "./provider.js";
5
6
  const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -76,7 +77,6 @@ export function openRouterModelsCache(fetcher = fetch) {
76
77
  return value;
77
78
  };
78
79
  }
79
- const escape = (s) => s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
80
80
  export function modelDollars(amount) {
81
81
  if (amount === null)
82
82
  return "Not reported";
@@ -96,14 +96,10 @@ function details(model) {
96
96
  export function openRouterPicker(catalog, selected, refreshUrl) {
97
97
  const rows = catalog.ok ? catalog.models : [];
98
98
  const current = rows.find(row => row.id === selected);
99
- const manual = selected && !current ? `<option selected value="${escape(selected)}" data-detail="This saved model is not in the loaded catalog. Availability and pricing are unverified.">${escape(selected)} · Saved choice</option>` : "";
100
- return `<div data-openrouter-picker><p class="meta">${catalog.ok
101
- ? `${rows.length} text models · ${catalog.source === "account" ? "Filtered for your OpenRouter account" : "Public catalog · Connect a key to check your account"} · Updated ${escape(catalog.checkedAt.slice(11, 16))} UTC`
102
- : escape(catalog.problem)} <a href="${escape(refreshUrl)}">Refresh models</a></p>` +
103
- `<label data-model-search hidden>Search models<input type="search" placeholder="Search by name or provider" autocomplete="off"></label>` +
104
- `<label>Model<select name="model" aria-describedby="openrouter-price"><option value="__custom__"${selected === null ? " selected" : ""}>Choose a model</option>${manual}${rows.map(row => `<option value="${escape(row.id)}" data-detail="${escape(details(row))}"${row.id === selected ? " selected" : ""}>${escape(row.name)} · ${modelDollars(row.input)} in / ${modelDollars(row.output)} out per 1M</option>`).join("")}</select></label>` +
105
- `<p id="openrouter-price" class="meta" aria-live="polite">${current ? escape(details(current)) : selected ? "This saved model is not in the loaded catalog. Availability and pricing are unverified." : "Choose a model to see its token prices and context size."}</p>` +
106
- `<p data-model-count class="meta" aria-live="polite"></p><p class="meta"><a href="https://openrouter.ai/models" target="_blank" rel="noreferrer">Compare models on OpenRouter ↗</a> · Prices in USD; catalog browsing uses no model tokens.</p></div>`;
99
+ const manual = selected && !current ? html `<option selected value="${selected}" data-detail="This saved model is not in the loaded catalog. Availability and pricing are unverified.">${selected} · Saved choice</option>` : "";
100
+ return html `<div data-openrouter-picker><p class="meta">${catalog.ok
101
+ ? `${rows.length} text models · ${catalog.source === "account" ? "Filtered for your OpenRouter account" : "Public catalog · Connect a key to check your account"} · Updated ${catalog.checkedAt.slice(11, 16)} UTC`
102
+ : catalog.problem} <a href="${refreshUrl}">Refresh models</a></p><label data-model-search hidden>Search models<input type="search" placeholder="Search by name or provider" autocomplete="off"></label><label>Model<select name="model" aria-describedby="openrouter-price"><option value="__custom__"${selected === null ? html ` selected` : ""}>Choose a model</option>${manual}${rows.map(row => html `<option value="${row.id}" data-detail="${details(row)}"${row.id === selected ? html ` selected` : ""}>${row.name} · ${modelDollars(row.input)} in / ${modelDollars(row.output)} out per 1M</option>`)}</select></label><p id="openrouter-price" class="meta" aria-live="polite">${current ? details(current) : selected ? "This saved model is not in the loaded catalog. Availability and pricing are unverified." : "Choose a model to see its token prices and context size."}</p><p data-model-count class="meta" aria-live="polite"></p><p class="meta"><a href="https://openrouter.ai/models" target="_blank" rel="noreferrer">Compare models on OpenRouter ↗</a> · Prices in USD; catalog browsing uses no model tokens.</p></div>`;
107
103
  }
108
104
  /** Only filters already-rendered options; credentials never enter this script. */
109
105
  export function openRouterPickerScript() {
package/dist/operate.js CHANGED
@@ -175,7 +175,7 @@ import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordina
175
175
  import { serveMcp } from "./mcp.js";
176
176
  import { createInterface } from "node:readline";
177
177
  import { propose, approve, addApprover, authenticateAccount, authenticateApprover as passwordApprover, describeScope, approvalOf, hashToken as hashApproverToken, fileAndSealUnderMode, modeFilingCoverage, acceptanceLinesToInput, parseAcceptanceCriteria, splitAcceptanceRubric, rubricIsPlaceholder, isCommitSha } from "./scope.js";
178
- import { presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
178
+ import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFromJson, modeWords, MODE_MAX_DAYS } from "./modes.js";
179
179
  import { WorktreePool } from "./worktree.js";
180
180
  import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
181
181
  import { worktreeAdoptionNotice } from "./worktree-notices.js";
@@ -370,7 +370,7 @@ External trackers — build what a tracker nominates, under local approvals
370
370
 
371
371
  toolroll claim <id> --runner <name> [--ttl <seconds>]
372
372
  toolroll heartbeat <lease> still working; extends the lease
373
- toolroll release <lease> done with it; fenced if superseded
373
+ toolroll worker release <lease> done with it; fenced if superseded
374
374
  toolroll reap release every lease that ran out
375
375
 
376
376
  toolroll tick --runner <name> --token <t> --repo <path>
@@ -1143,8 +1143,10 @@ async function dispatch(command, positional, flags, context) {
1143
1143
  return claimCommand(positional, flags, context);
1144
1144
  case "heartbeat":
1145
1145
  return leaseCommand("heartbeat", positional, flags, context);
1146
- case "release":
1147
- return leaseCommand("release", positional, flags, context);
1146
+ case "worker":
1147
+ if (positional[0] === "release")
1148
+ return leaseCommand("release", positional.slice(1), flags, context);
1149
+ return fail(context.write, context.json, "worker", "usage", "`toolroll worker release <lease>` returns a lease.", EXIT.usage);
1148
1150
  case "reap":
1149
1151
  return reapCommand(context);
1150
1152
  case "runner":
@@ -1476,18 +1478,20 @@ function claimCommand(positional, flags, context) {
1476
1478
  */
1477
1479
  function leaseCommand(command, positional, flags, context) {
1478
1480
  const { store, write, json, now } = context;
1481
+ // `release` answers as the worker verb it now is (the bare `toolroll release <lease>` form is deprecated).
1482
+ const name = command === "release" ? "worker release" : command;
1479
1483
  const lease = positional[0];
1480
1484
  if (lease === undefined)
1481
- return fail(write, json, command, "usage", `which lease? \`toolroll ${command} <lease>\``, EXIT.usage);
1485
+ return fail(write, json, name, "usage", `which lease? \`toolroll ${name} <lease>\``, EXIT.usage);
1482
1486
  const ttl = readTtl(flags);
1483
1487
  if (ttl === null)
1484
- return fail(write, json, command, "usage", "--ttl takes whole seconds", EXIT.usage);
1488
+ return fail(write, json, name, "usage", "--ttl takes whole seconds", EXIT.usage);
1485
1489
  const result = command === "heartbeat" ? heartbeat(store, lease, now, ttl) : release(store, lease, now);
1486
1490
  if (!result.ok) {
1487
1491
  const message = result.reason === "fenced"
1488
1492
  ? "superseded — another runner holds this task now; stop rather than retry"
1489
1493
  : "no such lease";
1490
- return fail(write, json, command, result.reason, message, EXIT.refused);
1494
+ return fail(write, json, name, result.reason, message, EXIT.refused);
1491
1495
  }
1492
1496
  if (command === "release") {
1493
1497
  const task = store.getTask(String(refExternalId(store, result.claim.taskRef)));
@@ -1496,7 +1500,7 @@ function leaseCommand(command, positional, flags, context) {
1496
1500
  if (task !== null && task.state === "running")
1497
1501
  store.setTaskState(task.id, "queued", now);
1498
1502
  }
1499
- return succeed(write, json, command, { lease: result.claim }, () => [
1503
+ return succeed(write, json, name, { lease: result.claim }, () => [
1500
1504
  command === "heartbeat"
1501
1505
  ? `Still yours until ${result.claim.expiresAt}.`
1502
1506
  : `Released ${result.claim.leaseId}.`,
@@ -4659,8 +4663,11 @@ async function modeCommand(positional, flags, context) {
4659
4663
  return fail(write, json, "mode set", "invalid", "--plan-auto requires automatic filing approval", EXIT.refused);
4660
4664
  }
4661
4665
  // Approving plans and merges from the signer's paired chat: never a preset default, only this flag.
4662
- if (flag(flags, "chat-approve"))
4666
+ // The signature names every chat app it covers: a grant signed before that named Telegram alone, and stays so.
4667
+ if (flag(flags, "chat-approve")) {
4663
4668
  terms.chatApprove = true;
4669
+ terms.chatApproveChats = CHAT_APPROVE_ALL;
4670
+ }
4664
4671
  // Automerge requires a live merge-capable grant on the repo (D1).
4665
4672
  if (terms.publication === "automerge" && !store.hasMergeCapableGrant(repo, now)) {
4666
4673
  return fail(write, json, "mode set", "no-grant", "automerge needs a merge-capable publication grant on this repo — file one first", EXIT.refused);
@@ -1,5 +1,6 @@
1
1
  import { type AuditAction } from "./remote-audit.js";
2
2
  import type { ApiTokenRow } from "./store.js";
3
+ import { type Html } from "./html.js";
3
4
  export declare const PEOPLE_AUDIT_CSS: string;
4
5
  export type PersonAuditView = {
5
6
  name: string;
@@ -11,4 +12,4 @@ export type PersonAuditView = {
11
12
  now: Date;
12
13
  };
13
14
  export declare const personHref: (name: string, more?: Record<string, string>) => string;
14
- export declare function personAuditHtml(view: PersonAuditView): string;
15
+ export declare function personAuditHtml(view: PersonAuditView): Html;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { basename } from "node:path";
7
7
  import { tokenStatus } from "./remote-audit.js";
8
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
8
+ import { html } from "./html.js";
9
9
  const stamp = (value) => `${value.slice(0, 16).replace("T", " ")} UTC`;
10
10
  export const PEOPLE_AUDIT_CSS = `.person-audit{max-width:820px;min-width:0;overflow-wrap:anywhere}.person-audit h2{font-size:1.05rem;margin:1.25rem 0 8px}` +
11
11
  `.person-audit .rows{display:grid;border:1px solid var(--so-line);border-radius:10px;overflow:hidden}.person-audit .row-item{padding:10px 14px;border-bottom:1px solid var(--so-line);min-width:0}` +
@@ -17,22 +17,22 @@ export const PEOPLE_AUDIT_CSS = `.person-audit{max-width:820px;min-width:0;overf
17
17
  `@media(max-width:600px){.person-audit .row-item a{min-height:44px}}`;
18
18
  export const personHref = (name, more = {}) => `/people?${new URLSearchParams({ person: name, ...more }).toString()}`;
19
19
  export function personAuditHtml(view) {
20
- const tokens = view.tokens.length === 0 ? `<p class="meta">No API tokens.</p>` : `<div class="rows">${view.tokens.map(one => {
20
+ const tokens = view.tokens.length === 0 ? html `<p class="meta">No API tokens.</p>` : html `<div class="rows">${view.tokens.map(one => {
21
21
  const status = tokenStatus(one, view.now);
22
- return `<div class="row-item" data-token-name="${e(one.name)}"><p><a href="${e(personHref(view.name, { "token-name": one.name }))}">${e(one.name)}</a></p>` +
23
- `<p class="meta">${status === "active" ? "Active" : status === "expired" ? "Expired" : "Revoked"} · ${one.access === "act" ? "Can act" : "Read only"} · ` +
24
- `last used ${one.lastUsedAt === null ? "never" : e(stamp(one.lastUsedAt))}</p></div>`;
25
- }).join("")}</div>`;
26
- const filtered = view.token === null ? "" : `<p class="meta">With ${e(view.token)}</p><p><a href="${e(personHref(view.name))}">All tokens</a></p>`;
27
- const actions = view.actions.length === 0 ? `<p class="meta">No remote actions${view.token === null ? "" : " with this token"}.</p>` : `<div class="rows">${view.actions.map(actionHtml).join("")}</div>`;
28
- const older = view.nextCursor === null ? "" : `<p><a href="${e(personHref(view.name, { ...(view.token === null ? {} : { "token-name": view.token }), before: view.nextCursor }))}">Older actions</a></p>`;
29
- return `<p><a href="/people">People</a></p><h1>${e(view.name)}</h1><section class="person-audit"><h2>API tokens</h2>${tokens}` +
30
- `<h2>Remote activity</h2>${filtered}${actions}${older}</section>`;
22
+ return html `<div class="row-item" data-token-name="${one.name}"><p><a href="${personHref(view.name, { "token-name": one.name })}">${one.name}</a></p>\
23
+ <p class="meta">${status === "active" ? "Active" : status === "expired" ? "Expired" : "Revoked"} · ${one.access === "act" ? "Can act" : "Read only"} · \
24
+ last used ${one.lastUsedAt === null ? "never" : stamp(one.lastUsedAt)}</p></div>`;
25
+ })}</div>`;
26
+ const filtered = view.token === null ? "" : html `<p class="meta">With ${view.token}</p><p><a href="${personHref(view.name)}">All tokens</a></p>`;
27
+ const actions = view.actions.length === 0 ? html `<p class="meta">No remote actions${view.token === null ? "" : " with this token"}.</p>` : html `<div class="rows">${view.actions.map(actionHtml)}</div>`;
28
+ const older = view.nextCursor === null ? "" : html `<p><a href="${personHref(view.name, { ...(view.token === null ? {} : { "token-name": view.token }), before: view.nextCursor })}">Older actions</a></p>`;
29
+ return html `<p><a href="/people">People</a></p><h1>${view.name}</h1><section class="person-audit"><h2>API tokens</h2>${tokens}\
30
+ <h2>Remote activity</h2>${filtered}${actions}${older}</section>`;
31
31
  }
32
32
  function actionHtml(action) {
33
33
  const label = action.outcome === "ok" ? "OK" : action.outcome === "refused" ? "Refused" : "Error";
34
34
  const detail = [stamp(action.at), action.token ?? "unknown token", action.source === "mcp" ? "Agent (MCP)" : "CLI",
35
35
  ...(action.repo === null ? [] : [basename(action.repo)]), ...(action.reason === null ? [] : [action.reason])];
36
- return `<div class="row-item" data-action="${action.id}"><p><span class="outcome outcome-${action.outcome}">${label}</span>${e(action.name)}` +
37
- `${action.taskId === null ? "" : ` · <a href="/t/${encodeURIComponent(action.taskId)}">${e(action.taskId)}</a>`}</p><p class="meta">${detail.map(e).join(" · ")}</p></div>`;
36
+ return html `<div class="row-item" data-action="${action.id}"><p><span class="outcome outcome-${action.outcome}">${label}</span>${action.name}\
37
+ ${action.taskId === null ? "" : html ` · <a href="/t/${encodeURIComponent(action.taskId)}">${action.taskId}</a>`}</p><p class="meta">${detail.join(" · ")}</p></div>`;
38
38
  }
@@ -175,17 +175,11 @@ export function reviewPlane(store, now, canSee = () => true) {
175
175
  }
176
176
  }
177
177
  // Chat replies that couldn't be delivered.
178
- for (const app of ["slack", "discord", "teams"]) {
179
- if (!tableExists(store, `${app}_part`))
180
- continue;
181
- const name = { slack: "Slack", discord: "Discord", teams: "Teams" }[app];
182
- for (const row of store.handle.prepare(`SELECT id, problem, attempts FROM ${app}_part WHERE state = 'dropped' AND created >= ? ORDER BY id`).all(since)) {
183
- add(`chat/${app}`, `${name} replies weren't delivered`, ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["problem"])) || "dropped"}` });
184
- }
185
- }
186
- if (tableExists(store, "telegram_conversation_part")) {
187
- for (const row of store.handle.prepare("SELECT last_error, attempts FROM telegram_conversation_part WHERE state = 'dropped' AND created_at >= ?").all(since)) {
188
- add("chat/telegram", "Telegram replies weren't delivered", ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["last_error"])) || "dropped"}` });
178
+ if (tableExists(store, "chat_part")) {
179
+ const names = { telegram: "Telegram", slack: "Slack", discord: "Discord", teams: "Teams" };
180
+ for (const row of store.handle.prepare("SELECT provider, problem, attempts FROM chat_part WHERE state = 'dropped' AND created >= ? ORDER BY provider = 'telegram' DESC, provider, id").all(since)) {
181
+ const app = String(row["provider"]);
182
+ add(`chat/${app}`, `${names[app] ?? app} replies weren't delivered`, ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["problem"])) || "dropped"}` });
189
183
  }
190
184
  }
191
185
  if (tableExists(store, "notification_delivery")) {
@@ -4,6 +4,7 @@
4
4
  * action ledger. Anyone signed in reads it; an instance operator changes it
5
5
  * with their password.
6
6
  */
7
+ import { type Html } from "./html.js";
7
8
  import type { LedgerEntry } from "./action-ledger.js";
8
9
  import { type SavedPolicy } from "./policy.js";
9
10
  export declare const POLICY_CSS: string;
@@ -14,7 +15,7 @@ export type PolicyView = {
14
15
  /** The project tools here, as a hint for the tools list. */
15
16
  toolNames: string[];
16
17
  };
17
- export declare function policyHtml(view: PolicyView, csrf: string, notice: {
18
+ export declare function policyHtml(view: PolicyView, notice: {
18
19
  said?: string | null;
19
20
  problem?: string | null;
20
- }): string;
21
+ }): Html;
package/dist/policy-ui.js CHANGED
@@ -1,6 +1,12 @@
1
+ /**
2
+ * Settings → Policy (sprint 8): the organisation policy — allowed providers,
3
+ * models and tools, and the permission ceiling — and its history from the
4
+ * action ledger. Anyone signed in reads it; an instance operator changes it
5
+ * with their password.
6
+ */
7
+ import { html, postForm } from "./html.js";
1
8
  import { whenUtc } from "./when-html.js";
2
9
  import { LEVEL_HINTS, LEVEL_NAMES, PERMISSION_LEVELS, POLICY_PROVIDERS, PROVIDER_NAMES, policyParts } from "./policy.js";
3
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
4
10
  export const POLICY_CSS = `.policy{max-width:720px;min-width:0}.policy dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:6px 16px;margin:12px 0 0;font-size:.875rem}` +
5
11
  `.policy dt{color:var(--muted-foreground)}.policy dd{margin:0;overflow-wrap:anywhere}.policy fieldset{border:0;padding:0;margin:20px 0 0;min-width:0}` +
6
12
  `.policy legend{font-weight:600;font-size:.875rem;margin-bottom:8px}.policy legend{padding:0}.policy .providers{display:grid;grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:0 16px}.policy .providers label{display:flex;gap:8px;align-items:center;min-height:44px;margin:0;padding:0;font-weight:400}` +
@@ -13,30 +19,18 @@ export const POLICY_CSS = `.policy{max-width:720px;min-width:0}.policy dl{displa
13
19
  function historyItem(entry) {
14
20
  const what = entry.action.replace(/^organisation policy: /, "");
15
21
  const [before, after] = (entry.detail ?? "").split(" → ");
16
- return `<li><span class="what">${e(what.charAt(0).toUpperCase() + what.slice(1))}: ${e(before ?? "")} → <strong>${e(after ?? "")}</strong></span>` +
17
- `<span class="meta">${e(entry.actor)} · ${whenUtc(entry.at)}</span></li>`;
22
+ return html `<li><span class="what">${what.charAt(0).toUpperCase() + what.slice(1)}: ${before ?? ""} → <strong>${after ?? ""}</strong></span><span class="meta">${entry.actor} · ${whenUtc(entry.at)}</span></li>`;
18
23
  }
19
- export function policyHtml(view, csrf, notice) {
20
- const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
24
+ export function policyHtml(view, notice) {
25
+ const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
21
26
  const parts = policyParts(view.policy);
22
- const changed = view.policy.updatedBy === null ? "" : `<p class="meta">Changed by ${e(view.policy.updatedBy)} on ${e((view.policy.updatedAt ?? "").slice(0, 10))}.</p>`;
23
- const current = `<dl><dt>Providers</dt><dd>${e(parts.providers === "any" ? "Any" : parts.providers)}</dd><dt>Models</dt><dd>${e(parts.models === "any" ? "Any" : parts.models)}</dd>` +
24
- `<dt>Tools</dt><dd>${e(parts.tools === "any" ? "Any" : parts.tools)}</dd><dt>Ceiling</dt><dd>${e(parts.ceiling)}</dd></dl>${changed}`;
25
- const history = view.history.length === 0 ? "" : `<details><summary>History (${view.history.length})</summary><ol>${view.history.map(historyItem).join("")}</ol></details>`;
27
+ const changed = view.policy.updatedBy === null ? "" : html `<p class="meta">Changed by ${view.policy.updatedBy} on ${(view.policy.updatedAt ?? "").slice(0, 10)}.</p>`;
28
+ const current = html `<dl><dt>Providers</dt><dd>${parts.providers === "any" ? "Any" : parts.providers}</dd><dt>Models</dt><dd>${parts.models === "any" ? "Any" : parts.models}</dd><dt>Tools</dt><dd>${parts.tools === "any" ? "Any" : parts.tools}</dd><dt>Ceiling</dt><dd>${parts.ceiling}</dd></dl>${changed}`;
29
+ const history = view.history.length === 0 ? "" : html `<details><summary>History (${view.history.length})</summary><ol>${view.history.map(historyItem)}</ol></details>`;
26
30
  if (!view.canChange)
27
- return `<section class="policy">${note}${current}<p class="meta">An instance operator sets the policy.</p>${history}</section>`;
28
- const providers = POLICY_PROVIDERS.map(one => `<label><input type="checkbox" name="provider" value="${one}"${view.policy.providers === null || view.policy.providers.includes(one) ? " checked" : ""}> ${PROVIDER_NAMES[one]}</label>`).join("");
29
- const levels = [...PERMISSION_LEVELS].reverse().map(level => `<label class="choice"><input type="radio" name="ceiling" value="${level}"${view.policy.ceiling === level ? " checked" : ""}>` +
30
- `<span><strong>${LEVEL_NAMES[level]}</strong><small>${e(LEVEL_HINTS[level])}</small></span></label>`).join("");
31
+ return html `<section class="policy">${note}${current}<p class="meta">An instance operator sets the policy.</p>${history}</section>`;
32
+ const providers = POLICY_PROVIDERS.map(one => html `<label><input type="checkbox" name="provider" value="${one}"${view.policy.providers === null || view.policy.providers.includes(one) ? html ` checked` : ""}> ${PROVIDER_NAMES[one]}</label>`);
33
+ const levels = [...PERMISSION_LEVELS].reverse().map(level => html `<label class="choice"><input type="radio" name="ceiling" value="${level}"${view.policy.ceiling === level ? html ` checked` : ""}><span><strong>${LEVEL_NAMES[level]}</strong><small>${LEVEL_HINTS[level]}</small></span></label>`);
31
34
  const toolHint = view.toolNames.length === 0 ? "" : ` Tools here: ${view.toolNames.slice(0, 12).join(", ")}${view.toolNames.length > 12 ? "…" : ""}.`;
32
- return `<section class="policy">${note}${changed}` +
33
- `<form method="post" action="/settings/policy"><input type="hidden" name="csrf" value="${e(csrf)}">` +
34
- `<fieldset><legend>Providers</legend><div class="providers">${providers}</div></fieldset>` +
35
- `<fieldset><legend><label for="policy-models">Models</label></legend><textarea id="policy-models" name="models" spellcheck="false" placeholder="Any model" aria-describedby="policy-models-hint">${e((view.policy.models ?? []).join("\n"))}</textarea>` +
36
- `<span class="hint" id="policy-models-hint">One per line; end with * to allow a family. Blank allows any.</span></fieldset>` +
37
- `<fieldset><legend><label for="policy-tools">Project tools</label></legend><textarea id="policy-tools" name="tools" spellcheck="false" placeholder="Any tool" aria-describedby="policy-tools-hint">${e((view.policy.tools ?? []).join("\n"))}</textarea>` +
38
- `<span class="hint" id="policy-tools-hint">One per line. Blank allows any.${e(toolHint)}</span></fieldset>` +
39
- `<fieldset><legend>Permission ceiling</legend>${levels}</fieldset>` +
40
- `<div class="step-up"><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label></div>` +
41
- `<button type="submit">Save policy</button></form>${history}</section>`;
35
+ return html `<section class="policy">${note}${changed}${postForm("/settings/policy", html `<fieldset><legend>Providers</legend><div class="providers">${providers}</div></fieldset><fieldset><legend><label for="policy-models">Models</label></legend><textarea id="policy-models" name="models" spellcheck="false" placeholder="Any model" aria-describedby="policy-models-hint">${(view.policy.models ?? []).join("\n")}</textarea><span class="hint" id="policy-models-hint">One per line; end with * to allow a family. Blank allows any.</span></fieldset><fieldset><legend><label for="policy-tools">Project tools</label></legend><textarea id="policy-tools" name="tools" spellcheck="false" placeholder="Any tool" aria-describedby="policy-tools-hint">${(view.policy.tools ?? []).join("\n")}</textarea><span class="hint" id="policy-tools-hint">One per line. Blank allows any.${toolHint}</span></fieldset><fieldset><legend>Permission ceiling</legend>${levels}</fieldset><div class="step-up"><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label></div><button type="submit">Save policy</button>`)}${history}</section>`;
42
36
  }
@@ -4,6 +4,7 @@
4
4
  * then read exactly what goes and confirm with their password (or a fresh
5
5
  * sign-in with the identity provider). The repository itself stays.
6
6
  */
7
+ import { type Html } from "./html.js";
7
8
  import { type ProjectHoldings } from "./project-delete.js";
8
9
  export declare const PROJECT_DELETE_CSS: string;
9
10
  export type ProjectSettingsView = {
@@ -21,9 +22,9 @@ export type ProjectSettingsView = {
21
22
  };
22
23
  };
23
24
  /** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
24
- export declare function projectSettingsHtml(view: ProjectSettingsView, csrf: string, notice: {
25
+ export declare function projectSettingsHtml(view: ProjectSettingsView, notice: {
25
26
  said?: string | null;
26
27
  problem?: string | null;
27
- }): string;
28
+ }): Html;
28
29
  /** The second step: exactly what goes, and the password. */
29
- export declare function projectDeleteConfirmHtml(view: ProjectSettingsView, csrf: string, problem: string | null): string;
30
+ export declare function projectDeleteConfirmHtml(view: ProjectSettingsView, problem: string | null): Html;
@@ -4,8 +4,8 @@
4
4
  * then read exactly what goes and confirm with their password (or a fresh
5
5
  * sign-in with the identity provider). The repository itself stays.
6
6
  */
7
+ import { html, postForm } from "./html.js";
7
8
  import { holdingsWords } from "./project-delete.js";
8
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
9
9
  export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:720px;min-width:0}.project-settings .path,.project-delete .path{font-family:var(--font-mono);font-size:.8125rem;overflow-wrap:anywhere;word-break:break-all}` +
10
10
  `.project-settings details.danger-zone{margin-top:24px;border:1px solid var(--border);border-radius:10px;padding:0 14px}.project-settings details.danger-zone summary{min-height:44px;display:flex;align-items:center;cursor:pointer;font-weight:500;color:var(--danger)}` +
11
11
  `.project-settings details.danger-zone[open]{padding-bottom:14px}.project-settings form,.project-delete form{display:grid;gap:12px;margin-top:12px}.project-settings label,.project-delete label{display:grid;gap:4px;font-size:.8125rem;font-weight:500}` +
@@ -16,39 +16,32 @@ export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:7
16
16
  `.project-settings .builds-at-once .builds-row{display:flex;gap:8px;align-items:stretch}.project-settings .builds-at-once input{width:5.5rem;min-height:44px;margin:0;box-sizing:border-box}.project-settings .builds-at-once button{min-height:44px;margin:0}` +
17
17
  `.project-settings .builds-at-once .meta{margin:0}.project-settings form.remove-project{margin-top:20px;gap:4px}.project-settings form.remove-project button{min-height:44px}.project-settings form.remove-project .meta{margin:0}`;
18
18
  /** Builds at once: one number, one Save. The worker's cap is said only when it lowers the number. */
19
- function buildsHtml(view, csrf) {
19
+ function buildsHtml(view) {
20
20
  const b = view.builds;
21
21
  if (b === undefined)
22
22
  return "";
23
23
  const capped = b.capacity !== null && b.capacity < b.setting ? ` Its worker runs ${b.capacity} at once, so ${b.capacity} for now.` : "";
24
24
  const now = `${b.building} building now.${capped}`;
25
25
  if (!b.canChange)
26
- return `<section class="builds-at-once"><h2>Builds at once</h2><p>Up to ${b.setting}. ${e(now)}</p></section>`;
27
- return `<section class="builds-at-once"><h2>Builds at once</h2><form method="post" action="/settings/project/concurrency">` +
28
- `<input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="repo" value="${e(view.repo)}">` +
29
- `<div class="builds-row"><input type="number" name="concurrency" min="1" max="64" step="1" inputmode="numeric" value="${b.setting}" aria-label="Builds at once" aria-describedby="builds-now"><button type="submit">Save</button></div>` +
30
- `<p class="meta" id="builds-now">${e(now)}</p></form></section>`;
26
+ return html `<section class="builds-at-once"><h2>Builds at once</h2><p>Up to ${b.setting}. ${now}</p></section>`;
27
+ return html `<section class="builds-at-once"><h2>Builds at once</h2>${postForm("/settings/project/concurrency", html `<div class="builds-row"><input type="number" name="concurrency" min="1" max="64" step="1" inputmode="numeric" value="${b.setting}" aria-label="Builds at once" aria-describedby="builds-now"><button type="submit">Save</button></div><p class="meta" id="builds-now">${now}</p>`, { hidden: { repo: view.repo } })}</section>`;
31
28
  }
32
- const note = (notice) => notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
29
+ const note = (notice) => notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
33
30
  /** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
34
- export function projectSettingsHtml(view, csrf, notice) {
35
- const held = `<p>Toolroll holds ${e(holdingsWords(view.holdings))} for <strong>${e(view.name)}</strong>.</p>`;
36
- const head = `<section class="project-settings">${note(notice)}<p class="path">${e(view.repo)}</p>${held}${buildsHtml(view, csrf)}`;
31
+ export function projectSettingsHtml(view, notice) {
32
+ const held = html `<p>Toolroll holds ${holdingsWords(view.holdings)} for <strong>${view.name}</strong>.</p>`;
33
+ const head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${buildsHtml(view)}`;
37
34
  if (!view.canDelete)
38
- return `${head}<p class="meta">An instance operator can delete a project.</p></section>`;
35
+ return html `<section class="project-settings">${head}<p class="meta">An instance operator can delete a project.</p></section>`;
39
36
  const body = view.running.length > 0
40
- ? `<p class="problem">Its work is running: ${e(view.running.join(", "))}. Stop it before deleting the project.</p>`
41
- : `<p>Removes everything Toolroll holds for ${e(view.name)}. The repository and its own branches stay. There's no undo.</p>` +
42
- `<form method="post" action="/settings/project/delete"><input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="repo" value="${e(view.repo)}">` +
43
- `<label><span>Type <strong>${e(view.name)}</strong> to continue</span><input name="name" autocomplete="off" autocapitalize="off" spellcheck="false" required></label>` +
44
- `<button type="submit">Continue</button></form>`;
37
+ ? html `<p class="problem">Its work is running: ${view.running.join(", ")}. Stop it before deleting the project.</p>`
38
+ : html `<p>Removes everything Toolroll holds for ${view.name}. The repository and its own branches stay. There's no undo.</p>${postForm("/settings/project/delete", html `<label><span>Type <strong>${view.name}</strong> to continue</span><input name="name" autocomplete="off" autocapitalize="off" spellcheck="false" required></label><button type="submit">Continue</button>`, { hidden: { repo: view.repo } })}`;
45
39
  // Removing is the reversible step: off the lists and the builder, everything kept.
46
- const remove = `<form class="remove-project" method="post" action="/projects/remove"><input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="repo" value="${e(view.repo)}">` +
47
- `<button type="submit">Remove from Toolroll</button><p class="meta">Its tasks and results stay saved. Add it again to bring it back.</p></form>`;
48
- return `${head}${remove}<details class="danger-zone"${notice.problem ? " open" : ""}><summary>Delete project</summary>${body}</details></section>`;
40
+ const remove = postForm("/projects/remove", html `<button type="submit">Remove from Toolroll</button><p class="meta">Its tasks and results stay saved. Add it again to bring it back.</p>`, { attrs: { class: "remove-project" }, hidden: { repo: view.repo } });
41
+ return html `<section class="project-settings">${head}${remove}<details class="danger-zone"${notice.problem ? html ` open` : ""}><summary>Delete project</summary>${body}</details></section>`;
49
42
  }
50
43
  /** The second step: exactly what goes, and the password. */
51
- export function projectDeleteConfirmHtml(view, csrf, problem) {
44
+ export function projectDeleteConfirmHtml(view, problem) {
52
45
  const h = view.holdings;
53
46
  const count = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
54
47
  const items = [
@@ -59,10 +52,5 @@ export function projectDeleteConfirmHtml(view, csrf, problem) {
59
52
  ...(h.teammates > 0 ? [count(h.teammates, "teammate")] : []),
60
53
  "Its budgets, settings and knowledge",
61
54
  ];
62
- return `<section class="project-delete">${problem === null ? "" : `<p class="problem" role="alert">${e(problem)}</p>`}` +
63
- `<p>This removes:</p><ul class="removes">${items.map(one => `<li>${e(one)}</li>`).join("")}</ul>` +
64
- `<p>The repository at <span class="path">${e(view.repo)}</span> and its own branches stay. The ledger keeps its history. There's no undo.</p>` +
65
- `<form method="post" action="/settings/project/delete"><input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="repo" value="${e(view.repo)}"><input type="hidden" name="name" value="${e(view.name)}"><input type="hidden" name="step" value="delete">` +
66
- `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>` +
67
- `<div class="actions"><button type="submit" class="danger">Delete project</button><a href="/settings/project?repo=${e(encodeURIComponent(view.repo))}">Cancel</a></div></form></section>`;
55
+ return html `<section class="project-delete">${problem === null ? "" : html `<p class="problem" role="alert">${problem}</p>`}<p>This removes:</p><ul class="removes">${items.map(one => html `<li>${one}</li>`)}</ul><p>The repository at <span class="path">${view.repo}</span> and its own branches stay. The ledger keeps its history. There's no undo.</p>${postForm("/settings/project/delete", html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label><div class="actions"><button type="submit" class="danger">Delete project</button><a href="/settings/project?repo=${encodeURIComponent(view.repo)}">Cancel</a></div>`, { hidden: { repo: view.repo, name: view.name, step: "delete" } })}</section>`;
68
56
  }
@@ -207,35 +207,30 @@ function deleteRows(store, repo, d, now) {
207
207
  const notifications = json(ids(db, `SELECT id FROM notification WHERE project = ? OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, T, R));
208
208
  const questions = json(ids(db, `SELECT id FROM teammate_question WHERE ${IN("teammate")} OR ${IN("card")}`, M, C));
209
209
  const proposals = json(ids(db, `SELECT id FROM mate_proposal WHERE ${IN("thread")}`, H));
210
- const conversations = json(ids(db, `SELECT id FROM telegram_conversation WHERE ${IN("task_id")}`, K));
211
- // Chats about the project's work, on every surface.
212
- for (const surface of ["discord", "slack", "teams"]) {
213
- del(`${surface}_progress`, IN("run"), R);
214
- del(`${surface}_action`, IN("proposal"), proposals);
215
- del(`${surface}_flow_action`, IN("card"), C);
216
- del(`${surface}_flow_prompt`, IN("card"), C);
217
- del(`${surface}_flow_choice`, IN("card"), C);
218
- del(`${surface}_flow_note`, IN("card"), C);
219
- del(`${surface}_question_action`, IN("question"), questions);
220
- del(`${surface}_question_prompt`, IN("question"), questions);
221
- del(`${surface}_ask_action`, IN("turn"), U);
222
- }
223
- del("telegram_action", IN("decision"), decisions);
224
- del("telegram_decision_message", IN("decision"), decisions);
225
- del("telegram_note_draft", IN("decision"), decisions);
226
- del("telegram_proposal_action", IN("proposal"), proposals);
227
- del("telegram_flow_action", IN("card"), C);
228
- del("telegram_flow_prompt", IN("card"), C);
229
- del("telegram_flow_choice", IN("card"), C);
210
+ // A chat message about one of the project's tasks (its turn named it), with its reply. Event ids are text ('m123'),
211
+ // and the same id can be another app's: each is kept whole, as provider:id.
212
+ const conversations = json(list(db, `SELECT provider || ':' || id FROM chat_event WHERE kind = 'message' AND json_extract(payload, '$.about.task') IN (SELECT value FROM json_each(?))`, K));
213
+ // Every app's planned parts about the project's work: a result screenshot, or a message naming one of its tasks or runs.
214
+ const parts = `SELECT provider || ':' || id FROM chat_part WHERE provider || ':' || event IN (SELECT value FROM json_each(?))
215
+ OR json_extract(payload, '$.image.taskId') IN (SELECT value FROM json_each(?)) OR json_extract(payload, '$.image.run') IN (SELECT value FROM json_each(?))`;
216
+ const doomedParts = `provider || ':' || part IN (${parts})`;
217
+ // Chats about the project's work, on every app.
218
+ del("chat_progress", IN("run"), R);
219
+ del("chat_action", `${IN("proposal")} OR ${IN("decision")} OR ${doomedParts}`, proposals, decisions, conversations, K, R);
220
+ del("chat_flow_action", `${IN("card")} OR ${doomedParts}`, C, conversations, K, R);
221
+ del("chat_flow_prompt", IN("card"), C);
222
+ del("chat_flow_choice", `${IN("card")} OR ${doomedParts}`, C, conversations, K, R);
223
+ del("chat_flow_note", IN("card"), C);
224
+ del("chat_question_action", `${IN("question")} OR ${doomedParts}`, questions, conversations, K, R);
225
+ del("chat_question_prompt", IN("question"), questions);
226
+ del("chat_ask_action", `${IN("turn")} OR ${doomedParts}`, U, conversations, K, R);
227
+ del("chat_note_draft", IN("decision"), decisions);
230
228
  del("flow_send", IN("card"), C);
231
- del("telegram_question_action", IN("question"), questions);
232
- del("telegram_question_prompt", IN("question"), questions);
233
229
  del("chat_decide_action", IN("task_id"), K);
234
230
  del("chat_decide_prompt", IN("task_id"), K);
235
- del("telegram_task_message", `${IN("task_id")} OR ${IN("source_run")}`, K, R);
236
- del("telegram_conversation_part", `${IN("conversation")} OR ${IN("source_run")} OR ${IN("task_id")}`, conversations, R, K);
237
- del("telegram_conversation", IN("id"), conversations);
238
- del("telegram_outbound_message", `project = ? OR ${IN("notification")} OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, notifications, T, R);
231
+ del("chat_message_ref", `${IN("decision")} OR ${IN("task_id")} OR ${IN("run")} OR project = ? OR ${IN("notification")} OR ${IN("task_ref")}`, decisions, K, R, repo, notifications, T);
232
+ del("chat_part", `provider || ':' || id IN (${parts})`, conversations, K, R);
233
+ del("chat_event", `provider || ':' || id IN (SELECT value FROM json_each(?))`, conversations);
239
234
  del("chat_card_task", IN("task_ref"), T);
240
235
  del("chat_batch_item", IN("task_ref"), T);
241
236
  del("result_shot_sent", IN("run"), R);
@@ -1,6 +1,7 @@
1
1
  /** Settings → Projects → Pull requests: one project's pull-request setup, in plain words. Off shows what setup
2
2
  * found (or what to fix) and one Turn on; on shows where pull requests go and how they merge. Every change takes
3
3
  * the password; turning on restates the exact repository and branch that were checked. */
4
+ import { type Html } from "./html.js";
4
5
  import type { Publishing, PublishingPlan } from "./pull-request-flow.js";
5
6
  export type PullRequestSettingsView = {
6
7
  repo: string;
@@ -19,5 +20,5 @@ export type PullRequestSettingsView = {
19
20
  said: string | null;
20
21
  problem: string | null;
21
22
  };
22
- export declare function pullRequestSettingsHtml(view: PullRequestSettingsView): string;
23
+ export declare function pullRequestSettingsHtml(view: PullRequestSettingsView): Html;
23
24
  export declare const PULL_REQUEST_SETTINGS_CSS = "\n .pr-settings { display: grid; gap: .6rem; max-width: 40rem; }\n .pr-settings p, .pr-settings h2 { margin: 0; }\n .pr-settings label { display: grid; gap: .25rem; }\n .pr-settings label input[type=password], .pr-settings select { min-height: 44px; box-sizing: border-box; width: 100%; }\n .pr-settings .pr-check { display: flex; align-items: center; gap: .5rem; min-height: 44px; }\n .pr-settings button { min-height: 44px; justify-self: start; }\n .pr-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n @media (max-width: 480px) { .pr-settings button { justify-self: stretch; } }\n";
@@ -1,47 +1,32 @@
1
- const escape = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1
+ /** Settings → Projects → Pull requests: one project's pull-request setup, in plain words. Off shows what setup
2
+ * found (or what to fix) and one Turn on; on shows where pull requests go and how they merge. Every change takes
3
+ * the password; turning on restates the exact repository and branch that were checked. */
4
+ import { html, postForm } from "./html.js";
2
5
  const METHOD_WORDS = { squash: "Squash and merge", merge: "Merge commit", rebase: "Rebase and merge" };
3
6
  export function pullRequestSettingsHtml(view) {
4
- const hidden = (act) => `<input type="hidden" name="csrf" value="${escape(view.csrf)}"><input type="hidden" name="repo" value="${escape(view.repo)}"><input type="hidden" name="act" value="${act}">`;
5
- const password = `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`;
7
+ const post = (act, body, hidden = {}) => postForm("/settings/pull-requests", body, { attrs: { class: "card pr-settings" }, hidden: { repo: view.repo, act, ...hidden } });
8
+ const password = html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`;
6
9
  const notes = [
7
- view.said === null ? "" : `<p class="notice" role="status">${escape(view.said)}</p>`,
8
- view.problem === null ? "" : `<p class="problem" role="alert">${escape(view.problem)}</p>`,
9
- ].join("");
10
- const head = `<p class="meta">${escape(view.name)} · <span class="mono">${escape(view.repo)}</span></p>${notes}`;
10
+ view.said === null ? "" : html `<p class="notice" role="status">${view.said}</p>`,
11
+ view.problem === null ? "" : html `<p class="problem" role="alert">${view.problem}</p>`,
12
+ ];
13
+ const head = html `<p class="meta">${view.name} · <span class="mono">${view.repo}</span></p>${notes}`;
11
14
  const publishing = view.publishing;
12
15
  if (publishing.on) {
13
- const status = `<section class="card pr-settings"><p><strong>On</strong> · <span class="mono">${escape(publishing.githubRepo)}</span> into <span class="mono">${escape(publishing.base)}</span></p>` +
14
- `<p class="meta">${publishing.legacy ? "Every finished build opens a pull request (set up with publish grant)." : "Complete offers “Complete and open a pull request”."} Set up by ${escape(publishing.grantedBy)}.</p></section>`;
16
+ const status = html `<section class="card pr-settings"><p><strong>On</strong> · <span class="mono">${publishing.githubRepo}</span> into <span class="mono">${publishing.base}</span></p><p class="meta">${publishing.legacy ? "Every finished build opens a pull request (set up with publish grant)." : "Complete offers “Complete and open a pull request”."} Set up by ${publishing.grantedBy}.</p></section>`;
15
17
  if (!view.canChange)
16
- return head + status;
17
- const methods = ["squash", "merge", "rebase"].map(method => `<option value="${method}"${publishing.mergeMethod === method ? " selected" : ""}>${METHOD_WORDS[method]}${method === "squash" ? " (default)" : ""}</option>`).join("");
18
- return head + status +
19
- `<form method="post" action="/settings/pull-requests" class="card pr-settings">${hidden("settings")}` +
20
- `<h2>Merging</h2>` +
21
- `<label>Merge method<select name="method">${methods}</select></label>` +
22
- `<label class="row pr-check"><input type="checkbox" name="when-green" value="1"${publishing.mergeWhenGreen ? " checked" : ""}> Merge when checks pass</label>` +
23
- `<p class="meta">Off by default. When on, a pull request merges by itself once its checks pass on the completed commit. Branches are deleted after merging.</p>` +
24
- `${password}<button type="submit">Save</button></form>` +
25
- `<details class="settings-more"><summary>Turn off pull requests</summary>` +
26
- `<form method="post" action="/settings/pull-requests" class="card pr-settings">${hidden("off")}` +
27
- `<p class="meta">Nothing more is pushed. Open pull requests stay on GitHub.</p>${password}<button type="submit" class="danger">Turn off</button></form></details>`;
18
+ return html `${head}${status}`;
19
+ const methods = ["squash", "merge", "rebase"].map(method => html `<option value="${method}"${publishing.mergeMethod === method ? html ` selected` : ""}>${METHOD_WORDS[method]}${method === "squash" ? " (default)" : ""}</option>`);
20
+ return html `${head}${status}${post("settings", html `<h2>Merging</h2><label>Merge method<select name="method">${methods}</select></label><label class="row pr-check"><input type="checkbox" name="when-green" value="1"${publishing.mergeWhenGreen ? html ` checked` : ""}> Merge when checks pass</label><p class="meta">Off by default. When on, a pull request merges by itself once its checks pass on the completed commit. Branches are deleted after merging.</p>${password}<button type="submit">Save</button>`)}<details class="settings-more"><summary>Turn off pull requests</summary>${post("off", html `<p class="meta">Nothing more is pushed. Open pull requests stay on GitHub.</p>${password}<button type="submit" class="danger">Turn off</button>`)}</details>`;
28
21
  }
29
22
  const check = view.check;
30
23
  if (check === null)
31
- return head + `<section class="card pr-settings"><p><strong>Off</strong></p><p class="meta">An approver can turn on pull requests.</p></section>`;
24
+ return html `${head}<section class="card pr-settings"><p><strong>Off</strong></p><p class="meta">An approver can turn on pull requests.</p></section>`;
32
25
  if (!check.ok) {
33
- return head + `<section class="card pr-settings"><p><strong>Can’t turn on yet</strong></p><p>${escape(check.message)}</p>` +
34
- `<p><a class="button-link" href="/settings/pull-requests?repo=${encodeURIComponent(view.repo)}">Check again</a></p></section>`;
26
+ return html `${head}<section class="card pr-settings"><p><strong>Can’t turn on yet</strong></p><p>${check.message}</p><p><a class="button-link" href="/settings/pull-requests?repo=${encodeURIComponent(view.repo)}">Check again</a></p></section>`;
35
27
  }
36
28
  const plan = check.plan;
37
- return head +
38
- `<form method="post" action="/settings/pull-requests" class="card pr-settings">${hidden("on")}` +
39
- `<input type="hidden" name="github" value="${escape(plan.githubRepo)}"><input type="hidden" name="base" value="${escape(plan.base)}">` +
40
- `<p><strong>Ready to turn on</strong></p>` +
41
- `<ul class="pr-terms"><li>Pull requests open on <span class="mono">${escape(plan.githubRepo)}</span> into <span class="mono">${escape(plan.base)}</span>${plan.account === null ? "" : `, as ${escape(plan.account)}`}.</li>` +
42
- `<li>Complete offers “Complete and open a pull request”, from the exact completed commit. A flow's Pull request zone can open one too.</li>` +
43
- `<li>Merging squashes and deletes the branch. A person merges with their password; a flow merges only when its zone says so, after a person approved the card.</li></ul>` +
44
- `${password}<button type="submit">Turn on pull requests</button></form>`;
29
+ return html `${head}${post("on", html `<p><strong>Ready to turn on</strong></p><ul class="pr-terms"><li>Pull requests open on <span class="mono">${plan.githubRepo}</span> into <span class="mono">${plan.base}</span>${plan.account === null ? "" : `, as ${plan.account}`}.</li><li>Complete offers “Complete and open a pull request”, from the exact completed commit. A flow's Pull request zone can open one too.</li><li>Merging squashes and deletes the branch. A person merges with their password; a flow merges only when its zone says so, after a person approved the card.</li></ul>${password}<button type="submit">Turn on pull requests</button>`, { github: plan.githubRepo, base: plan.base })}`;
45
30
  }
46
31
  export const PULL_REQUEST_SETTINGS_CSS = `
47
32
  .pr-settings { display: grid; gap: .6rem; max-width: 40rem; }