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,9 +1,7 @@
1
+ /** Shared setup views over the existing project configuration and approval doors. */
2
+ import { html, joinHtml, postForm } from "./html.js";
1
3
  import { ASSISTANTS } from "./setup-guide.js";
2
4
  import { openRouterPicker } from "./openrouter-models.js";
3
- const escape = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4
- export function hiddenFields(values) {
5
- return Object.entries(values).map(([name, value]) => `<input type="hidden" name="${escape(name)}" value="${escape(value)}">`).join("");
6
- }
7
5
  export function connectionWords(connection) {
8
6
  return {
9
7
  connected: "Connected", "signed-out": "Not signed in", "not-installed": "CLI not installed",
@@ -12,42 +10,19 @@ export function connectionWords(connection) {
12
10
  }
13
11
  export function connectionHtml(provider, connection, task = "") {
14
12
  const query = new URLSearchParams({ provider, ...(task ? { task } : {}) });
15
- return `<div class="card assistant-account"><p><strong>${escape(ASSISTANTS[provider].name)} · ${connectionWords(connection)}</strong></p>` +
16
- `<p class="meta">${[connection.email, connection.plan, connection.method].filter(Boolean).map(one => escape(one)).join(" · ")}</p>` +
17
- `<p class="meta">Checked ${escape(new Date(connection.checkedAt).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZoneName: "short" }))} on the computer running this console. Sign-in does not prove model access or remaining quota.</p>` +
18
- `<a class="button-link" href="/control/connection?${escape(query.toString())}">${connection.state === "connected" ? "Manage connection" : "Connect account"}</a> · ` +
19
- `<a href="/control?${escape(query.toString())}&amp;check-connection=1">Check again</a></div>`;
13
+ return html `<div class="card assistant-account"><p><strong>${ASSISTANTS[provider].name} · ${connectionWords(connection)}</strong></p><p class="meta">${[connection.email, connection.plan, connection.method].filter(Boolean).join(" · ")}</p><p class="meta">Checked ${new Date(connection.checkedAt).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZoneName: "short" })} on the computer running this console. Sign-in does not prove model access or remaining quota.</p><a class="button-link" href="/control/connection?${query.toString()}">${connection.state === "connected" ? "Manage connection" : "Connect account"}</a> · <a href="/control?${query.toString()}&amp;check-connection=1">Check again</a></div>`;
20
14
  }
21
15
  export function controlSetupHtml(data) {
22
16
  const { provider, inputs, task = "" } = data;
23
17
  const query = new URLSearchParams({ provider, ...(task ? { task } : {}) });
24
18
  const model = data.catalog !== null
25
19
  ? openRouterPicker(data.catalog, inputs.model || null, `/control?${query}&refresh-models=1`)
26
- : `<label>Model<input type="text" name="model" value="${escape(inputs.model)}" list="setup-models" required autocomplete="off" placeholder="Exact model id or supported alias"></label>` +
27
- `<datalist id="setup-models">${data.models.map(one => `<option value="${escape(one.value)}">${escape(one.label)}</option>`).join("")}</datalist>`;
28
- return `<h1>Project setup</h1><p>${escape(data.repo)}</p>` +
29
- `<p class="meta">Choose the default builder for new tasks and the preparation command for this project.</p>` +
30
- `<nav class="assistant-picker row" aria-label="Choose provider">${Object.keys(ASSISTANTS).map(id => `<a class="button-link" href="/control?${escape(new URLSearchParams({ provider: id, ...(task ? { task } : {}) }).toString())}"${id === provider ? ' aria-current="page"' : ""}>${ASSISTANTS[id].name}</a>`).join(" ")}</nav>` +
31
- connectionHtml(provider, data.connection, task) +
32
- `<form method="post" action="/control/setup-preview" class="card">${hiddenFields({ csrf: data.csrf, repo: data.repo, provider, task })}` + model +
33
- `<details class="setup-advanced"><summary>Project preparation</summary>` +
34
- (data.preparation === null ? "" : `<p class="meta">Suggested from ${escape(data.preparation.evidence)}: ${escape(data.preparation.label)} — <code>${escape(data.preparation.command)}</code>. Review before approving.</p>`) +
35
- `<label>Preparation command<textarea name="command" rows="2" maxlength="2000">${escape(inputs.command)}</textarea></label>` +
36
- `<label>Preparation timeout in seconds<input name="seconds" type="number" min="1" max="3600" value="${escape(inputs.seconds)}" required></label>` +
37
- `<p class="meta">Preparation applies to subsequent runs, including already approved tasks. Saving setup does not execute it.</p></details>` +
38
- `<button type="submit">Review project setup</button></form>` +
39
- `<section class="card"><h2>Agent instructions</h2>` +
40
- (data.instructions.message ? `<p>${escape(data.instructions.message)}</p>` : data.instructions.installed ? `<p>Toolroll instructions are installed.</p>` :
41
- `<p>Give the agent the project's Toolroll handoff instructions.</p><form method="post" action="/control/instructions-preview">${hiddenFields({ csrf: data.csrf, repo: data.repo })}<button>Review instructions</button></form>`) +
42
- `</section><p><a href="/fleet">Configure planning, repair, and review agents</a> · <a href="/tasks/new">Describe a task</a></p>`;
20
+ : html `<label>Model<input type="text" name="model" value="${inputs.model}" list="setup-models" required autocomplete="off" placeholder="Exact model id or supported alias"></label><datalist id="setup-models">${data.models.map(one => html `<option value="${one.value}">${one.label}</option>`)}</datalist>`;
21
+ return html `<h1>Project setup</h1><p>${data.repo}</p><p class="meta">Choose the default builder for new tasks and the preparation command for this project.</p><nav class="assistant-picker row" aria-label="Choose provider">${joinHtml(Object.keys(ASSISTANTS).map(id => html `<a class="button-link" href="/control?${new URLSearchParams({ provider: id, ...(task ? { task } : {}) }).toString()}"${id === provider ? html ` aria-current="page"` : ""}>${ASSISTANTS[id].name}</a>`), " ")}</nav>${connectionHtml(provider, data.connection, task)}${postForm("/control/setup-preview", html `${model}<details class="setup-advanced"><summary>Project preparation</summary>${data.preparation === null ? "" : html `<p class="meta">Suggested from ${data.preparation.evidence}: ${data.preparation.label} — <code>${data.preparation.command}</code>. Review before approving.</p>`}<label>Preparation command<textarea name="command" rows="2" maxlength="2000">${inputs.command}</textarea></label><label>Preparation timeout in seconds<input name="seconds" type="number" min="1" max="3600" value="${inputs.seconds}" required></label><p class="meta">Preparation applies to subsequent runs, including already approved tasks. Saving setup does not execute it.</p></details><button type="submit">Review project setup</button>`, { attrs: { class: "card" }, hidden: { repo: data.repo, provider, task } })}<section class="card"><h2>Agent instructions</h2>${data.instructions.message ? html `<p>${data.instructions.message}</p>` : data.instructions.installed ? html `<p>Toolroll instructions are installed.</p>` :
22
+ html `<p>Give the agent the project's Toolroll handoff instructions.</p>${postForm("/control/instructions-preview", html `<button>Review instructions</button>`, { hidden: { repo: data.repo } })}`}</section><p><a href="/fleet">Configure planning, repair, and review agents</a> · <a href="/tasks/new">Describe a task</a></p>`;
43
23
  }
24
+ /** `fields` may still carry the session's csrf (a caller's habit): postForm writes the one CSRF field. */
44
25
  export function setupPreviewHtml(inputs, fields) {
45
- return `<h1>Approve project setup</h1><div class="card"><p>${escape(fields.repo ?? "")}</p>` +
46
- `<p><strong>Builder:</strong> ${escape(inputs.provider)} · ${escape(inputs.model)}</p>` +
47
- `<p><strong>Preparation:</strong></p><pre class="recap">${escape(inputs.command || "No preparation command")}</pre>` +
48
- `<p>Preparation timeout: ${escape(inputs.seconds)} seconds.</p>` +
49
- `<p>The builder default applies to new tasks. Preparation applies to subsequent runs, including already approved tasks. Existing signed task settings and verification rules remain in force.</p>` +
50
- `<form method="post" action="/control/setup-approve">${hiddenFields({ ...fields, ...inputs })}` +
51
- `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>` +
52
- `<button>Approve project setup</button></form></div>`;
26
+ const { csrf: _csrf, ...rest } = fields;
27
+ return html `<h1>Approve project setup</h1><div class="card"><p>${fields.repo ?? ""}</p><p><strong>Builder:</strong> ${inputs.provider} · ${inputs.model}</p><p><strong>Preparation:</strong></p><pre class="recap">${inputs.command || "No preparation command"}</pre><p>Preparation timeout: ${inputs.seconds} seconds.</p><p>The builder default applies to new tasks. Preparation applies to subsequent runs, including already approved tasks. Existing signed task settings and verification rules remain in force.</p>${postForm("/control/setup-approve", html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><button>Approve project setup</button>`, { hidden: { ...rest, ...inputs } })}</div>`;
53
28
  }
@@ -4,6 +4,7 @@
4
4
  * can see and end everyone's.
5
5
  */
6
6
  import type { ApiTokenRow, WebSessionRow } from "./store.js";
7
+ import { type Html } from "./html.js";
7
8
  export declare const CREDENTIALS_CSS: string;
8
9
  /** "Chrome on macOS", from a user agent, for a person to recognise a session. */
9
10
  export declare function deviceWords(agent: string | null): string;
@@ -19,12 +20,12 @@ export type CredentialsView = {
19
20
  /** Request limits (request-budget-ui.ts): words for each token's line, and the operator's disclosure. */
20
21
  limits?: {
21
22
  note: (token: ApiTokenRow) => string;
22
- section: string;
23
+ section: Html;
23
24
  };
24
25
  };
25
- export declare function credentialsHtml(view: CredentialsView, csrf: string, notice: {
26
+ export declare function credentialsHtml(view: CredentialsView, notice: {
26
27
  said?: string | null;
27
28
  problem?: string | null;
28
- }): string;
29
+ }): Html;
29
30
  /** The token, once: a focused page with no script. */
30
- export declare function tokenShownHtml(name: string, token: string, access: "read" | "act", expiresAt: string): string;
31
+ export declare function tokenShownHtml(name: string, token: string, access: "read" | "act", expiresAt: string): Html;
@@ -1,6 +1,6 @@
1
1
  import { daysLeft, expiryNoticeDue, TOKEN_DAYS, tokenLive } from "./api-tokens.js";
2
2
  import { projectName } from "./project.js";
3
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
3
+ import { html, postForm } from "./html.js";
4
4
  export const CREDENTIALS_CSS = `.credentials{max-width:820px;min-width:0}.credentials .card{padding:14px 16px;margin:12px 0}.credentials h2{font-size:1.05rem;margin:0 0 8px}` +
5
5
  `.credentials .rows{display:grid;gap:0;border:1px solid var(--so-line);border-radius:10px;overflow:hidden}.credentials .row-item{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 14px;border-bottom:1px solid var(--so-line)}` +
6
6
  `.credentials .row-item:last-child{border-bottom:0}.credentials .row-item p{margin:0}.credentials .row-item .meta{font-size:.85rem}.credentials form{margin:0}.credentials button{min-height:40px;white-space:nowrap}` +
@@ -22,39 +22,38 @@ const when = (value, now) => {
22
22
  const minutes = Math.round((now - at) / 60_000);
23
23
  return minutes < 1 ? "just now" : minutes < 60 ? `${minutes} min ago` : minutes < 60 * 24 ? `${Math.round(minutes / 60)} h ago` : new Date(at).toISOString().slice(0, 10);
24
24
  };
25
- export function credentialsHtml(view, csrf, notice) {
26
- const hidden = (fields) => Object.entries(fields).map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`).join("");
27
- const post = (fields, label, style = "secondary") => `<form method="post" action="/settings/sessions">${hidden({ csrf, ...fields, ...(view.everyone ? { everyone: "1" } : {}) })}<button class="${style}">${label}</button></form>`;
28
- const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
29
- const toggle = view.canSeeEveryone ? `<p class="meta"><a href="/settings/sessions${view.everyone ? "" : "?everyone=1"}">${view.everyone ? "Show only mine" : "Show everyone's"}</a></p>` : "";
30
- const sessions = view.sessions.length === 0 ? `<p class="meta">No one else is signed in.</p>` : `<div class="rows">${view.sessions.map(one => `<div class="row-item" data-session="${e(one.idHash.slice(0, 12))}"><div><p>${view.everyone ? `<strong>${e(one.account)}</strong> · ` : ""}${e(deviceWords(one.agent))}${one.here ? `<span class="badge-here">This browser</span>` : ""}</p>` +
31
- `<p class="meta">${one.ssoAt === null ? "Password" : "Identity provider"} · ${e(one.address ?? "unknown address")} · active ${e(when(one.lastSeen, view.now))} · signed in ${e(when(one.createdAt, view.now))}</p></div>` +
32
- `${one.here ? "" : post({ action: "end-session", session: one.idHash }, "Sign out")}</div>`).join("")}</div>`;
25
+ export function credentialsHtml(view, notice) {
26
+ const post = (fields, label, style = "secondary") => postForm("/settings/sessions", html `<button class="${style}">${label}</button>`, { hidden: { ...fields, ...(view.everyone ? { everyone: "1" } : {}) } });
27
+ const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
28
+ const toggle = view.canSeeEveryone ? html `<p class="meta"><a href="/settings/sessions${view.everyone ? "" : "?everyone=1"}">${view.everyone ? "Show only mine" : "Show everyone's"}</a></p>` : "";
29
+ const sessions = view.sessions.length === 0 ? html `<p class="meta">No one else is signed in.</p>` : html `<div class="rows">${view.sessions.map(one => html `<div class="row-item" data-session="${one.idHash.slice(0, 12)}"><div><p>${view.everyone ? html `<strong>${one.account}</strong> · ` : ""}${deviceWords(one.agent)}${one.here ? html `<span class="badge-here">This browser</span>` : ""}</p>\
30
+ <p class="meta">${one.ssoAt === null ? "Password" : "Identity provider"} · ${one.address ?? "unknown address"} · active ${when(one.lastSeen, view.now)} · signed in ${when(one.createdAt, view.now)}</p></div>\
31
+ ${one.here ? "" : post({ action: "end-session", session: one.idHash }, "Sign out")}</div>`)}</div>`;
33
32
  const others = view.sessions.filter(one => !one.here && one.account === view.who).length;
34
33
  const endOthers = others > 0 && !view.everyone ? post({ action: "end-others" }, `Sign out everywhere else (${others})`) : "";
35
34
  const live = view.tokens.filter(one => tokenLive(one, view.now));
36
35
  // v111: its projects, and one state: when it stops (soon, said once and marked) or, once replaced, when it ends.
37
36
  const ends = (one) => {
38
37
  if (one.replacedBy !== null && one.overlapUntil !== null)
39
- return `<span class="soon">Replaced · stops ${e(one.overlapUntil.slice(11, 16))} UTC</span>`;
38
+ return html `<span class="soon">Replaced · stops ${one.overlapUntil.slice(11, 16)} UTC</span>`;
40
39
  if (expiryNoticeDue(one, view.now) === null)
41
- return `Expires ${e(one.expiresAt.slice(0, 10))}`;
40
+ return html `Expires ${one.expiresAt.slice(0, 10)}`;
42
41
  const days = daysLeft(one.expiresAt, view.now);
43
- return `<span class="soon">Expires ${days <= 1 ? "within a day" : `in ${days} days`}</span>`;
42
+ return html `<span class="soon">Expires ${days <= 1 ? "within a day" : `in ${days} days`}</span>`;
44
43
  };
45
- const scope = (one) => one.projects === null ? "All projects" : one.projects.length === 0 ? "No projects" : one.projects.map(repo => e(projectName(repo))).join(", ");
46
- const tokens = live.length === 0 ? `<p class="meta">No API tokens.</p>` : `<div class="rows">${live.map(one => `<div class="row-item" data-token="${e(one.id)}"><div><p>${view.everyone ? `<strong>${e(one.account)}</strong> · ` : ""}${e(one.name)} · ${one.access === "act" ? "Can act" : "Read only"}</p>` +
47
- `<p class="meta">${scope(one)} · ${ends(one)} · last used ${e(when(one.lastUsedAt, view.now))}${view.limits === undefined ? "" : ` · ${e(view.limits.note(one))}`}</p></div>${post({ action: "revoke-token", token: one.id }, "Revoke")}</div>`).join("")}</div>`;
48
- const create = `<details class="card"><summary>New API token</summary><form method="post" action="/settings/sessions" class="create">${hidden({ csrf, action: "create-token" })}` +
49
- `<label>Name<input type="text" name="name" maxlength="60" required placeholder="for example: CI"></label>` +
50
- `<label>It can<select name="access"><option value="read">Read (tasks, results, the ledger)</option><option value="act">Act as you (file and manage work; never approve)</option></select></label>` +
51
- `<label>Expires in<select name="days">${TOKEN_DAYS.map(days => `<option value="${days}"${days === 90 ? " selected" : ""}>${days} days</option>`).join("")}</select></label>` +
52
- `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button>Make the token</button></form></details>`;
53
- return `<section class="credentials">${note}${toggle}<h2>Signed in</h2>${sessions}${endOthers}<h2 style="margin-top:1.5rem">API tokens</h2>` +
54
- `<p class="meta">For scripts and CI: send <code>Authorization: Bearer &lt;token&gt;</code>. A token can't approve anything.</p>${tokens}${view.everyone ? "" : create}${view.limits?.section ?? ""}</section>`;
44
+ const scope = (one) => one.projects === null ? "All projects" : one.projects.length === 0 ? "No projects" : one.projects.map(repo => projectName(repo)).join(", ");
45
+ const tokens = live.length === 0 ? html `<p class="meta">No API tokens.</p>` : html `<div class="rows">${live.map(one => html `<div class="row-item" data-token="${one.id}"><div><p>${view.everyone ? html `<strong>${one.account}</strong> · ` : ""}${one.name} · ${one.access === "act" ? "Can act" : "Read only"}</p>\
46
+ <p class="meta">${scope(one)} · ${ends(one)} · last used ${when(one.lastUsedAt, view.now)}${view.limits === undefined ? "" : ` · ${view.limits.note(one)}`}</p></div>${post({ action: "revoke-token", token: one.id }, "Revoke")}</div>`)}</div>`;
47
+ const create = html `<details class="card"><summary>New API token</summary>${postForm("/settings/sessions", html `\
48
+ <label>Name<input type="text" name="name" maxlength="60" required placeholder="for example: CI"></label>\
49
+ <label>It can<select name="access"><option value="read">Read (tasks, results, the ledger)</option><option value="act">Act as you (file and manage work; never approve)</option></select></label>\
50
+ <label>Expires in<select name="days">${TOKEN_DAYS.map(days => html `<option value="${days}"${days === 90 ? " selected" : ""}>${days} days</option>`)}</select></label>\
51
+ <label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label><button>Make the token</button>`, { attrs: { class: "create" }, hidden: { action: "create-token" } })}</details>`;
52
+ return html `<section class="credentials">${note}${toggle}<h2>Signed in</h2>${sessions}${endOthers}<h2 style="margin-top:1.5rem">API tokens</h2>\
53
+ <p class="meta">For scripts and CI: send <code>Authorization: Bearer &lt;token&gt;</code>. A token can't approve anything.</p>${tokens}${view.everyone ? "" : create}${view.limits?.section ?? ""}</section>`;
55
54
  }
56
55
  /** The token, once: a focused page with no script. */
57
56
  export function tokenShownHtml(name, token, access, expiresAt) {
58
- return `<h1>${e(name)}</h1><div class="card"><p>Your API token, shown once. Only a hash is kept, so copy it now.</p><p class="mono secret-value">${e(token)}</p>` +
59
- `<p class="meta">${access === "act" ? "It acts as you (never approves)" : "It reads"} until ${e(expiresAt.slice(0, 10))}. Revoke it any time on Sessions &amp; tokens.</p></div><p class="meta"><a href="/settings/sessions">Back to Sessions &amp; tokens</a></p>`;
57
+ return html `<h1>${name}</h1><div class="card"><p>Your API token, shown once. Only a hash is kept, so copy it now.</p><p class="mono secret-value">${token}</p>\
58
+ <p class="meta">${access === "act" ? "It acts as you (never approves)" : "It reads"} until ${expiresAt.slice(0, 10)}. Revoke it any time on Sessions &amp; tokens.</p></div><p class="meta"><a href="/settings/sessions">Back to Sessions &amp; tokens</a></p>`;
60
59
  }
@@ -1,3 +1,4 @@
1
+ import { type Html } from "./html.js";
1
2
  import type { DemoExchange } from "./demo.js";
2
3
  /** What a finished exchange's stored result holds, read back from its evidence. */
3
4
  export type DemoResultView = {
@@ -17,14 +18,13 @@ export type DemoResultView = {
17
18
  taskHref: string;
18
19
  };
19
20
  /** The conversation itself; the live script swaps this region in place. */
20
- export declare function demoThreadHtml(exchanges: readonly DemoExchange[], csrf: string, resultOf: (exchange: DemoExchange) => DemoResultView | null): string;
21
+ export declare function demoThreadHtml(exchanges: readonly DemoExchange[], resultOf: (exchange: DemoExchange) => DemoResultView | null): Html;
21
22
  export declare function demoChatHtml(input: {
22
23
  exchanges: readonly DemoExchange[];
23
- csrf: string;
24
24
  version: number;
25
25
  problem: string | null;
26
26
  resultOf: (exchange: DemoExchange) => DemoResultView | null;
27
- }): string;
27
+ }): Html;
28
28
  /**
29
29
  * Keeps a running build live: polls while one is working and swaps the
30
30
  * thread in place, unless the visitor is typing inside it. Enter sends.
package/dist/demo-chat.js CHANGED
@@ -4,48 +4,26 @@
4
4
  * running build's progress live; without it the page still works by reload.
5
5
  */
6
6
  import { START_COMMAND } from "./first-run.js";
7
+ import { html, postForm } from "./html.js";
7
8
  import { headlineOf } from "./task-status.js";
8
9
  const SUGGESTIONS = ["Fix the flaky refund test", "Rewrite the empty Payouts page", "Put the new invoice view behind a flag"];
9
- const esc = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
10
- const hidden = (csrf) => `<input type="hidden" name="csrf" value="${esc(csrf)}">`;
11
- function hint(csrf) {
12
- return `<section class="demo-hint" aria-label="Get started">` +
13
- `<p class="demo-hint-title">Ask for something, like “fix the flaky refund test”</p>` +
14
- `<form method="post" action="/chat/demo/ask" class="demo-suggestions">${hidden(csrf)}` +
15
- SUGGESTIONS.map(one => `<button type="submit" name="message" value="${esc(one)}">${esc(one)}</button>`).join("") +
16
- `</form></section>`;
10
+ function hint() {
11
+ return html `<section class="demo-hint" aria-label="Get started"><p class="demo-hint-title">Ask for something, like “fix the flaky refund test”</p>${postForm("/chat/demo/ask", SUGGESTIONS.map(one => html `<button type="submit" name="message" value="${one}">${one}</button>`), { attrs: { class: "demo-suggestions" } })}</section>`;
17
12
  }
18
- function planCard(exchange, csrf) {
13
+ function planCard(exchange) {
19
14
  const plan = exchange.plan;
20
15
  const goal = exchange.note === null ? plan.goal : `${plan.goal} Also: ${exchange.note}`;
21
16
  const actions = exchange.state === "proposed"
22
- ? `<div class="demo-actions">` +
23
- `<form method="post" action="/chat/demo/${exchange.id}/approve" class="approve-form">${hidden(csrf)}<button type="submit">Approve</button></form>` +
24
- `<details class="demo-more"><summary>Change it</summary>` +
25
- `<form method="post" action="/chat/demo/${exchange.id}/change">${hidden(csrf)}` +
26
- `<label for="demo-change-${exchange.id}">What should change?</label>` +
27
- `<textarea id="demo-change-${exchange.id}" name="note" rows="2" maxlength="500" required></textarea>` +
28
- `<button type="submit">Update plan</button></form></details></div>`
17
+ ? html `<div class="demo-actions">${postForm(`/chat/demo/${exchange.id}/approve`, html `<button type="submit">Approve</button>`, { attrs: { class: "approve-form" } })}<details class="demo-more"><summary>Change it</summary>${postForm(`/chat/demo/${exchange.id}/change`, html `<label for="demo-change-${exchange.id}">What should change?</label><textarea id="demo-change-${exchange.id}" name="note" rows="2" maxlength="500" required></textarea><button type="submit">Update plan</button>`)}</details></div>`
29
18
  : exchange.state === "replaced"
30
- ? `<p class="meta">Replaced by the updated plan below.</p>`
31
- : `<p class="meta">Approved.</p>`;
32
- return `<section class="card demo-plan" aria-label="Plan">` +
33
- `<p class="demo-kicker">Plan · ${esc(plan.project)}</p>` +
34
- `<h2>${esc(plan.title)}</h2>` +
35
- `<p>${esc(goal)}</p>` +
36
- `<h3>Boundaries</h3><ul>${plan.boundaries.map(one => `<li>${esc(one)}</li>`).join("")}</ul>` +
37
- `<h3>Checks</h3><ul>${plan.checks.map(one => `<li>${esc(one)}</li>`).join("")}</ul>` +
38
- actions + `</section>`;
19
+ ? html `<p class="meta">Replaced by the updated plan below.</p>`
20
+ : html `<p class="meta">Approved.</p>`;
21
+ return html `<section class="card demo-plan" aria-label="Plan"><p class="demo-kicker">Plan · ${plan.project}</p><h2>${plan.title}</h2><p>${goal}</p><h3>Boundaries</h3><ul>${plan.boundaries.map(one => html `<li>${one}</li>`)}</ul><h3>Checks</h3><ul>${plan.checks.map(one => html `<li>${one}</li>`)}</ul>${actions}</section>`;
39
22
  }
40
23
  function buildCard(exchange) {
41
24
  const stages = [["planning", "Planning"], ["building", "Building"], ["checking", "Running checks"]];
42
25
  const at = stages.findIndex(([key]) => key === exchange.stage);
43
- return `<section class="card demo-build" id="demo-${exchange.id}-work" aria-label="Build" aria-busy="true">` +
44
- `<p class="demo-kicker">${esc(exchange.plan.project)}</p>` +
45
- `<h2>${esc(stages[Math.max(0, at)][1])}…</h2>` +
46
- `<ol class="demo-steps">${stages.map(([, label], index) => `<li class="${index < at ? "done" : index === at ? "now" : ""}"${index === at ? ` aria-current="step"` : ""}>${esc(label)}</li>`).join("")}</ol>` +
47
- (exchange.progress === null ? "" : `<p class="meta demo-progress">${esc(exchange.progress)}</p>`) +
48
- `</section>`;
26
+ return html `<section class="card demo-build" id="demo-${exchange.id}-work" aria-label="Build" aria-busy="true"><p class="demo-kicker">${exchange.plan.project}</p><h2>${stages[Math.max(0, at)][1]}…</h2><ol class="demo-steps">${stages.map(([, label], index) => html `<li class="${index < at ? "done" : index === at ? "now" : ""}"${index === at ? html ` aria-current="step"` : ""}>${label}</li>`)}</ol>${exchange.progress === null ? "" : html `<p class="meta demo-progress">${exchange.progress}</p>`}</section>`;
49
27
  }
50
28
  function diffHtml(diff) {
51
29
  return diff.replace(/\n$/, "").split("\n").map(line => {
@@ -53,71 +31,46 @@ function diffHtml(diff) {
53
31
  : line.startsWith("+++") || line.startsWith("---") ? "meta"
54
32
  : line.startsWith("@@") ? "hunk"
55
33
  : line.startsWith("+") ? "add" : line.startsWith("-") ? "del" : "ctx";
56
- return `<span class="demo-diff-${kind}">${esc(line) || " "}</span>`;
57
- }).join("");
34
+ return html `<span class="demo-diff-${kind}">${line || " "}</span>`;
35
+ });
58
36
  }
59
- function resultCard(exchange, csrf, result) {
37
+ function resultCard(exchange, result) {
60
38
  if (result === null)
61
- return `<section class="card demo-result"><p class="meta">This result's saved evidence is unavailable.</p></section>`;
39
+ return html `<section class="card demo-result"><p class="meta">This result's saved evidence is unavailable.</p></section>`;
62
40
  const passed = result.checks.status === "passed";
63
41
  // The shared headline (task-status.ts): the same words as every real task.
64
42
  const headline = headlineOf({ stage: exchange.state === "complete" ? "complete" : exchange.state === "sent-back" ? "building" : "finished",
65
43
  checks: { status: passed ? "passed" : "failed", exitCode: null, head: null } });
66
- const status = `<span class="badge${headline === "Complete" ? " demo-complete" : headline === "Ready for review" ? " demo-ready" : ""}" data-headline="${headline}">${headline}</span>`;
67
- const summary = `${result.files} file${result.files === 1 ? "" : "s"} changed · +${result.additions} −${result.deletions} · ` +
68
- `<span class="${passed ? "demo-pass" : "demo-fail"}">${esc(result.checks.detail)}</span>`;
44
+ const status = html `<span class="badge${headline === "Complete" ? " demo-complete" : headline === "Ready for review" ? " demo-ready" : ""}" data-headline="${headline}">${headline}</span>`;
45
+ const summary = html `${result.files} file${result.files === 1 ? "" : "s"} changed · +${result.additions} −${result.deletions} · <span class="${passed ? "demo-pass" : "demo-fail"}">${result.checks.detail}</span>`;
69
46
  const actions = exchange.state === "ready"
70
- ? `<div class="demo-actions">` +
71
- `<form method="post" action="/chat/demo/${exchange.id}/complete">${hidden(csrf)}<button type="submit" class="demo-primary">Complete</button></form>` +
72
- `<details class="demo-more"><summary>Request changes</summary>` +
73
- `<form method="post" action="/chat/demo/${exchange.id}/revise">${hidden(csrf)}` +
74
- `<label for="demo-revise-${exchange.id}">What should change?</label>` +
75
- `<textarea id="demo-revise-${exchange.id}" name="note" rows="2" maxlength="500" required></textarea>` +
76
- `<button type="submit">Send back</button></form></details></div>`
77
- : `<p class="meta"><a href="${esc(result.taskHref)}">Open in Tasks</a></p>`;
78
- return `<section class="card demo-result" id="demo-${exchange.id}-work" aria-label="Result">` +
79
- `<p class="demo-state">${status}<span class="meta">${esc(exchange.plan.project)}</span></p>` +
80
- `<h2>${esc(exchange.plan.title)}</h2>` +
81
- `<p>${esc(exchange.plan.conclusion)}</p>` +
82
- `<p class="meta">${summary}</p>` +
83
- actions +
84
- (result.diff === null ? "" : `<details class="demo-evidence"${exchange.state === "ready" ? " open" : ""}><summary>Changes</summary><pre class="demo-diff">${diffHtml(result.diff)}</pre></details>`) +
85
- (result.checkLog === null ? "" : `<details class="demo-evidence"><summary>Check log</summary><pre class="demo-log">${esc(result.checkLog)}</pre></details>`) +
86
- (result.screenshot === null ? "" : `<details class="demo-evidence"><summary>Screenshot</summary><figure><img src="${esc(result.screenshot.href)}" alt="${esc(result.screenshot.caption)}" width="960" height="600"><figcaption class="meta">${esc(result.screenshot.caption)}</figcaption></figure></details>`) +
87
- `</section>`;
47
+ ? html `<div class="demo-actions">${postForm(`/chat/demo/${exchange.id}/complete`, html `<button type="submit" class="demo-primary">Complete</button>`)}<details class="demo-more"><summary>Request changes</summary>${postForm(`/chat/demo/${exchange.id}/revise`, html `<label for="demo-revise-${exchange.id}">What should change?</label><textarea id="demo-revise-${exchange.id}" name="note" rows="2" maxlength="500" required></textarea><button type="submit">Send back</button>`)}</details></div>`
48
+ : html `<p class="meta"><a href="${result.taskHref}">Open in Tasks</a></p>`;
49
+ return html `<section class="card demo-result" id="demo-${exchange.id}-work" aria-label="Result"><p class="demo-state">${status}<span class="meta">${exchange.plan.project}</span></p><h2>${exchange.plan.title}</h2><p>${exchange.plan.conclusion}</p><p class="meta">${summary}</p>${actions}${result.diff === null ? "" : html `<details class="demo-evidence"${exchange.state === "ready" ? html ` open` : ""}><summary>Changes</summary><pre class="demo-diff">${diffHtml(result.diff)}</pre></details>`}${result.checkLog === null ? "" : html `<details class="demo-evidence"><summary>Check log</summary><pre class="demo-log">${result.checkLog}</pre></details>`}${result.screenshot === null ? "" : html `<details class="demo-evidence"><summary>Screenshot</summary><figure><img src="${result.screenshot.href}" alt="${result.screenshot.caption}" width="960" height="600"><figcaption class="meta">${result.screenshot.caption}</figcaption></figure></details>`}</section>`;
88
50
  }
89
51
  /** The conversation itself; the live script swaps this region in place. */
90
- export function demoThreadHtml(exchanges, csrf, resultOf) {
52
+ export function demoThreadHtml(exchanges, resultOf) {
91
53
  if (exchanges.length === 0)
92
- return hint(csrf);
93
- return exchanges.map(exchange => {
54
+ return hint();
55
+ return html `${exchanges.map(exchange => {
94
56
  const parts = [
95
- `<div class="demo-said demo-you"><p>${esc(exchange.asked)}</p></div>`,
96
- `<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>${esc(exchange.reply)}</p></div>`,
97
- planCard(exchange, csrf),
57
+ html `<div class="demo-said demo-you"><p>${exchange.asked}</p></div>`,
58
+ html `<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>${exchange.reply}</p></div>`,
59
+ planCard(exchange),
98
60
  ];
99
61
  if (exchange.state === "working")
100
62
  parts.push(buildCard(exchange));
101
63
  if (exchange.state === "ready" || exchange.state === "complete" || exchange.state === "sent-back")
102
- parts.push(resultCard(exchange, csrf, resultOf(exchange)));
64
+ parts.push(resultCard(exchange, resultOf(exchange)));
103
65
  if (exchange.state === "complete") {
104
- parts.push(`<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`, `<section class="card demo-handoff" data-demo-handoff aria-label="Your own project"><h2>Now try it on your own project</h2>` +
105
- `<p>In your repository's folder, run:</p><pre class="demo-command"><code>${esc(START_COMMAND)}</code></pre>` +
106
- `<p class="meta">It opens in your browser, already signed in.</p></section>`);
66
+ parts.push(html `<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`, html `<section class="card demo-handoff" data-demo-handoff aria-label="Your own project"><h2>Now try it on your own project</h2><p>In your repository's folder, run:</p><pre class="demo-command"><code>${START_COMMAND}</code></pre><p class="meta">It opens in your browser, already signed in.</p></section>`);
107
67
  }
108
- return `<article class="demo-turn" id="demo-${exchange.id}">${parts.join("")}</article>`;
109
- }).join("");
68
+ return html `<article class="demo-turn" id="demo-${exchange.id}">${parts}</article>`;
69
+ })}`;
110
70
  }
111
71
  export function demoChatHtml(input) {
112
72
  const working = input.exchanges.some(one => one.state === "working");
113
- return `<div class="demo-chat">` +
114
- (input.problem === null ? "" : `<p class="problem" role="alert">${esc(input.problem)}</p>`) +
115
- `<div id="demo-thread" class="demo-thread" aria-live="polite" data-version="${input.version}" data-working="${working ? "1" : "0"}">` +
116
- demoThreadHtml(input.exchanges, input.csrf, input.resultOf) + `</div>` +
117
- `<form method="post" action="/chat/demo/ask" class="demo-composer">${hidden(input.csrf)}` +
118
- `<label for="demo-message" class="so-sr-only">Message the lead</label>` +
119
- `<textarea id="demo-message" name="message" rows="2" maxlength="500" placeholder="Ask the lead for a change" required></textarea>` +
120
- `<button type="submit" class="demo-primary">Send</button></form></div>`;
73
+ return html `<div class="demo-chat">${input.problem === null ? "" : html `<p class="problem" role="alert">${input.problem}</p>`}<div id="demo-thread" class="demo-thread" aria-live="polite" data-version="${input.version}" data-working="${working ? "1" : "0"}">${demoThreadHtml(input.exchanges, input.resultOf)}</div>${postForm("/chat/demo/ask", html `<label for="demo-message" class="so-sr-only">Message the lead</label><textarea id="demo-message" name="message" rows="2" maxlength="500" placeholder="Ask the lead for a change" required></textarea><button type="submit" class="demo-primary">Send</button>`, { attrs: { class: "demo-composer" } })}</div>`;
121
74
  }
122
75
  /**
123
76
  * Keeps a running build live: polls while one is working and swaps the
@@ -190,17 +190,13 @@ export async function deliverDiscordPart(options) {
190
190
  if (!options.current() ||
191
191
  !state.owns(identity.installation, options.owner, now))
192
192
  return false;
193
- const row = state
194
- .prepare("SELECT p.* FROM chat_part p JOIN chat_event e ON e.id=p.event WHERE e.installation=? AND p.state='pending' AND (e.kind!='notice' OR ?=1) AND (p.next_at IS NULL OR p.next_at<=?) ORDER BY p.id LIMIT 1")
195
- .get(identity.installation, options.canNotify?.() === false ? 0 : 1, now.toISOString());
193
+ const row = state.nextPart(identity.installation, options.canNotify?.() !== false, now);
196
194
  if (!row)
197
195
  return false;
198
196
  const event = state.event(row.event), binding = event.binding === null ? null : state.bindingById(event.binding);
199
197
  if (!binding ||
200
198
  new Date(row.created).getTime() + 86_400_000 < now.getTime()) {
201
- state
202
- .prepare("UPDATE chat_part SET state='dropped',problem='Delivery expired or access changed; open the saved chat' WHERE id=?")
203
- .run(row.id);
199
+ state.dropPart(row.id, "Delivery expired or access changed; open the saved chat");
204
200
  return true;
205
201
  }
206
202
  try {
@@ -214,16 +210,16 @@ export async function deliverDiscordPart(options) {
214
210
  throw new DiscordError("Connected projects changed");
215
211
  let text = content.text, buttons = [], file;
216
212
  if (content.image && content.shot && resultShotsPruned(options.evidenceRoot, content.image.run)) {
217
- state.prepare("UPDATE chat_part SET state='dropped',problem='Removed by retention' WHERE id=?").run(row.id);
213
+ state.dropPart(row.id, "Removed by retention");
218
214
  return true;
219
215
  }
220
216
  // A screenshot sent with a result replies to that result's message once it is placed, and waits until then.
221
- const result = content.shot?.follows == null ? undefined : state.prepare("SELECT message,state,next_at FROM chat_part WHERE id=?").get(content.shot.follows);
222
- if (result !== undefined && !discordId(result["message"]) && result["state"] === "pending") {
223
- state.prepare("UPDATE chat_part SET next_at=? WHERE id=?").run(shotWaitsUntil(result["next_at"], now), row.id);
217
+ const result = content.shot?.follows == null ? null : state.part(content.shot.follows);
218
+ if (result !== null && !discordId(result.message) && result.state === "pending") {
219
+ state.holdPart(row.id, shotWaitsUntil(result.next_at, now));
224
220
  return true;
225
221
  }
226
- const follows = result?.["message"];
222
+ const follows = result?.message;
227
223
  const thread = discordId(follows) ? String(follows) : event.thread;
228
224
  if (content.image) {
229
225
  const image = verifyResultImage(store, options.evidenceRoot, repos, content.image);
@@ -260,8 +256,7 @@ export async function deliverDiscordPart(options) {
260
256
  preview.text.length <= 3400 &&
261
257
  discordPlain(text).length <= 3900) {
262
258
  buttons = state
263
- .prepare("SELECT token,phase FROM chat_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid")
264
- .all(row.id, now.toISOString())
259
+ .partTokens(row.id, now)
265
260
  .map((action) => ({
266
261
  type: 2,
267
262
  style: action.phase === "yes" ? 4 : action.phase === "confirm" ? 1 : 2,
@@ -317,6 +312,10 @@ export async function deliverDiscordPart(options) {
317
312
  custom_id: `so_${one.token}`,
318
313
  }))
319
314
  : []),
315
+ // A result's, plan's, failure's or pull request's own buttons (chat-decide.ts): its acts, then its links.
316
+ ...(content.decide?.rows.flat() ?? []).flatMap((one, index) => "token" in one
317
+ ? [{ type: 2, style: index === 0 ? 1 : 2, label: one.label.slice(0, 80), custom_id: `so_${one.token}` }]
318
+ : link(options.origin(), one.link)),
320
319
  ...link(options.origin(), content.link),
321
320
  ...(content.also ?? []).flatMap(one => link(options.origin(), one)),
322
321
  ];
@@ -355,13 +354,7 @@ export async function deliverDiscordPart(options) {
355
354
  // Preserve the original remote attachment; never upload it again on an edit.
356
355
  file = undefined;
357
356
  }
358
- state
359
- .prepare("UPDATE chat_part SET message=?,uploaded=? WHERE id=?")
360
- .run(target, content.image &&
361
- Array.isArray(found.attachments) &&
362
- found.attachments.length
363
- ? 1
364
- : 0, row.id);
357
+ state.setPartFile(row.id, null, Boolean(content.image && Array.isArray(found.attachments) && found.attachments.length), target);
365
358
  }
366
359
  }
367
360
  await channelAccess(shared, binding, session?.ceilingDigest);
@@ -396,17 +389,13 @@ export async function deliverDiscordPart(options) {
396
389
  !answer.attachments.some((v) => object(v).filename === file.name &&
397
390
  Number(object(v).size) === file.bytes.length))))
398
391
  throw new DiscordError("Discord did not confirm the message and file identity", 15_000, true);
399
- state
400
- .prepare("UPDATE chat_part SET state='sent',message=?,attempts=attempts+1,problem=NULL,next_at=NULL WHERE id=?")
401
- .run(answer.id, row.id);
402
- state
403
- .prepare("UPDATE chat_runtime SET problem=NULL WHERE installation=? AND owner=?")
404
- .run(identity.installation, options.owner);
392
+ state.partSent(row.id, String(answer.id), options.clock?.() ?? new Date());
393
+ state.setProblem(identity.installation, options.owner, null);
405
394
  return true;
406
395
  }
407
396
  catch (error) {
408
397
  if (error instanceof ChatDeliveryError && error.permanent) {
409
- state.prepare("UPDATE chat_part SET state='dropped',next_at=NULL,problem=? WHERE id=?").run(error.message, row.id);
398
+ state.dropPart(row.id, error.message);
410
399
  return true;
411
400
  }
412
401
  // No permission to attach files here: one plain line instead of the result's screenshots.
@@ -420,18 +409,10 @@ export async function deliverDiscordPart(options) {
420
409
  : new DiscordError("Discord delivery is waiting to retry", 15_000, true);
421
410
  const until = new Date((options.clock?.() ?? new Date()).getTime() +
422
411
  Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
423
- state
424
- .prepare("UPDATE chat_part SET attempts=attempts+1,uncertain=uncertain+?,next_at=?,problem=? WHERE id=?")
425
- .run(problem.uncertain ? 1 : 0, until, problem.message, row.id);
426
- state
427
- .prepare("UPDATE chat_runtime SET problem=? WHERE installation=? AND owner=?")
428
- .run(problem.code === "ratelimited"
429
- ? "Discord asked us to wait. Saved replies will retry."
430
- : problem.message, identity.installation, options.owner);
412
+ state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
413
+ state.setProblem(identity.installation, options.owner, problem.code === "ratelimited" ? "Discord asked us to wait. Saved replies will retry." : problem.message);
431
414
  if (problem.code === "ratelimited")
432
- state
433
- .prepare("UPDATE chat_runtime SET retry_at=? WHERE installation=?")
434
- .run(until, identity.installation);
415
+ state.deferUntil(identity.installation, until);
435
416
  return true;
436
417
  }
437
418
  }
@@ -1,7 +1,8 @@
1
1
  import type { Store } from "./store.js";
2
- export declare function discordSettingsHtml(store: Store, dir: string, csrf: string, options?: {
2
+ import { type Html } from "./html.js";
3
+ export declare function discordSettingsHtml(store: Store, dir: string, options?: {
3
4
  code?: string;
4
5
  problem?: string;
5
6
  now?: Date;
6
7
  who?: string;
7
- }): string;
8
+ }): Html;