toolroll 0.9.54 → 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 (199) hide show
  1. package/CHANGELOG.md +9 -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-polish.d.ts +5 -4
  19. package/dist/chat-polish.js +9 -10
  20. package/dist/check-levels-ui.d.ts +2 -1
  21. package/dist/check-levels-ui.js +26 -53
  22. package/dist/cli.d.ts +3 -1
  23. package/dist/cli.js +9 -3
  24. package/dist/coding-shipping-ui.d.ts +2 -1
  25. package/dist/coding-shipping-ui.js +6 -6
  26. package/dist/coding-ui.d.ts +3 -1
  27. package/dist/coding-ui.js +30 -29
  28. package/dist/contracts/cli.d.ts +1 -3
  29. package/dist/contracts/console-api.d.ts +1 -1
  30. package/dist/contracts/console-api.js +6 -5
  31. package/dist/contracts/health.d.ts +1 -3
  32. package/dist/control-ui.d.ts +5 -4
  33. package/dist/control-ui.js +9 -34
  34. package/dist/credentials-ui.d.ts +5 -4
  35. package/dist/credentials-ui.js +23 -24
  36. package/dist/demo-chat.d.ts +3 -3
  37. package/dist/demo-chat.js +29 -76
  38. package/dist/discord-settings.d.ts +3 -2
  39. package/dist/discord-settings.js +17 -42
  40. package/dist/envelope.js +10 -0
  41. package/dist/evidence-pack.d.ts +2 -1
  42. package/dist/evidence-pack.js +45 -49
  43. package/dist/export-ui.d.ts +7 -2
  44. package/dist/export-ui.js +4 -10
  45. package/dist/flow-gallery-ui.d.ts +4 -3
  46. package/dist/flow-gallery-ui.js +28 -43
  47. package/dist/flow-starters-ui.d.ts +2 -1
  48. package/dist/flow-starters-ui.js +12 -15
  49. package/dist/flow-triggers.d.ts +1 -0
  50. package/dist/flow-triggers.js +16 -14
  51. package/dist/flows-ui.d.ts +4 -3
  52. package/dist/flows-ui.js +19 -30
  53. package/dist/flows.js +2 -1
  54. package/dist/guides.js +108 -3
  55. package/dist/html.d.ts +76 -0
  56. package/dist/html.js +173 -0
  57. package/dist/integrations-ui.d.ts +2 -1
  58. package/dist/integrations-ui.js +19 -23
  59. package/dist/kits-ui.d.ts +5 -4
  60. package/dist/kits-ui.js +18 -20
  61. package/dist/knowledge-ui.d.ts +6 -5
  62. package/dist/knowledge-ui.js +26 -25
  63. package/dist/lead-context.d.ts +11 -1
  64. package/dist/lead-context.js +6 -8
  65. package/dist/ledger-view.d.ts +2 -1
  66. package/dist/ledger-view.js +27 -29
  67. package/dist/limits-ui.d.ts +2 -1
  68. package/dist/limits-ui.js +7 -7
  69. package/dist/live-bus.d.ts +50 -8
  70. package/dist/live-bus.js +133 -50
  71. package/dist/mcp-oauth.js +35 -36
  72. package/dist/models-ui.d.ts +2 -1
  73. package/dist/models-ui.js +25 -35
  74. package/dist/monitoring-ui.d.ts +4 -3
  75. package/dist/monitoring-ui.js +13 -26
  76. package/dist/openrouter-models.d.ts +2 -1
  77. package/dist/openrouter-models.js +5 -9
  78. package/dist/operate.js +11 -7
  79. package/dist/people-audit-ui.d.ts +2 -1
  80. package/dist/people-audit-ui.js +13 -13
  81. package/dist/policy-ui.d.ts +3 -2
  82. package/dist/policy-ui.js +17 -23
  83. package/dist/project-delete-ui.d.ts +4 -3
  84. package/dist/project-delete-ui.js +15 -27
  85. package/dist/pull-request-ui.d.ts +2 -1
  86. package/dist/pull-request-ui.js +17 -32
  87. package/dist/recipe-ui.d.ts +8 -7
  88. package/dist/recipe-ui.js +39 -32
  89. package/dist/release-adapters.d.ts +60 -0
  90. package/dist/release-adapters.js +435 -0
  91. package/dist/release-cli.d.ts +24 -0
  92. package/dist/release-cli.js +181 -0
  93. package/dist/release-gate.d.ts +80 -0
  94. package/dist/release-gate.js +150 -0
  95. package/dist/release-links.d.ts +29 -0
  96. package/dist/release-links.js +102 -0
  97. package/dist/release.d.ts +331 -0
  98. package/dist/release.js +550 -0
  99. package/dist/reply-shape.d.ts +2 -1
  100. package/dist/reply-shape.js +5 -5
  101. package/dist/repository-context-ui.d.ts +2 -1
  102. package/dist/repository-context-ui.js +2 -7
  103. package/dist/request-budget-ui.d.ts +2 -1
  104. package/dist/request-budget-ui.js +16 -16
  105. package/dist/result-review.d.ts +2 -3
  106. package/dist/result-review.js +3 -9
  107. package/dist/retention-ui.d.ts +7 -1
  108. package/dist/retention-ui.js +7 -11
  109. package/dist/serve.d.ts +11 -2
  110. package/dist/serve.js +69 -58
  111. package/dist/server/chat.d.ts +3 -6
  112. package/dist/server/chat.js +969 -1006
  113. package/dist/server/chrome.d.ts +291 -0
  114. package/dist/server/chrome.js +747 -0
  115. package/dist/server/console.css +2819 -0
  116. package/dist/server/flows.d.ts +2 -6
  117. package/dist/server/flows.js +840 -766
  118. package/dist/server/guards.d.ts +1 -1
  119. package/dist/server/guards.js +5 -2
  120. package/dist/server/handler-context.d.ts +1 -1
  121. package/dist/server/handler-context.js +1 -1
  122. package/dist/server/handler-registry.d.ts +8 -6
  123. package/dist/server/handler-registry.js +14 -2
  124. package/dist/server/http.d.ts +274 -0
  125. package/dist/server/http.js +239 -0
  126. package/dist/server/live.d.ts +34 -0
  127. package/dist/server/live.js +309 -0
  128. package/dist/server/pages.d.ts +2 -6
  129. package/dist/server/pages.js +808 -789
  130. package/dist/server/people-tokens.d.ts +1 -6
  131. package/dist/server/people-tokens.js +580 -572
  132. package/dist/server/remote.d.ts +4 -7
  133. package/dist/server/remote.js +37 -70
  134. package/dist/server/render-chat.d.ts +402 -0
  135. package/dist/server/render-chat.js +927 -0
  136. package/dist/server/render-pages.d.ts +535 -0
  137. package/dist/server/render-pages.js +1277 -0
  138. package/dist/server/render-people.d.ts +32 -0
  139. package/dist/server/render-people.js +121 -0
  140. package/dist/server/render-results.d.ts +744 -0
  141. package/dist/server/render-results.js +2237 -0
  142. package/dist/server/render-settings.d.ts +147 -0
  143. package/dist/server/render-settings.js +577 -0
  144. package/dist/server/render-tasks.d.ts +632 -0
  145. package/dist/server/render-tasks.js +2345 -0
  146. package/dist/server/request-context.d.ts +46 -0
  147. package/dist/server/request-context.js +17 -0
  148. package/dist/server/route-table.d.ts +25 -3
  149. package/dist/server/route-table.js +163 -75
  150. package/dist/server/runtime.d.ts +15 -7
  151. package/dist/server/runtime.js +7 -1
  152. package/dist/server/session.d.ts +138 -0
  153. package/dist/server/session.js +94 -0
  154. package/dist/server/settings.d.ts +1 -5
  155. package/dist/server/settings.js +2181 -2117
  156. package/dist/server/task-views.d.ts +11 -4
  157. package/dist/server/task-views.js +26 -22
  158. package/dist/server/tasks.d.ts +9 -9
  159. package/dist/server/tasks.js +1486 -1418
  160. package/dist/server-telemetry.d.ts +4 -3
  161. package/dist/server-telemetry.js +4 -8
  162. package/dist/skills-ui.d.ts +4 -3
  163. package/dist/skills-ui.js +14 -17
  164. package/dist/skills.d.ts +1 -1
  165. package/dist/skills.js +2 -1
  166. package/dist/slack-settings.d.ts +3 -2
  167. package/dist/slack-settings.js +17 -42
  168. package/dist/spend-ui.d.ts +2 -1
  169. package/dist/spend-ui.js +9 -32
  170. package/dist/sso-ui.d.ts +3 -2
  171. package/dist/sso-ui.js +26 -27
  172. package/dist/storage-ui.d.ts +2 -1
  173. package/dist/storage-ui.js +14 -33
  174. package/dist/surface.js +14 -3
  175. package/dist/task-composer.d.ts +2 -1
  176. package/dist/task-composer.js +7 -7
  177. package/dist/task-status.d.ts +4 -3
  178. package/dist/task-status.js +15 -16
  179. package/dist/team-http.d.ts +5 -9
  180. package/dist/team-http.js +5 -63
  181. package/dist/team-ui.d.ts +2 -1
  182. package/dist/team-ui.js +2 -5
  183. package/dist/teammates-ui.d.ts +5 -4
  184. package/dist/teammates-ui.js +61 -90
  185. package/dist/teams-settings.d.ts +3 -2
  186. package/dist/teams-settings.js +20 -31
  187. package/dist/telegram-settings.d.ts +3 -2
  188. package/dist/telegram-settings.js +14 -25
  189. package/dist/toolroll-update-ui.d.ts +3 -2
  190. package/dist/toolroll-update-ui.js +35 -39
  191. package/dist/tools-ui.d.ts +5 -2
  192. package/dist/tools-ui.js +31 -31
  193. package/dist/when-html.d.ts +11 -3
  194. package/dist/when-html.js +5 -6
  195. package/dist/workspace-ui.d.ts +4 -2
  196. package/dist/workspace-ui.js +10 -10
  197. package/package.json +1 -1
  198. package/dist/server/shared.d.ts +0 -3151
  199. package/dist/server/shared.js +0 -12001
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.55 — 2026-10-09
4
+
5
+ - **One door per route.** Every page's access comes from one table with one
6
+ handler each; pages escape automatically and forms carry CSRF protection.
7
+ - **One live stream.** It re-checks your session and project access before
8
+ every update.
9
+ - **One-command release.** `toolroll release <branch>` runs a whole release
10
+ after your scope approval and never merges on pending or failed CI.
11
+
3
12
  ## 0.9.54 — 2026-10-09
4
13
 
5
14
  - **One chat core.** Telegram, Slack, Discord and Teams now share one chat
package/README.md CHANGED
@@ -477,7 +477,7 @@ toolroll task block api --on schema # api waits for schema
477
477
  toolroll ready --json # what could be dispatched now
478
478
  toolroll claim schema --runner builder-1 --key dispatch-schema
479
479
  toolroll heartbeat <lease> # still working
480
- toolroll release <lease> # done holding it
480
+ toolroll worker release <lease> # done holding it
481
481
  ```
482
482
 
483
483
  Four properties make that loop safe to run unattended.
@@ -540,9 +540,10 @@ service unit never carries it. Cron remains first-class if you prefer it —
540
540
  cron's cadence, and a stray cron tick alongside a watch is safe (ordinary
541
541
  claims settle the race), it just is not needed.
542
542
 
543
- An honesty note for Windows: every pull request now type-checks, builds, and
544
- runs the native Task Scheduler/link tests plus the core dispatch contract on
545
- Windows with Node 22 and 24. Approved setup and verification commands use
543
+ An honesty note for Windows: every pull request builds Toolroll on Windows
544
+ with Node 22 and runs the native Job Object containment tests there; the full
545
+ suite runs on Ubuntu (Node 22 and 24), and on macOS nightly and for every
546
+ release. Approved setup and verification commands use
546
547
  Windows' native command shell. The scheduled-task definition follows the Task
547
548
  Scheduler XML schema and every `schtasks` interaction is covered by scripted
548
549
  tests. A physical Windows install has not yet been certified; the exact
@@ -86,14 +86,17 @@ export function skillFingerprint(content) {
86
86
  export const RELEASED_SKILLS = [
87
87
  "6ff9b4458f332636b6c5968c416bee4770564ef0190d62ec8640e84051fb7f60", // skills install --claude-code
88
88
  "3fb6741eec6d394a0a41604cae321da05d85c74f440424b740151c131f125742", // the operator skill
89
+ "448fc51dc200b4d46c53bf191fd7ecee7c4f7487413bf4dbfb1f984a3b401993", // skills install --claude-code, with the release guide
89
90
  ];
90
91
  const KNOWN_SKILLS = new Set([...RELEASED_SKILLS, ...[operatorSkillContent("0.0.0"), claudeCodeSkillContent()].map(skillFingerprint)]);
91
- /** SHA-256 of every guide copy (console.md, operating.md, runner.md) an earlier `skills install --claude-code` wrote
92
+ /** SHA-256 of every guide copy (console.md, operating.md, runner.md, release.md) an earlier `skills install --claude-code` wrote
92
93
  * beside SKILL.md, and of this build's. Only an exact one of these is removed: an edited copy is the person's. */
93
94
  const RELEASED_GUIDES = [
94
95
  "a5aa20f93d803fd49bbac2908e46353a4f90c95c2bb57993ee724770c28adb0b", "65fc1774c4d6dd99e71a7cfb245a61148ac74889765477ddd5066703cd28f55c", "01604f373b92de737ae7f75804c7f7f09dd690ed96a3e676be75f71358819f23",
95
96
  "1e1ee5c46c84b65c931e86c005802478415ec026b6240367e7f12a8d66d608eb", "e5270e70eb09f202980a236a0ae36ac2e1c3df91840c24d40a6a8bd4c0d47fe8", "969dd5e97b63f27dc559889a551f4392bcf289f2b47ce802338868f06e05f1c9",
96
97
  "bea301cb7b7d7711afb165b72c355ff829e6341abb75c59431c4a0a7c129b6cd",
98
+ "4b087ed3fdb60b8b7c56a2312a990eab1a854078592e355d852f4a4c5b13c752", "ac61340c17d780994052cd8ed9a0541f1fa64b80f95fc7e915e6899baa163caa", "a11a8576cc17a9e40ecfbf01503979298a4b003a4badffbd1dbd455b0eafde64",
99
+ "37518ca87fdbb54ff649289ec1893232137fd30c9db6e29cd1fc4bc9ba5e2f1a",
97
100
  ];
98
101
  const guideHash = (content) => createHash("sha256").update(content).digest("hex");
99
102
  const KNOWN_GUIDES = new Set([...RELEASED_GUIDES, ...CLAUDE_CODE_GUIDES.map(name => guideHash(claudeCodeGuideContent(name)))]);
@@ -4,6 +4,7 @@
4
4
  * the identity provider); everyone else reads them. Every change is in the
5
5
  * ledger, before → after.
6
6
  */
7
+ import { type Html } from "./html.js";
7
8
  import type { ApprovalRules } from "./approval-policy.js";
8
9
  export declare const APPROVAL_RULES_CSS: string;
9
10
  export type ApprovalRulesView = {
@@ -19,7 +20,7 @@ export type ApprovalRulesView = {
19
20
  };
20
21
  /** The rules in plain words, for anyone who can see the project. */
21
22
  export declare function rulesSummary(rules: ApprovalRules): string;
22
- export declare function approvalRulesHtml(view: ApprovalRulesView, csrf: string, notice: {
23
+ export declare function approvalRulesHtml(view: ApprovalRulesView, notice: {
23
24
  said?: string | null;
24
25
  problem?: string | null;
25
- }): string;
26
+ }): Html;
@@ -1,4 +1,10 @@
1
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1
+ /**
2
+ * Settings → Approval rules (v102): a project's separation of duties. An
3
+ * instance operator sets them (with their password, or a fresh sign-in with
4
+ * the identity provider); everyone else reads them. Every change is in the
5
+ * ledger, before → after.
6
+ */
7
+ import { html, postForm } from "./html.js";
2
8
  export const APPROVAL_RULES_CSS = `.approval-rules{max-width:720px;min-width:0}.approval-rules fieldset{border:0;padding:0;margin:18px 0 0;min-width:0}` +
3
9
  `.approval-rules legend{font-weight:600;font-size:.875rem;margin-bottom:8px}.approval-rules .choice{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;align-items:start;margin:0 0 10px;font-weight:400}` +
4
10
  `.approval-rules .choice input{margin:.2rem 0 0}.approval-rules .choice strong{display:block;font-size:.875rem;font-weight:500}.approval-rules .choice small{display:block;color:var(--muted-foreground);font-size:.8125rem;margin-top:2px}` +
@@ -15,25 +21,14 @@ export function rulesSummary(rules) {
15
21
  parts.push(`Work touching ${rules.protectedPaths.join(", ")} is protected: two people approve it, never an AI teammate or an operating mode.`);
16
22
  return parts.length === 0 ? "No approval rules: anyone who can approve on this project can approve its work, including their own." : parts.join(" ");
17
23
  }
18
- export function approvalRulesHtml(view, csrf, notice) {
19
- const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
20
- const changed = view.rules.updatedBy === null ? "" : ` <span class="meta">Changed by ${e(view.rules.updatedBy)} on ${e((view.rules.updatedAt ?? "").slice(0, 10))}.</span>`;
21
- const current = `<p class="current">${e(rulesSummary(view.rules))}${changed}</p>`;
24
+ export function approvalRulesHtml(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>` : "";
26
+ const changed = view.rules.updatedBy === null ? "" : html ` <span class="meta">Changed by ${view.rules.updatedBy} on ${(view.rules.updatedAt ?? "").slice(0, 10)}.</span>`;
27
+ const current = html `<p class="current">${rulesSummary(view.rules)}${changed}</p>`;
22
28
  if (!view.canChange)
23
- return `<section class="approval-rules">${note}${current}<p class="meta">An instance operator sets these.</p></section>`;
29
+ return html `<section class="approval-rules">${note}${current}<p class="meta">An instance operator sets these.</p></section>`;
24
30
  const protect = view.rules.protectProject ? "project" : view.rules.protectedPaths.length > 0 ? "paths" : "none";
25
- const radio = (value, title, hint) => `<label class="choice"><input type="radio" name="protect" value="${value}"${protect === value ? " checked" : ""}><span><strong>${title}</strong><small>${hint}</small></span></label>`;
26
- const few = view.approvers < 2 ? `<p class="meta">Only ${view.approvers === 1 ? "one person" : "no one"} can approve on this project, so protected work would wait for a second approver. Add one in People first.</p>` : "";
27
- return `<section class="approval-rules">${note}${current}` +
28
- `<form method="post" action="/settings/approval"><input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="repo" value="${e(view.repo)}">` +
29
- `<fieldset><legend>Who approves</legend><label class="choice"><input type="checkbox" name="not_requester" value="1"${view.rules.notRequester ? " checked" : ""}>` +
30
- `<span><strong>Someone other than the requester</strong><small>The person who filed a task, or asked for a revision, can't approve it. Operating modes stop approving their signer's own filings here.</small></span></label></fieldset>` +
31
- `<fieldset><legend>Protected work</legend>` +
32
- radio("none", "Nothing protected", "One approval is enough.") +
33
- radio("project", "The whole project", "Every task needs two different people to approve the same scope.") +
34
- radio("paths", "Only these paths", "Tasks that touch them, or don't say what they touch, need two people.") +
35
- `<div class="paths"><label>Protected paths, one per line<textarea name="paths" spellcheck="false" placeholder="infra/**&#10;migrations/**">${e(view.rules.protectedPaths.join("\n"))}</textarea></label></div>` +
36
- few + `</fieldset>` +
37
- `<div class="step-up"><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label></div>` +
38
- `<button type="submit">Save rules</button></form></section>`;
31
+ const radio = (value, title, hint) => html `<label class="choice"><input type="radio" name="protect" value="${value}"${protect === value ? html ` checked` : ""}><span><strong>${title}</strong><small>${hint}</small></span></label>`;
32
+ const few = view.approvers < 2 ? html `<p class="meta">Only ${view.approvers === 1 ? "one person" : "no one"} can approve on this project, so protected work would wait for a second approver. Add one in People first.</p>` : "";
33
+ return html `<section class="approval-rules">${note}${current}${postForm("/settings/approval", html `<fieldset><legend>Who approves</legend><label class="choice"><input type="checkbox" name="not_requester" value="1"${view.rules.notRequester ? html ` checked` : ""}><span><strong>Someone other than the requester</strong><small>The person who filed a task, or asked for a revision, can't approve it. Operating modes stop approving their signer's own filings here.</small></span></label></fieldset><fieldset><legend>Protected work</legend>${radio("none", "Nothing protected", html `One approval is enough.`)}${radio("project", "The whole project", html `Every task needs two different people to approve the same scope.`)}${radio("paths", "Only these paths", html `Tasks that touch them, or don't say what they touch, need two people.`)}<div class="paths"><label>Protected paths, one per line<textarea name="paths" spellcheck="false" placeholder="infra/**&#10;migrations/**">${view.rules.protectedPaths.join("\n")}</textarea></label></div>${few}</fieldset><div class="step-up"><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label></div><button type="submit">Save rules</button>`, { hidden: { repo: view.repo } })}</section>`;
39
34
  }
@@ -4,6 +4,7 @@ import type { AssignmentSnapshot } from './assignment.js';
4
4
  import type { DisplayStatus, StatusTone, WorkStatus } from './workspace-ui.js';
5
5
  import { type AssignmentStatusOptions, type AssignmentWorkStatus } from './assignment-presentation.js';
6
6
  import { type PullRequestFact, type TaskStatus } from './task-status.js';
7
+ import { type Html } from './html.js';
7
8
  /** The existing verified receipt reader may discover damage after a verdict
8
9
  * was saved. Keep the limitation visible without inventing another work stage. */
9
10
  export declare function assignmentWithEvidence(assignment: AssignmentSnapshot, resultStatus: DisplayStatus | null, resultRunId?: number | null): AssignmentSnapshot;
@@ -19,7 +20,7 @@ export type AssignmentAttemptRow = {
19
20
  };
20
21
  /** Every version of the assignment, oldest first, as the card lists them. */
21
22
  export declare function assignmentAttemptsOf(assignment: AssignmentSnapshot): AssignmentAttemptRow[];
22
- export declare function assignmentAttemptsHtml(assignment: AssignmentSnapshot): string;
23
+ export declare function assignmentAttemptsHtml(assignment: AssignmentSnapshot): Html;
23
24
  type AssignmentCardOptions = Omit<AssignmentStatusOptions, 'links'> & {
24
25
  hideAction?: boolean;
25
26
  problem?: boolean;
@@ -61,7 +62,7 @@ export declare function assignmentCardOf(assignment: AssignmentSnapshot, options
61
62
  export declare function assignmentSummaryHtml(assignment: AssignmentSnapshot, options?: AssignmentCardOptions & {
62
63
  compact?: boolean;
63
64
  sentence?: string | undefined;
64
- }): string;
65
+ }): Html;
65
66
  /** Shared assignment layout; actions remain comfortable on touch screens. */
66
67
  export declare const ASSIGNMENT_CSS = ".assignment-summary{min-width:0;overflow-wrap:anywhere}.assignment-summary .assignment-state{font-weight:600;margin:0}.assignment-summary h2.assignment-state{font-size:1.125rem;color:var(--foreground)}.assignment-summary>.assignment-attempts,.assignment-summary>.assignment-notices{border:0;box-shadow:none;padding:0;margin:.25rem 0 0;background:transparent}.assignment-summary>.button-link{margin-top:.7rem;min-height:44px}.assignment-summary>.work-action{display:inline-flex;margin-inline-start:.6rem;min-height:44px;align-items:center}.assignment-detail{margin:.55rem 0}.assignment-attempts summary,.assignment-notices summary{min-height:44px;display:list-item;align-content:center;cursor:pointer}.assignment-notices summary{font-size:.8125rem;color:var(--foreground)}.assignment-notices p{margin:.25rem 0 .5rem}.assignment-attempts ol{padding-left:1.4rem}.assignment-attempts li{margin:.4rem 0}.assignment-attempts li a{display:inline-flex;align-items:center;min-height:44px;margin-right:.7rem}.assignment-attempts li p{margin:0 0 .4rem}";
67
68
  export {};
@@ -2,7 +2,7 @@ import { assignmentPresentationOf, shortenedMaterialReason } from './assignment-
2
2
  import { plainReasonWords } from './proof.js';
3
3
  import { chatResultHref } from './chat-controls.js';
4
4
  import { statusDetailsHtml, statusWhyHtml } from './task-status.js';
5
- const escape = (value) => value.replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[char]);
5
+ import { html } from './html.js';
6
6
  /** The existing verified receipt reader may discover damage after a verdict
7
7
  * was saved. Keep the limitation visible without inventing another work stage. */
8
8
  export function assignmentWithEvidence(assignment, resultStatus, resultRunId = assignment.receipt?.runId ?? null) {
@@ -58,8 +58,8 @@ export function assignmentAttemptsOf(assignment) {
58
58
  }
59
59
  export function assignmentAttemptsHtml(assignment) {
60
60
  if (assignment.attempts.length === 0)
61
- return '';
62
- return `<details class="assignment-attempts"><summary>Attempts <span class="meta">${assignment.attempts.length}</span></summary><ol>` + assignmentAttemptsOf(assignment).map(attempt => `<li data-attempt-task="${escape(attempt.taskId)}" data-history-version="${escape(attempt.taskId)}"><a href="${escape(attempt.href)}">${escape(attempt.label)}</a>${attempt.runId === null ? '' : ` <a href="/r/${attempt.runId}">Run #${attempt.runId}</a>`}${attempt.detail === null ? '' : `<p class="meta">${escape(attempt.detail)}</p>`}</li>`).join('') + `</ol>${assignment.owner === null ? '' : `<p class="meta">Lead: ${escape(assignment.owner.label)}${assignment.owner.active ? '' : ' · access ended'}</p>`}<p class="work-meta work-id">Task <span class="mono">${escape(assignment.rootId)}</span></p></details>`;
61
+ return html ``;
62
+ return html `<details class="assignment-attempts"><summary>Attempts <span class="meta">${assignment.attempts.length}</span></summary><ol>${assignmentAttemptsOf(assignment).map(attempt => html `<li data-attempt-task="${attempt.taskId}" data-history-version="${attempt.taskId}"><a href="${attempt.href}">${attempt.label}</a>${attempt.runId === null ? '' : html ` <a href="/r/${attempt.runId}">Run #${attempt.runId}</a>`}${attempt.detail === null ? '' : html `<p class="meta">${attempt.detail}</p>`}</li>`)}</ol>${assignment.owner === null ? '' : html `<p class="meta">Lead: ${assignment.owner.label}${assignment.owner.active ? '' : ' · access ended'}</p>`}<p class="work-meta work-id">Task <span class="mono">${assignment.rootId}</span></p></details>`;
63
63
  }
64
64
  /** Only recognized storage limits and explicitly identified older records are
65
65
  * secondary. Unknown findings and damaged current material stay in view. */
@@ -100,14 +100,7 @@ export function assignmentCardOf(assignment, options = {}) {
100
100
  export function assignmentSummaryHtml(assignment, options = {}) {
101
101
  const card = assignmentCardOf(assignment, options);
102
102
  const tone = card.status.tone;
103
- return `<section class="${options.compact ? 'assignment-summary' : 'card assignment-summary'}" aria-label="assignment progress" data-assignment="${escape(assignment.rootId)}" data-work-status="${card.token}" data-tone="${card.tone}" data-headline="${escape(card.status.headline)}" data-headline-tone="${tone}"${options.compact ? '' : ' data-task-status'}>` +
104
- (options.compact ? `<span class="status-line" data-work-status="${card.token}" data-tone="${card.tone}"><i class="status-dot" aria-hidden="true"></i><span class="status-label">${card.label}</span></span>` : `<h2 class="assignment-state status-headline"><i aria-hidden="true"></i>${escape(card.label)}</h2>`) +
105
- `<p class="meta assignment-detail status-sentence">${escape(options.sentence ?? card.status.sentence)}</p>` +
106
- (card.action === null ? '' : `<a class="${options.compact ? 'work-action' : 'button-link'}" href="${escape(card.action.href)}"${card.action.openResult ? ' data-open-result' : ''}${options.compact ? '' : ' data-primary-action'}>${escape(card.action.label)}${options.compact ? ' →' : ''}</a>`) +
107
- (options.compact ? '' : statusDetailsHtml(card.status)) +
108
- statusWhyHtml(card.status, card.reasons, card.diagnostics) +
109
- (card.notices === null ? '' : `<details class="assignment-notices"><summary>${card.notices.summary}</summary>${card.notices.lines.map(one => `<p class="meta">${escape(one)}</p>`).join('')}</details>`) +
110
- assignmentAttemptsHtml(assignment) + `</section>`;
103
+ return html `<section class="${options.compact ? 'assignment-summary' : 'card assignment-summary'}" aria-label="assignment progress" data-assignment="${assignment.rootId}" data-work-status="${card.token}" data-tone="${card.tone}" data-headline="${card.status.headline}" data-headline-tone="${tone}"${options.compact ? '' : html ` data-task-status`}>${options.compact ? html `<span class="status-line" data-work-status="${card.token}" data-tone="${card.tone}"><i class="status-dot" aria-hidden="true"></i><span class="status-label">${card.label}</span></span>` : html `<h2 class="assignment-state status-headline"><i aria-hidden="true"></i>${card.label}</h2>`}<p class="meta assignment-detail status-sentence">${options.sentence ?? card.status.sentence}</p>${card.action === null ? '' : html `<a class="${options.compact ? 'work-action' : 'button-link'}" href="${card.action.href}"${card.action.openResult ? html ` data-open-result` : ''}${options.compact ? '' : html ` data-primary-action`}>${card.action.label}${options.compact ? ' →' : ''}</a>`}${options.compact ? '' : statusDetailsHtml(card.status)}${statusWhyHtml(card.status, card.reasons, card.diagnostics)}${card.notices === null ? '' : html `<details class="assignment-notices"><summary>${card.notices.summary}</summary>${card.notices.lines.map(one => html `<p class="meta">${one}</p>`)}</details>`}${assignmentAttemptsHtml(assignment)}</section>`;
111
104
  }
112
105
  /** Shared assignment layout; actions remain comfortable on touch screens. */
113
106
  export const ASSIGNMENT_CSS = `.assignment-summary{min-width:0;overflow-wrap:anywhere}.assignment-summary .assignment-state{font-weight:600;margin:0}.assignment-summary h2.assignment-state{font-size:1.125rem;color:var(--foreground)}.assignment-summary>.assignment-attempts,.assignment-summary>.assignment-notices{border:0;box-shadow:none;padding:0;margin:.25rem 0 0;background:transparent}.assignment-summary>.button-link{margin-top:.7rem;min-height:44px}.assignment-summary>.work-action{display:inline-flex;margin-inline-start:.6rem;min-height:44px;align-items:center}.assignment-detail{margin:.55rem 0}.assignment-attempts summary,.assignment-notices summary{min-height:44px;display:list-item;align-content:center;cursor:pointer}.assignment-notices summary{font-size:.8125rem;color:var(--foreground)}.assignment-notices p{margin:.25rem 0 .5rem}.assignment-attempts ol{padding-left:1.4rem}.assignment-attempts li{margin:.4rem 0}.assignment-attempts li a{display:inline-flex;align-items:center;min-height:44px;margin-right:.7rem}.assignment-attempts li p{margin:0 0 .4rem}`;
@@ -3,6 +3,7 @@
3
3
  * back up now, then the schedule. Past backups and how to restore sit in
4
4
  * disclosures.
5
5
  */
6
+ import { type Html } from "./html.js";
6
7
  import type { BackupRun, BackupSettings } from "./store.js";
7
8
  /** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
8
9
  export declare const BACKUP_EVERY_HOURS: readonly [1, 6, 12, 24];
@@ -19,4 +20,4 @@ export type BackupView = {
19
20
  export declare function backupHtml(view: BackupView, notice: {
20
21
  said?: string | null;
21
22
  problem?: string | null;
22
- }): string;
23
+ }): Html;
package/dist/backup-ui.js CHANGED
@@ -1,9 +1,14 @@
1
+ /**
2
+ * Settings → Backups (sprint 8): how the last backup went and one button to
3
+ * back up now, then the schedule. Past backups and how to restore sit in
4
+ * disclosures.
5
+ */
6
+ import { html, postForm } from "./html.js";
1
7
  import { bytesWords } from "./storage.js";
2
8
  import { whenUtc } from "./when-html.js";
3
9
  /** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
4
10
  export const BACKUP_EVERY_HOURS = [1, 6, 12, 24];
5
11
  export const MAX_KEEP = 100;
6
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
7
12
  const when = (at) => whenUtc(at);
8
13
  const whenWords = (at) => `${at.slice(0, 16).replace("T", " ")} UTC`;
9
14
  const everyWords = (hours) => hours === 1 ? "Every hour" : hours === 24 ? "Every day" : `Every ${hours} hours`;
@@ -26,36 +31,20 @@ function state(view) {
26
31
  : good === undefined ? "The first scheduled backup runs within a minute or two of the console starting."
27
32
  : `Next one about ${whenWords(new Date(Date.parse(good.startedAt) + view.settings.everyHours * 3_600_000).toISOString())}.`;
28
33
  if (last === undefined)
29
- return `<h2>No backups yet</h2><p class="meta">${e(next)}</p>`;
34
+ return html `<h2>No backups yet</h2><p class="meta">${next}</p>`;
30
35
  if (last.ok === null)
31
- return `<h2>Backing up</h2><p class="meta">Started ${when(last.startedAt)}.</p>`;
36
+ return html `<h2>Backing up</h2><p class="meta">Started ${when(last.startedAt)}.</p>`;
32
37
  if (last.ok === false) {
33
- return `<h2 data-backup-state="failed">Last backup failed</h2><p class="problem" role="alert">${when(last.startedAt)}: ${e(last.error ?? "no reason was recorded")}</p>` +
34
- `<p class="meta">${good === undefined ? "No backup has succeeded yet." : `Last good backup ${when(good.startedAt)}.`} ${e(next)}</p>`;
38
+ return html `<h2 data-backup-state="failed">Last backup failed</h2><p class="problem" role="alert">${when(last.startedAt)}: ${last.error ?? "no reason was recorded"}</p><p class="meta">${good === undefined ? "No backup has succeeded yet." : html `Last good backup ${when(good.startedAt)}.`} ${next}</p>`;
35
39
  }
36
- return `<h2 data-backup-state="ok">Backed up ${when(last.startedAt)}</h2><p class="meta">${last.bytes === null ? "" : `${e(bytesWords(last.bytes))} · `}${e(next)}</p>`;
40
+ return html `<h2 data-backup-state="ok">Backed up ${when(last.startedAt)}</h2><p class="meta">${last.bytes === null ? "" : `${bytesWords(last.bytes)} · `}${next}</p>`;
37
41
  }
38
42
  export function backupHtml(view, notice) {
39
43
  const s = view.settings;
40
- const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
41
- const csrf = `<input type="hidden" name="csrf" value="${e(view.csrf)}">`;
42
- const choices = [`<option value="off"${s.enabled ? "" : " selected"}>Off</option>`,
43
- ...BACKUP_EVERY_HOURS.map(hours => `<option value="${hours}"${s.enabled && s.everyHours === hours ? " selected" : ""}>${everyWords(hours)}</option>`)].join("");
44
- const runs = view.runs.map(run => `<li data-backup-run="${run.id}"><span>${when(run.startedAt)}</span>` +
45
- (run.ok === true ? `<span>Succeeded${run.bytes === null ? "" : ` · ${e(bytesWords(run.bytes))}`}</span>${run.file === null ? "" : `<span class="mono meta">${e(fileName(run.file))}</span>`}`
46
- : run.ok === false ? `<span class="failed">Failed: ${e(run.error ?? "no reason recorded")}</span>` : `<span>Running</span>`) +
47
- `<span class="meta">${run.trigger === "manual" ? "by hand" : "scheduled"}</span></li>`).join("");
48
- return `<section class="backups">${note}` +
49
- `<div class="card backup-state"><div>${state(view)}</div>` +
50
- `<form method="post" action="/settings/backups/now">${csrf}<button type="submit">Back up now</button></form></div>` +
51
- `<h2>Schedule</h2>` +
52
- `<form method="post" action="/settings/backups">${csrf}<fieldset>` +
53
- `<div class="pair"><label>Back up<select name="every">${choices}</select></label>` +
54
- `<label>Keep the newest<input type="number" name="keep" min="1" max="${MAX_KEEP}" step="1" value="${s.keep}" required></label></div>` +
55
- `<label>Folder<input type="text" name="folder" value="${e(s.folder ?? "")}" placeholder="${e(view.defaultFolder)}" spellcheck="false"></label>` +
56
- `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>` +
57
- `</fieldset><button type="submit">Save</button></form>` +
58
- `<details><summary>Recent backups${view.runs.length === 0 ? "" : ` (${view.runs.length})`}</summary>${runs === "" ? `<p class="meta">None yet.</p>` : `<ol>${runs}</ol>`}<p class="meta">In <span class="mono">${e(view.folder)}</span>. Only the database is copied: provider keys and sign-in files stay out.</p></details>` +
59
- `<details><summary>Restore a backup</summary><p>Stop Toolroll, then run:</p><pre><code>toolroll restore &lt;file&gt; --dry-run</code></pre>` +
60
- `<p class="meta">The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code>--dry-run</code> to restore; the current database is kept as a copy first.</p></details></section>`;
44
+ const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
45
+ const choices = [html `<option value="off"${s.enabled ? "" : html ` selected`}>Off</option>`,
46
+ ...BACKUP_EVERY_HOURS.map(hours => html `<option value="${hours}"${s.enabled && s.everyHours === hours ? html ` selected` : ""}>${everyWords(hours)}</option>`)];
47
+ const runs = view.runs.map(run => html `<li data-backup-run="${run.id}"><span>${when(run.startedAt)}</span>${run.ok === true ? html `<span>Succeeded${run.bytes === null ? "" : ` · ${bytesWords(run.bytes)}`}</span>${run.file === null ? "" : html `<span class="mono meta">${fileName(run.file)}</span>`}`
48
+ : run.ok === false ? html `<span class="failed">Failed: ${run.error ?? "no reason recorded"}</span>` : html `<span>Running</span>`}<span class="meta">${run.trigger === "manual" ? "by hand" : "scheduled"}</span></li>`);
49
+ return html `<section class="backups">${note}<div class="card backup-state"><div>${state(view)}</div>${postForm("/settings/backups/now", html `<button type="submit">Back up now</button>`)}</div><h2>Schedule</h2>${postForm("/settings/backups", html `<fieldset><div class="pair"><label>Back up<select name="every">${choices}</select></label><label>Keep the newest<input type="number" name="keep" min="1" max="${MAX_KEEP}" step="1" value="${s.keep}" required></label></div><label>Folder<input type="text" name="folder" value="${s.folder ?? ""}" placeholder="${view.defaultFolder}" spellcheck="false"></label><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label></fieldset><button type="submit">Save</button>`)}<details><summary>Recent backups${view.runs.length === 0 ? "" : ` (${view.runs.length})`}</summary>${runs.length === 0 ? html `<p class="meta">None yet.</p>` : html `<ol>${runs}</ol>`}<p class="meta">In <span class="mono">${view.folder}</span>. Only the database is copied: provider keys and sign-in files stay out.</p></details><details><summary>Restore a backup</summary><p>Stop Toolroll, then run:</p><pre><code>toolroll restore &lt;file&gt; --dry-run</code></pre><p class="meta">The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code>--dry-run</code> to restore; the current database is kept as a copy first.</p></details></section>`;
61
50
  }
@@ -7,6 +7,7 @@
7
7
  * brandIconHtml, or BrandIcon in React (browser/brand-mark.tsx).
8
8
  */
9
9
  import { type BrandIconId } from "./brand-icons.js";
10
+ import { type Html } from "./html.js";
10
11
  /**
11
12
  * The icon for a service id, catalog tool name, chat app or integration key
12
13
  * ("telegram", "agent:codex", "mcp:<repo>:sentry"), or null for a letter.
@@ -15,12 +16,12 @@ export declare function brandIconFor(name: string): BrandIconId | null;
15
16
  /** The letter a mark without a logo shows. */
16
17
  export declare function brandLetter(label: string): string;
17
18
  /** The logo alone, decorative: an svg its surroundings size and colour. */
18
- export declare function brandIconHtml(id: BrandIconId): string;
19
+ export declare function brandIconHtml(id: BrandIconId): Html;
19
20
  /**
20
21
  * The mark, decorative (the name always sits beside it). `name` null is a
21
22
  * letter whatever the label: a custom tool never borrows a logo by its name.
22
23
  */
23
- export declare function brandMarkHtml(name: string | null, label: string, connected: boolean): string;
24
+ export declare function brandMarkHtml(name: string | null, label: string, connected: boolean): Html;
24
25
  /** Where the integration tiles (Settings → Tools) switch to their phone layout, and the mark with them. */
25
26
  export declare const TILE_PHONE = "@media(max-width:600px)";
26
27
  /** The mark's own sizes; `--brand-mark` is its side, for layouts that line up beside it. */
@@ -7,6 +7,7 @@
7
7
  * brandIconHtml, or BrandIcon in React (browser/brand-mark.tsx).
8
8
  */
9
9
  import { BRAND_ICONS } from "./brand-icons.js";
10
+ import { html } from "./html.js";
10
11
  /** Service ids, catalog tool names and integration keys that differ from their icon's id. */
11
12
  const ALIASES = {
12
13
  atlassian: "jira", "chrome-devtools": "chrome", codex: "openai", "microsoft-teams": "teams", "better-stack": "betterstack",
@@ -25,11 +26,10 @@ export function brandIconFor(name) {
25
26
  export function brandLetter(label) {
26
27
  return (label.trim().match(/[\p{L}\p{N}]/u)?.[0] ?? "?").toUpperCase();
27
28
  }
28
- const e = (value) => value.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
29
29
  /** The logo alone, decorative: an svg its surroundings size and colour. */
30
30
  export function brandIconHtml(id) {
31
31
  const icon = BRAND_ICONS[id];
32
- return `<svg viewBox="0 0 24 24" focusable="false" aria-hidden="true"><path fill="currentColor"${icon.evenodd ? ` fill-rule="evenodd"` : ""} d="${icon.path}"/></svg>`;
32
+ return html `<svg viewBox="0 0 24 24" focusable="false" aria-hidden="true"><path fill="currentColor"${icon.evenodd && html ` fill-rule="evenodd"`} d="${icon.path}"/></svg>`;
33
33
  }
34
34
  /**
35
35
  * The mark, decorative (the name always sits beside it). `name` null is a
@@ -37,8 +37,8 @@ export function brandIconHtml(id) {
37
37
  */
38
38
  export function brandMarkHtml(name, label, connected) {
39
39
  const icon = name === null ? null : brandIconFor(name);
40
- const inside = icon === null ? e(brandLetter(label)) : brandIconHtml(icon);
41
- return `<span class="brand-mark" data-connected="${connected}"${icon === null ? " data-letter" : ""} aria-hidden="true">${inside}</span>`;
40
+ const inside = icon === null ? brandLetter(label) : brandIconHtml(icon);
41
+ return html `<span class="brand-mark" data-connected="${String(connected)}"${icon === null && html ` data-letter`} aria-hidden="true">${inside}</span>`;
42
42
  }
43
43
  /** Where the integration tiles (Settings → Tools) switch to their phone layout, and the mark with them. */
44
44
  export const TILE_PHONE = "@media(max-width:600px)";