toolroll 0.9.85 → 0.9.87

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 (220) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/agent-fence.d.ts +6 -2
  3. package/dist/agent-fence.js +3 -2
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/assignment.js +4 -3
  10. package/dist/backup-ui.d.ts +5 -4
  11. package/dist/backup-ui.js +65 -32
  12. package/dist/browser/terminal.css +1 -1
  13. package/dist/browser/terminal.js +57 -54
  14. package/dist/browser/workspace.css +2 -2
  15. package/dist/browser/workspace.js +35 -68
  16. package/dist/browser-crew.js +1 -1
  17. package/dist/browser-workspace.d.ts +363 -177
  18. package/dist/browser-workspace.js +10 -9
  19. package/dist/chat-actions.d.ts +11 -1
  20. package/dist/chat-actions.js +53 -1
  21. package/dist/chat-channel.js +1 -0
  22. package/dist/chat-controls.d.ts +1 -1
  23. package/dist/chat-controls.js +1 -1
  24. package/dist/chat-quiet.js +3 -2
  25. package/dist/chat-settings-page.d.ts +29 -9
  26. package/dist/chat-settings-page.js +59 -28
  27. package/dist/check-levels-ui.d.ts +2 -1
  28. package/dist/check-levels-ui.js +67 -58
  29. package/dist/claim.d.ts +3 -0
  30. package/dist/claim.js +4 -1
  31. package/dist/contracts/chat-actions.d.ts +9 -0
  32. package/dist/contracts/chat-actions.js +3 -0
  33. package/dist/contracts/cli.d.ts +43 -0
  34. package/dist/contracts/cli.js +4 -0
  35. package/dist/contracts/console-api.d.ts +4 -1
  36. package/dist/contracts/console-api.js +4 -1
  37. package/dist/contracts/flow-propose.d.ts +4 -1
  38. package/dist/contracts/flow-propose.js +1 -1
  39. package/dist/contracts/flow.d.ts +41 -0
  40. package/dist/contracts/flow.js +4 -2
  41. package/dist/contracts/lead-tools.d.ts +21 -0
  42. package/dist/contracts/lead-tools.js +2 -0
  43. package/dist/contracts/store-json.d.ts +2 -0
  44. package/dist/contracts/trigger-payloads.d.ts +20 -0
  45. package/dist/contracts/trigger-payloads.js +4 -0
  46. package/dist/coordinator.js +1 -0
  47. package/dist/credentials-ui.d.ts +5 -3
  48. package/dist/credentials-ui.js +58 -31
  49. package/dist/deploy-gate.js +4 -0
  50. package/dist/design/kit.css +685 -0
  51. package/dist/design/kit.d.ts +208 -0
  52. package/dist/design/kit.js +177 -0
  53. package/dist/design/tokens.css +19 -1
  54. package/dist/discord-settings.js +8 -4
  55. package/dist/dispatch.d.ts +3 -0
  56. package/dist/dispatch.js +4 -1
  57. package/dist/export-ui.d.ts +6 -4
  58. package/dist/export-ui.js +18 -8
  59. package/dist/flow-browser.d.ts +103 -0
  60. package/dist/flow-browser.js +204 -0
  61. package/dist/flow-changed.d.ts +14 -0
  62. package/dist/flow-changed.js +53 -0
  63. package/dist/flow-code.d.ts +5 -1
  64. package/dist/flow-code.js +4 -1
  65. package/dist/flow-engine.js +16 -0
  66. package/dist/flow-gallery-ui.d.ts +41 -4
  67. package/dist/flow-gallery-ui.js +95 -63
  68. package/dist/flow-live.js +9 -1
  69. package/dist/flow-pull-request.js +15 -7
  70. package/dist/flow-share.js +3 -1
  71. package/dist/flow-starters-ui.d.ts +3 -9
  72. package/dist/flow-starters-ui.js +12 -23
  73. package/dist/flow-steps.d.ts +9 -1
  74. package/dist/flow-steps.js +55 -5
  75. package/dist/flow-triggers.js +55 -0
  76. package/dist/flow-unattended-words.d.ts +20 -0
  77. package/dist/flow-unattended-words.js +24 -0
  78. package/dist/flow-unattended.d.ts +9 -5
  79. package/dist/flow-unattended.js +45 -23
  80. package/dist/flows-cli.d.ts +19 -0
  81. package/dist/flows-cli.js +54 -5
  82. package/dist/flows-ui.d.ts +12 -6
  83. package/dist/flows-ui.js +152 -32
  84. package/dist/flows.js +5 -2
  85. package/dist/integrations-ui.d.ts +6 -4
  86. package/dist/integrations-ui.js +34 -40
  87. package/dist/integrations.d.ts +2 -2
  88. package/dist/integrations.js +4 -4
  89. package/dist/kits-ui.d.ts +8 -10
  90. package/dist/kits-ui.js +29 -33
  91. package/dist/kits.js +1 -1
  92. package/dist/knowledge-ui.js +76 -19
  93. package/dist/lead-tools.js +35 -6
  94. package/dist/lead-voice.js +16 -5
  95. package/dist/ledger-view.d.ts +2 -0
  96. package/dist/ledger-view.js +41 -38
  97. package/dist/models-ui.d.ts +8 -3
  98. package/dist/models-ui.js +82 -59
  99. package/dist/monitoring-ui.d.ts +4 -2
  100. package/dist/monitoring-ui.js +44 -13
  101. package/dist/needs-you.d.ts +1 -1
  102. package/dist/needs-you.js +8 -0
  103. package/dist/operate-remote-arguments.js +1 -0
  104. package/dist/operate-remote.js +1 -1
  105. package/dist/operate.d.ts +1 -1
  106. package/dist/operate.js +249 -6
  107. package/dist/passwordless.d.ts +1 -1
  108. package/dist/passwordless.js +1 -1
  109. package/dist/people-audit-ui.d.ts +2 -1
  110. package/dist/people-audit-ui.js +25 -23
  111. package/dist/policy-ui.d.ts +3 -2
  112. package/dist/policy-ui.js +27 -21
  113. package/dist/policy.d.ts +3 -0
  114. package/dist/policy.js +14 -1
  115. package/dist/project-delete-ui.d.ts +4 -2
  116. package/dist/project-delete-ui.js +135 -126
  117. package/dist/project-delete.js +7 -3
  118. package/dist/project-knowledge.d.ts +10 -10
  119. package/dist/publish.d.ts +11 -0
  120. package/dist/publish.js +16 -3
  121. package/dist/pull-request-flow.js +35 -4
  122. package/dist/pull-request-ui.d.ts +2 -1
  123. package/dist/pull-request-ui.js +39 -28
  124. package/dist/recipe-ui.d.ts +4 -2
  125. package/dist/recipe-ui.js +14 -4
  126. package/dist/release-adapters.d.ts +3 -0
  127. package/dist/release-adapters.js +22 -2
  128. package/dist/release-cli.js +81 -9
  129. package/dist/release.d.ts +21 -0
  130. package/dist/release.js +40 -4
  131. package/dist/releases.js +1 -1
  132. package/dist/request-budget-ui.d.ts +4 -2
  133. package/dist/request-budget-ui.js +30 -19
  134. package/dist/result-follow-ups.js +4 -0
  135. package/dist/retention-ui.d.ts +4 -3
  136. package/dist/retention-ui.js +15 -12
  137. package/dist/scope.d.ts +8 -0
  138. package/dist/scope.js +13 -0
  139. package/dist/serve.d.ts +0 -1
  140. package/dist/serve.js +51 -7
  141. package/dist/server/agents-computers.d.ts +9 -0
  142. package/dist/server/agents-computers.js +67 -0
  143. package/dist/server/chat.js +20 -18
  144. package/dist/server/chrome.d.ts +1 -1
  145. package/dist/server/chrome.js +19 -9
  146. package/dist/server/console.css +31 -2
  147. package/dist/server/flows.js +79 -36
  148. package/dist/server/home.d.ts +35 -0
  149. package/dist/server/home.js +104 -0
  150. package/dist/server/http.d.ts +3 -0
  151. package/dist/server/http.js +5 -0
  152. package/dist/server/kit-standalone.d.ts +22 -0
  153. package/dist/server/kit-standalone.js +69 -0
  154. package/dist/server/kit.d.ts +405 -0
  155. package/dist/server/kit.js +361 -0
  156. package/dist/server/pages.d.ts +2 -1
  157. package/dist/server/pages.js +213 -174
  158. package/dist/server/people-tokens.js +26 -85
  159. package/dist/server/render-history.d.ts +51 -0
  160. package/dist/server/render-history.js +228 -0
  161. package/dist/server/render-new-task.d.ts +29 -0
  162. package/dist/server/render-new-task.js +96 -0
  163. package/dist/server/render-next.d.ts +63 -0
  164. package/dist/server/render-next.js +109 -0
  165. package/dist/server/render-pages.d.ts +35 -190
  166. package/dist/server/render-pages.js +154 -563
  167. package/dist/server/render-people.d.ts +24 -3
  168. package/dist/server/render-people.js +93 -72
  169. package/dist/server/render-projects.d.ts +103 -0
  170. package/dist/server/render-projects.js +247 -0
  171. package/dist/server/render-results.d.ts +5 -24
  172. package/dist/server/render-results.js +48 -185
  173. package/dist/server/render-settings.d.ts +5 -74
  174. package/dist/server/render-settings.js +102 -493
  175. package/dist/server/render-tasks.d.ts +43 -5
  176. package/dist/server/render-tasks.js +131 -91
  177. package/dist/server/route-table.js +31 -5
  178. package/dist/server/settings.js +276 -255
  179. package/dist/server/task-views.d.ts +8 -0
  180. package/dist/server/task-views.js +90 -2
  181. package/dist/server/tasks.d.ts +18 -0
  182. package/dist/server/tasks.js +148 -41
  183. package/dist/server/terminals.js +1 -1
  184. package/dist/server/welcome.d.ts +22 -0
  185. package/dist/server/welcome.js +69 -0
  186. package/dist/settings-groups-ui.d.ts +279 -0
  187. package/dist/settings-groups-ui.js +615 -0
  188. package/dist/settings-kit.d.ts +42 -6
  189. package/dist/settings-kit.js +69 -7
  190. package/dist/settings-proofs.js +1 -0
  191. package/dist/setup-guide.d.ts +2 -2
  192. package/dist/skills-ui.js +59 -7
  193. package/dist/slack-settings.js +12 -5
  194. package/dist/spend-ui.d.ts +45 -26
  195. package/dist/spend-ui.js +124 -56
  196. package/dist/sso-ui.d.ts +5 -2
  197. package/dist/sso-ui.js +63 -31
  198. package/dist/storage-ui.d.ts +3 -2
  199. package/dist/storage-ui.js +56 -50
  200. package/dist/store.d.ts +182 -5
  201. package/dist/store.js +512 -38
  202. package/dist/surface.js +23 -3
  203. package/dist/task-archive.d.ts +10 -0
  204. package/dist/task-archive.js +16 -0
  205. package/dist/task-control.d.ts +1 -1
  206. package/dist/teams-settings.d.ts +2 -1
  207. package/dist/teams-settings.js +24 -41
  208. package/dist/telegram-settings.d.ts +0 -4
  209. package/dist/telegram-settings.js +52 -25
  210. package/dist/tidy.d.ts +130 -0
  211. package/dist/tidy.js +503 -0
  212. package/dist/toolroll-update-ui.d.ts +8 -4
  213. package/dist/toolroll-update-ui.js +55 -45
  214. package/dist/tools-ui.d.ts +5 -3
  215. package/dist/tools-ui.js +107 -42
  216. package/dist/work-index.d.ts +24 -0
  217. package/dist/work-index.js +56 -30
  218. package/dist/workspace-ui.d.ts +1 -1
  219. package/dist/workspace-ui.js +8 -4
  220. package/package.json +1 -1
@@ -49,6 +49,7 @@ import { readFlowSecrets } from "./flow-secrets.js";
49
49
  import { prepareUnattendedCandidates, grantSignsScriptOf, scriptProvenanceOf, scriptRanDigestOf } from "./flow-unattended.js";
50
50
  import { tmpdir } from "node:os";
51
51
  import { envValue } from "./names.js";
52
+ import { recordBranchCheckRun, unprovenNoticeRuns } from "./tidy.js";
52
53
  import { linearLabelNames, readFormSubmission, readGithubEvent, readGithubIssue, readGithubIssueEventList, readGithubIssueList, readGithubRun, readGithubRunList, readInboundMail, readLinearAnswer, readLinearEvent, readLinearIssue, readWebhookPayload } from "./contracts/trigger-payloads.js";
53
54
  import { isRecord } from "./is-record.js";
54
55
  export { FLOW_TRIGGER_KINDS };
@@ -925,6 +926,9 @@ async function checkTrigger(store, trigger, config, now, io) {
925
926
  store.updateFlowTrigger(trigger.id, { failures, lastAt: now.toISOString(), lastOutcome: fetched.problem, nextAt: new Date(now.getTime() + BACKOFF_MS[Math.min(failures, BACKOFF_MS.length) - 1]).toISOString() }, now);
926
927
  return { ok: false, said: fetched.problem, added: 0 };
927
928
  }
929
+ // Tidy's CI evidence (task-tidy.md): the watched branch's completed runs, green and red, beside the failures it carded.
930
+ if (config.kind === "github" && config.watch === "checks")
931
+ await observeBranchRuns(store, trigger, config, io.gh, now);
928
932
  const by = config.kind === "github" ? "GitHub" : config.kind === "linear" ? "Linear" : "Email";
929
933
  // Known items are left out before the batch is taken (v126): a page of things already received can't keep the unseen
930
934
  // ones behind it waiting.
@@ -1206,6 +1210,14 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
1206
1210
  : config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body.pull_request, "pull")
1207
1211
  : config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body.workflow_run) : null;
1208
1212
  items = found === null ? [] : [found];
1213
+ // A signed workflow run on the watched branch is tidy's CI evidence too (task-tidy.md), green or red: recorded only
1214
+ // inside `written`, which re-proves the trigger is still active with the same config, credential and address.
1215
+ if (config.watch === "checks" && config.branch !== null && event === "workflow_run" && action === "completed") {
1216
+ const branch = config.branch;
1217
+ const recorded = written(() => { recordBranchCheckRun(store, { repo: config.repo, branch }, readGithubRun(body.workflow_run), now); return { status: 200, said: "Recorded." }; });
1218
+ if (recorded.status !== 200)
1219
+ return recorded;
1220
+ }
1209
1221
  }
1210
1222
  else if (config.kind === "linear") {
1211
1223
  const body = readLinearEvent(payload), sent = Number(body.webhookTimestamp);
@@ -1648,3 +1660,46 @@ export function receiveFlowForm(store, token, fields, now) {
1648
1660
  store.updateFlowTrigger(trigger.id, { lastAt: now.toISOString(), lastOutcome: "Someone sent the form." }, now);
1649
1661
  return { status: 200, html: thanks };
1650
1662
  }
1663
+ /** Tidy's CI evidence (docs/plans/task-tidy.md): the watched branch's recent completed workflow runs, read through the
1664
+ * person's own gh login. Recorded only while the trigger still watches exactly that repository and branch once GitHub
1665
+ * answered; any failure records nothing (tidy then keeps the notices). */
1666
+ async function observeBranchRuns(store, trigger, config, gh, now) {
1667
+ if (config.branch === null)
1668
+ return;
1669
+ const branch = config.branch;
1670
+ let result;
1671
+ try {
1672
+ result = await gh("gh", ["api", "-H", "Accept: application/vnd.github+json", `repos/${config.repo}/actions/runs?branch=${encodeURIComponent(branch)}&status=completed&per_page=50`], { timeoutMs: 20_000 });
1673
+ }
1674
+ catch {
1675
+ return;
1676
+ }
1677
+ if (result.code !== 0)
1678
+ return;
1679
+ let body;
1680
+ try {
1681
+ body = JSON.parse(result.stdout);
1682
+ }
1683
+ catch {
1684
+ return;
1685
+ }
1686
+ const runs = [...readGithubRunList(body).workflow_runs ?? []];
1687
+ // A few older failure cards' own runs, so tidy can tell whether each was a push to this branch (task-tidy.md).
1688
+ for (const id of unprovenNoticeRuns(store, trigger.id, config.repo)) {
1689
+ try {
1690
+ const one = await gh("gh", ["api", "-H", "Accept: application/vnd.github+json", `repos/${config.repo}/actions/runs/${id}`], { timeoutMs: 20_000 });
1691
+ if (one.code === 0)
1692
+ runs.push(readGithubRun(JSON.parse(one.stdout)));
1693
+ }
1694
+ catch { /* read again next time */ }
1695
+ }
1696
+ // Re-proved and recorded in one transaction: a trigger paused or changed meanwhile records nothing.
1697
+ store.transact(() => {
1698
+ const current = store.getFlowTrigger(trigger.id);
1699
+ const still = current === null ? null : triggerConfigOf(current);
1700
+ if (current === null || current.state !== "active" || current.configJson !== trigger.configJson || still === null || still.kind !== "github" || still.watch !== "checks" || still.repo !== config.repo || still.branch !== branch)
1701
+ return;
1702
+ for (const run of runs)
1703
+ recordBranchCheckRun(store, { repo: config.repo, branch }, run, now);
1704
+ });
1705
+ }
@@ -1,6 +1,26 @@
1
1
  /** The words a "runs on its own" grant signs (flow-unattended.ts), kept free of server imports so the flow page's
2
2
  * signing form says each limit exactly as the signature will. */
3
3
  export declare const dollarWords: (micro: number) => string;
4
+ /** The most runs a day a grant may allow. */
5
+ export declare const UNATTENDED_MAX_RUNS = 50;
6
+ /** The largest per-attempt budget a grant may name: $100. */
7
+ export declare const UNATTENDED_MAX_BUDGET_MICROUSD = 100000000;
8
+ /** A per-attempt budget as typed ("5", "$7.50", "" for no cap), the one way the server and the flow page read it. */
9
+ export declare function unattendedBudgetOf(typed: string): {
10
+ ok: true;
11
+ value: number | null;
12
+ } | {
13
+ ok: false;
14
+ message: string;
15
+ };
16
+ /** Runs a day as typed ("3", "" for no limit), the one way the server and the flow page read it. */
17
+ export declare function unattendedRunsOf(typed: string): {
18
+ ok: true;
19
+ value: number | null;
20
+ } | {
21
+ ok: false;
22
+ message: string;
23
+ };
4
24
  /** "Oct 19": the end's day, where space is short (the exact time goes beside it, on hover). */
5
25
  export declare const untilDayWords: (iso: string) => string;
6
26
  /** "Oct 19, 14:05 UTC": the expiry as every surface says it. */
@@ -1,6 +1,30 @@
1
1
  /** The words a "runs on its own" grant signs (flow-unattended.ts), kept free of server imports so the flow page's
2
2
  * signing form says each limit exactly as the signature will. */
3
3
  export const dollarWords = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
4
+ /** The most runs a day a grant may allow. */
5
+ export const UNATTENDED_MAX_RUNS = 50;
6
+ /** The largest per-attempt budget a grant may name: $100. */
7
+ export const UNATTENDED_MAX_BUDGET_MICROUSD = 100_000_000;
8
+ /** A per-attempt budget as typed ("5", "$7.50", "" for no cap), the one way the server and the flow page read it. */
9
+ export function unattendedBudgetOf(typed) {
10
+ const text = typed.trim().replace(/^\$/, "");
11
+ if (text === "")
12
+ return { ok: true, value: null };
13
+ const value = Number(text);
14
+ if (!/^\d+(\.\d{1,2})?$/.test(text) || value <= 0 || Math.round(value * 1_000_000) > UNATTENDED_MAX_BUDGET_MICROUSD)
15
+ return { ok: false, message: `Give the budget in dollars, up to ${dollarWords(UNATTENDED_MAX_BUDGET_MICROUSD)}, or leave it empty.` };
16
+ return { ok: true, value: Math.round(value * 1_000_000) };
17
+ }
18
+ /** Runs a day as typed ("3", "" for no limit), the one way the server and the flow page read it. */
19
+ export function unattendedRunsOf(typed) {
20
+ const text = typed.trim();
21
+ if (text === "")
22
+ return { ok: true, value: null };
23
+ const value = Number(text);
24
+ if (!/^\d+$/.test(text) || !Number.isInteger(value) || value < 1 || value > UNATTENDED_MAX_RUNS)
25
+ return { ok: false, message: `Give runs a day as 1 to ${UNATTENDED_MAX_RUNS}, or leave it empty.` };
26
+ return { ok: true, value };
27
+ }
4
28
  /** "Oct 19": the end's day, where space is short (the exact time goes beside it, on hover). */
5
29
  export const untilDayWords = (iso) => new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" }).format(new Date(iso));
6
30
  /** "Oct 19, 14:05 UTC": the expiry as every surface says it. */
@@ -8,10 +8,8 @@ export declare const FLOW_ACCEPTANCE: {
8
8
  report: AcceptanceCriterion[];
9
9
  task: AcceptanceCriterion[];
10
10
  };
11
- /** The most runs a day a grant may allow. */
12
- export declare const UNATTENDED_MAX_RUNS = 50;
13
- /** The largest per-attempt budget a grant may name: $100. */
14
- export declare const UNATTENDED_MAX_BUDGET_MICROUSD = 100000000;
11
+ /** The limits a grant may name, and their one parser, live with its words (the flow page validates with them too). */
12
+ export { UNATTENDED_MAX_BUDGET_MICROUSD, UNATTENDED_MAX_RUNS } from "./flow-unattended-words.js";
15
13
  /** The code a script schedule runs, as signed: the script's version and content digest, and for one that runs a project
16
14
  * file, that file's path and, sorted by path, the digest of its bytes and of every project file it statically imports. */
17
15
  export type SignedScript = {
@@ -77,7 +75,7 @@ export type UnattendedGrant = {
77
75
  * revision signed, and neither project protected. Shared by every dispatch belt. */
78
76
  export declare const grantStandsSql: (digest: string, now: string, repo: string) => string;
79
77
  /** Why a grant ended, as its row and ledger line say it. A renewal's ledger line is the new signature's. */
80
- export type UnattendedEnd = "expired" | "revoked" | "renewed" | "schedule changed" | "script changed" | "project protected" | "access changed";
78
+ export type UnattendedEnd = "expired" | "revoked" | "renewed" | "flow changed" | "schedule changed" | "script changed" | "project protected" | "access changed";
81
79
  /**
82
80
  * The one road for what ends grants from outside this module (store.ts calls it): a flow's schedule added, paused,
83
81
  * resumed or removed; a project's approval rules protecting work; an account's access or standing changing. Every live
@@ -222,6 +220,12 @@ export type UnattendedView = {
222
220
  optional: string | null;
223
221
  }[];
224
222
  schedulesDigest: string;
223
+ /** No live grant, and the latest one was ended by a change (the flow, a script, a schedule, the project's rules or
224
+ * the signer's access) before it would have run out: why, and when, so the page says it no longer runs on its own. */
225
+ ended: {
226
+ why: string;
227
+ at: string;
228
+ } | null;
225
229
  };
226
230
  export declare function unattendedViewOf(store: Store, flow: FlowRow, viewer: {
227
231
  name: string;
@@ -31,7 +31,7 @@ import { lstat, readFile, realpath, stat } from "node:fs/promises";
31
31
  import { createRequire } from "node:module";
32
32
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
33
33
  import { isProtectedWork } from "./approval-policy.js";
34
- import { dollarWords as dollars, scheduleWords, UNATTENDED_RULE_WORDS, unattendedCoverWords, unattendedEndWords, unattendedHeadWords, unattendedLimitWords, untilWords } from "./flow-unattended-words.js";
34
+ import { dollarWords as dollars, scheduleWords, unattendedBudgetOf, unattendedRunsOf, UNATTENDED_RULE_WORDS, unattendedCoverWords, unattendedEndWords, unattendedHeadWords, unattendedLimitWords, untilWords } from "./flow-unattended-words.js";
35
35
  import { MODE_MAX_DAYS } from "./modes.js";
36
36
  import { accountReachesSql } from "./project-access.js";
37
37
  import { canonicalJson } from "./canonical-json.js";
@@ -70,10 +70,8 @@ export const FLOW_ACCEPTANCE = {
70
70
  report: [{ id: "c1", statement: "The report answers the request, citing where in the repository each point comes from", how: null, evidence: ["manual-review"] }],
71
71
  task: [{ id: "c1", statement: "The requested change is made and the project's existing checks still pass", how: null, evidence: ["manual-review"] }],
72
72
  };
73
- /** The most runs a day a grant may allow. */
74
- export const UNATTENDED_MAX_RUNS = 50;
75
- /** The largest per-attempt budget a grant may name: $100. */
76
- export const UNATTENDED_MAX_BUDGET_MICROUSD = 100_000_000;
73
+ /** The limits a grant may name, and their one parser, live with its words (the flow page validates with them too). */
74
+ export { UNATTENDED_MAX_BUDGET_MICROUSD, UNATTENDED_MAX_RUNS } from "./flow-unattended-words.js";
77
75
  export const unattendedTermsJson = (terms) => canonicalJson(terms);
78
76
  /** sha256 over a domain-separated canonical encoding, 32 hex like the mode digest. */
79
77
  export function unattendedDigestOf(terms) {
@@ -213,10 +211,11 @@ function closeGrant(store, row, by, reason, now) {
213
211
  export function endUnattendedGrants(store, filter, by, reason, now) {
214
212
  const rows = "flow" in filter ? store.handle.prepare("SELECT * FROM flow_unattended_grant WHERE flow = ? AND revoked_at IS NULL").all(filter.flow)
215
213
  : "signer" in filter ? store.handle.prepare("SELECT * FROM flow_unattended_grant WHERE signed_by = ? AND revoked_at IS NULL").all(filter.signer)
216
- // A script saved or removed: the grants in its project that signed a schedule running it (an unreadable one too, to be safe).
214
+ // A script saved or removed: the grants in its project that signed a schedule running it, or whose flow has a step
215
+ // that runs it (pre-authorized work would run the new code unseen); an unreadable one too, to be safe.
217
216
  : "script" in filter ? store.handle.prepare("SELECT * FROM flow_unattended_grant WHERE repo = ? AND revoked_at IS NULL").all(filter.script.repo).filter(row => {
218
217
  const terms = unattendedTermsFromJson(String(row["terms_json"]));
219
- return terms === null || terms.triggers.some(one => one.script?.name === filter.script.name);
218
+ return terms === null || terms.triggers.some(one => one.script?.name === filter.script.name) || flowStepRunsScript(store, Number(row["flow"]), filter.script.name);
220
219
  })
221
220
  // A project's rules reach the flows in it and the flows whose build zones work in it.
222
221
  : store.handle.prepare(`SELECT ug.* FROM flow_unattended_grant ug JOIN flow f ON f.id = ug.flow WHERE ug.revoked_at IS NULL
@@ -605,22 +604,13 @@ export function parseUnattendedAsk(input) {
605
604
  const days = Number(text(input.days));
606
605
  if (!Number.isInteger(days) || days < 1 || days > MODE_MAX_DAYS)
607
606
  return { ok: false, message: `Choose 1 to ${MODE_MAX_DAYS} days: it always ends.` };
608
- const budgetText = text(input.budget).replace(/^\$/, "");
609
- let budgetMicrousd = null;
610
- if (budgetText !== "") {
611
- const value = Number(budgetText);
612
- if (!/^\d+(\.\d{1,2})?$/.test(budgetText) || value <= 0 || Math.round(value * 1_000_000) > UNATTENDED_MAX_BUDGET_MICROUSD)
613
- return { ok: false, message: `Give the budget in dollars, up to ${dollars(UNATTENDED_MAX_BUDGET_MICROUSD)}, or leave it empty.` };
614
- budgetMicrousd = Math.round(value * 1_000_000);
615
- }
616
- const runsText = text(input.runs);
617
- let dailyRunCap = null;
618
- if (runsText !== "") {
619
- const value = Number(runsText);
620
- if (!Number.isInteger(value) || value < 1 || value > UNATTENDED_MAX_RUNS)
621
- return { ok: false, message: `Give runs a day as 1 to ${UNATTENDED_MAX_RUNS}, or leave it empty.` };
622
- dailyRunCap = value;
623
- }
607
+ const budget = unattendedBudgetOf(text(input.budget));
608
+ if (!budget.ok)
609
+ return budget;
610
+ const runs = unattendedRunsOf(text(input.runs));
611
+ if (!runs.ok)
612
+ return runs;
613
+ const budgetMicrousd = budget.value, dailyRunCap = runs.value;
624
614
  const includeText = Array.isArray(input.include) ? input.include.map(text).join(",") : text(input.include);
625
615
  const include = includeText === "" ? [] : includeText.split(/[\s,]+/).filter(one => one !== "").map(Number);
626
616
  if (include.some(id => !Number.isSafeInteger(id) || id < 1))
@@ -772,5 +762,37 @@ export function unattendedViewOf(store, flow, viewer, now) {
772
762
  terms: { head: unattendedHeadWords(viewer.name, FLOW_ACCEPTANCE), rules: [...UNATTENDED_RULE_WORDS] },
773
763
  schedules: candidates.map(one => ({ id: one.id, words: one.words, optional: one.optional })),
774
764
  schedulesDigest: candidatesDigestOf(candidates),
765
+ ended: live === null ? unattendedEndedOf(store, flow.id, now) : null,
775
766
  };
776
767
  }
768
+ /** Why the flow's latest grant ended early, in words, when a change ended it while it would still have run. */
769
+ const ENDED_WORDS = {
770
+ "flow changed": "the flow changed", "script changed": "a script it runs changed", "schedule changed": "a schedule changed",
771
+ "project protected": "the project's approval rules changed", "access changed": "its signer's access changed",
772
+ };
773
+ function unattendedEndedOf(store, flowId, now) {
774
+ const row = store.handle.prepare("SELECT revoked_at, revoke_reason, absolute_expiry FROM flow_unattended_grant WHERE flow = ? ORDER BY id DESC LIMIT 1").get(flowId);
775
+ if (row === undefined || row["revoked_at"] == null)
776
+ return null;
777
+ const why = ENDED_WORDS[String(row["revoke_reason"])];
778
+ return why === undefined || Date.parse(String(row["absolute_expiry"])) <= now.getTime() ? null : { why, at: String(row["revoked_at"]) };
779
+ }
780
+ /** Whether one of the flow's "Run a script" steps runs this script. A drawing that isn't readable and well formed counts,
781
+ * to be safe. */
782
+ function flowStepRunsScript(store, flowId, name) {
783
+ const flow = store.getFlow(flowId);
784
+ if (flow === null)
785
+ return false;
786
+ let drawn;
787
+ try {
788
+ drawn = JSON.parse(flow.definitionJson);
789
+ }
790
+ catch {
791
+ return true;
792
+ }
793
+ // Anything but a well-formed drawing (an object whose stages are a list of objects) fails closed: the grant ends.
794
+ const stages = drawn !== null && typeof drawn === "object" && !Array.isArray(drawn) ? drawn.stages : undefined;
795
+ if (!Array.isArray(stages) || !stages.every(stage => stage !== null && typeof stage === "object" && !Array.isArray(stage)))
796
+ return true;
797
+ return stages.some(stage => stage.kind === "check" && stage.script === name);
798
+ }
@@ -296,6 +296,25 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
296
296
  readonly takesValue: true;
297
297
  readonly meaning: 'also cover these script or plane-review schedules (trigger ids, like 12,14); plain schedules are covered already';
298
298
  }];
299
+ }, {
300
+ readonly action: 'browser';
301
+ readonly synopsis: "let a flow's script zones marked browser: true start Chromium in their sandbox (on: a person approves with their own password, previews until --yes; off: any approver revokes)";
302
+ readonly mutation: 'identity-idempotent';
303
+ readonly audience: 'operator';
304
+ readonly positionals: readonly ['flow', 'on|off'];
305
+ readonly flags: readonly [...{
306
+ name: string;
307
+ takesValue: boolean;
308
+ meaning: string;
309
+ }[], {
310
+ name: string;
311
+ takesValue: boolean;
312
+ meaning: string;
313
+ }, {
314
+ readonly name: 'digest';
315
+ readonly takesValue: true;
316
+ readonly meaning: "required with --yes: the approval digest the preview showed; refuses if the flow or its scripts changed since";
317
+ }];
299
318
  }, {
300
319
  readonly action: 'archive';
301
320
  readonly synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes';
package/dist/flows-cli.js CHANGED
@@ -19,6 +19,7 @@ import { STARTER_FLOWS, starterFlowOf, starterOf, startersFor, starterTerms, swi
19
19
  import { addFlowTriggerTo, checkFlowTriggerNow, describeTrigger, removeFlowTrigger, setFlowTriggerOn, triggerConfigOf, validateTriggerConfig, rawTakesDeliveries } from './flow-triggers.js';
20
20
  import { deciderOf, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowDigest, flowFromSteps, flowTerms, stepsFor } from './flows.js';
21
21
  import { prepareUnattendedCandidates, activeUnattendedGrant, parseUnattendedAsk, revokeUnattended, signUnattended, unattendedViewOf, unattendedWords, untilWords, UNATTENDED_MAX_RUNS } from './flow-unattended.js';
22
+ import { browserApprovalDigest, browserTerms, browserTermsOf, browserViewOf, revokeFlowBrowser, signFlowBrowser } from './flow-browser.js';
22
23
  import { MODE_MAX_DAYS } from './modes.js';
23
24
  import { verifyApproverByPassword } from './principal.js';
24
25
  import { authenticateApprover, proveCredential } from './scope.js';
@@ -71,6 +72,8 @@ export const FLOWS_DESCRIPTORS = [
71
72
  { name: 'budget', takesValue: true, meaning: 'optional dollar cap per attempt, like 5 or 2.50' },
72
73
  { name: 'runs', takesValue: true, meaning: `optional limit on runs a day (UTC), 1 to ${UNATTENDED_MAX_RUNS}` },
73
74
  { name: 'include', takesValue: true, meaning: 'also cover these script or plane-review schedules (trigger ids, like 12,14); plain schedules are covered already' }] },
75
+ { action: 'browser', synopsis: "let a flow's script zones marked browser: true start Chromium in their sandbox (on: a person approves with their own password, previews until --yes; off: any approver revokes)", mutation: 'identity-idempotent', audience: 'operator', positionals: ['flow', 'on|off'], flags: [...write, yes,
76
+ { name: 'digest', takesValue: true, meaning: "required with --yes: the approval digest the preview showed; refuses if the flow or its scripts changed since" }] },
74
77
  { action: 'archive', synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes', mutation: 'identity-idempotent', positionals: ['flow'], flags: [...write, yes] },
75
78
  ];
76
79
  /** `--file` is a value here (a path in the project), where elsewhere it is a switch; so are unattended's `--budget`, `--runs` and `--include`. */
@@ -160,10 +163,12 @@ export async function runFlowsCommand(positional, flags, context) {
160
163
  const refuse = (project, reason, message, code = EXIT.refused) => { record(project, 'refused', reason); return fail(reason, message, code); };
161
164
  // A trigger from anyone widens what comes in (chat-management.md §0a.3): only the person's own proof counts, never a
162
165
  // machine credential standing in for them (a remote run's API token, a lead's token), though both pass the check above.
163
- const widening = (project) => {
166
+ // The same proof approves browser use in a flow's script zones (flow-browser.ts): the method is recorded with it.
167
+ const personMethod = (project) => {
164
168
  const proved = authenticateApprover(store, acting.name, acting.token, project);
165
- return proved.ok && personProof(proved.method) ? { widening: 'proved' } : {};
169
+ return proved.ok && personProof(proved.method) ? proved.method : null;
166
170
  };
171
+ const widening = (project) => personMethod(project) === null ? {} : { widening: 'proved' };
167
172
  const settle = (project) => { try {
168
173
  advanceFlows(store, project, context.clock(), { evidenceRoot: context.evidenceRoot });
169
174
  }
@@ -359,8 +364,10 @@ export async function runFlowsCommand(positional, flags, context) {
359
364
  return refuse(project, 'unknown-script', `There's no script called ${name} in ${projectName(project)}.`);
360
365
  if (script.held === null)
361
366
  return ok({ applied: false, repo: project, said: `${script.name} already runs.` }, [`${script.name} already runs.`]);
362
- store.approveFlowScript(project, script.name);
363
- record(project, 'accepted', `script ${script.name} approved`);
367
+ // Bound to the version just read: a script swapped under the same name meanwhile is refused, never approved unseen.
368
+ if (store.approveFlowScript(project, script.name, { version: script.version, digest: script.digest }) !== 'approved')
369
+ return refuse(project, 'changed', `${script.name} changed while this ran. Read it again (toolroll flows show), then approve it. Nothing was approved.`);
370
+ record(project, 'accepted', `script ${script.name} v${script.version} approved`);
364
371
  settle(project);
365
372
  return ok({ applied: true, repo: project, said: `Approved ${script.name}. Zones that run it go on.` }, [`Approved ${script.name}. Zones that run it go on.`]);
366
373
  }
@@ -396,6 +403,42 @@ export async function runFlowsCommand(positional, flags, context) {
396
403
  `Revoke: toolroll flows unattended ${flow.id} off`,
397
404
  ]);
398
405
  }
406
+ if (action === 'browser') {
407
+ const flow = flowOf(args[0], reachable);
408
+ if (flow === null)
409
+ return refuse(null, 'unknown-flow', 'No such flow in your projects. toolroll flows list names them.');
410
+ const turn = args[1];
411
+ if (turn !== 'on' && turn !== 'off')
412
+ return fail('usage', `Use flows browser ${flow.id} on, or flows browser ${flow.id} off.`, EXIT.usage);
413
+ if (turn === 'off') {
414
+ // Lowering authority is one step for any approver on the project.
415
+ if (!revokeFlowBrowser(store, flow.id, who, now))
416
+ return refuse(flow.repo, 'not-on', `${flow.name} has no approved browser use. Nothing was changed.`);
417
+ record(flow.repo, 'accepted', `flow #${flow.id} browser off`);
418
+ return ok({ applied: true, browser: null }, [`${flow.name}'s scripts can no longer start a browser.`]);
419
+ }
420
+ if (flowDefinitionOf(flow) === null)
421
+ return refuse(flow.repo, 'unreadable', 'This flow\'s drawing can\'t be read. Save it again from the editor.');
422
+ const terms = browserTermsOf(store, flow);
423
+ if (terms === null || terms.zones.length === 0)
424
+ return refuse(flow.repo, 'refused', terms === null ? 'A zone that starts a browser has no script it can approve: missing, waiting for approval, or one that runs a project file (write the script on the zone instead). Nothing was changed.'
425
+ : `No zone in ${flow.name} says it starts a browser. Mark its script zone with browser: true (flows edit), then approve. Nothing was changed.`);
426
+ const digest = browserApprovalDigest(terms);
427
+ if (!flags.has('yes'))
428
+ return preview(flow.repo, `Approve browser use in the ${flow.name} flow`, [...browserTerms(terms.zones), `Approval digest ${digest}: approve exactly this with --yes --digest ${digest}.`], { flowId: flow.id, digest, zones: terms.zones });
429
+ // A person's own proof only: a lead's or remote run's token passes the approver check above but never approves this.
430
+ // The digest the person read is required: approving with --yes alone could approve something they never saw.
431
+ const given = text('digest');
432
+ if (given === null)
433
+ return fail('usage', `Approve exactly what you read: flows browser ${flow.id} on --yes --digest <the digest the preview showed>.`, EXIT.usage);
434
+ const signed = signFlowBrowser(store, { flow: flow.id, signer: who, proof: personMethod(flow.repo), sawDigest: given }, now);
435
+ if (!signed.ok)
436
+ return refuse(flow.repo, 'refused', `${signed.message} Nothing was changed.`);
437
+ record(flow.repo, 'accepted', `flow #${flow.id} browser on`);
438
+ return ok({ applied: true, browser: { digest: signed.grant.digest, zones: signed.grant.zones, signedBy: signed.grant.signedBy } }, [
439
+ `${flow.name} may start a browser. Approved by ${who}:`, ...browserTerms(signed.grant.zones).map(line => ` ${line}`), `Revoke: toolroll flows browser ${flow.id} off`,
440
+ ]);
441
+ }
399
442
  if (action === 'archive') {
400
443
  const cards = store.flowCards(flow.id, false).length, triggers = store.flowTriggers(flow.id).filter(one => one.state === 'active').length;
401
444
  if (!flags.has('yes'))
@@ -566,6 +609,7 @@ function zoneOf(stage, flow) {
566
609
  ...(stage.limit === undefined ? {} : { remindAfterMinutes: stage.limit.minutes, thenMoveTo: stage.limit.to }),
567
610
  ...(stage.kind === 'approval' ? { decider: deciderOf(stage, flow) } : {}),
568
611
  ...(stage.script === null ? {} : { script: stage.script }),
612
+ ...(stage.browser === true ? { browser: true } : {}),
569
613
  ...(stage.merge === undefined ? {} : { merge: stage.merge }),
570
614
  ...(stage.subagent === undefined ? {} : { subagent: stage.subagent }),
571
615
  ...(stage.options === undefined ? {} : { options: stage.options.map(one => ({ label: one.label, to: one.to })) }),
@@ -589,6 +633,10 @@ function describeFlow(store, flow, now) {
589
633
  }),
590
634
  cards: store.flowCards(flow.id, true).slice(0, 10).map(card => cardOf(definition, card)),
591
635
  unattended: unattendedJson(store, flow, now),
636
+ // The latest grant a change ended early (the flow, a script, a schedule): why and when, as the flow page says it.
637
+ ...(() => { const ended = unattendedViewOf(store, flow, { name: flow.owner, approver: true }, now).ended; return ended === null ? {} : { unattendedEnded: ended }; })(),
638
+ // Browser use, only for a flow with a zone that starts one: the same status line the flow page shows.
639
+ ...(() => { const view = browserViewOf(store, flow); return view === null ? {} : { browser: { status: view.status, words: view.words, signedBy: view.signedBy, signedAt: view.signedAt } }; })(),
592
640
  };
593
641
  }
594
642
  /** Whether a flow runs on its own, and until when: null when it doesn't. */
@@ -603,9 +651,10 @@ function showLines(flow) {
603
651
  return [
604
652
  `#${flow.id} ${flow.name} · ${flow.project} · owner ${flow.owner}`,
605
653
  flow.unattended === null
606
- ? `Runs on its own: no · turn on: toolroll flows unattended ${flow.id} on --days N`
654
+ ? `Runs on its own: no${'unattendedEnded' in flow && flow.unattendedEnded !== undefined ? ` (it ended ${untilWords(flow.unattendedEnded.at)}: ${flow.unattendedEnded.why})` : ''} · turn on: toolroll flows unattended ${flow.id} on --days N`
607
655
  : `Runs on its own until ${untilWords(flow.unattended.until)} · revoke: toolroll flows unattended ${flow.id} off`,
608
656
  ...(flow.unattended === null ? [] : [` Covers: ${flow.unattended.covers.join('; ')}`]),
657
+ ...(flow.browser === undefined ? [] : [`${flow.browser.words} · ${flow.browser.status === 'allowed' ? `turn off: toolroll flows browser ${flow.id} off` : `allow: toolroll flows browser ${flow.id} on`}`]),
609
658
  '', 'Zones',
610
659
  ...flow.zones.flatMap((zone, index) => [
611
660
  ` ${index + 1}. ${zone.title} (${zone.id}) — ${zone.does}${zone.id === flow.start ? ' · start' : ''}`,
@@ -2,12 +2,18 @@ import type { BrowserFlowView } from "./browser-workspace.js";
2
2
  import type { FlowRow, Store } from "./store.js";
3
3
  import { type FlowFile, type FlowImportPlan } from "./flow-share.js";
4
4
  import { type Html } from "./html.js";
5
- export declare const FLOWS_CSS = ".flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:10px 0 0}.flows .flow-import{margin-top:18px;padding-top:14px;border-top:1px solid var(--so-line)}.flows .flow-import h3{font-size:.95rem;margin:0}.flows .flow-terms{list-style:none;padding:0;margin:8px 0 16px;display:grid;gap:10px}.flows .flow-terms li{white-space:pre-line;overflow-wrap:anywhere}.flows .flow-terms pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.8rem;margin:6px 0 0}.flows .flow-import-preview h2{font-size:1rem;margin:20px 0 4px}.flows .flow-untrusted{font-weight:600}.flows button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}.flows fieldset{border:0;padding:0;margin:0;display:grid;gap:10px;min-width:0}.flows legend{font-weight:600;padding:0;margin-bottom:6px}.flows textarea[hidden]{display:none}.flows button.secondary{background:transparent;color:var(--foreground);border:1px solid var(--border)}@media(max-width:600px){.flows input,.flows select{font-size:16px}}@media(max-width:760px){.flows .card{padding:10px 12px;margin:8px 0;position:relative}.flows .flow-row{row-gap:2px}.flows .flow-row h2 a::after{content:\"\";position:absolute;inset:0;border-radius:inherit}.flows .flow-counts{line-height:1.35}}";
6
- /** The flows a person can open, with how many cards wait in each, and the new-flow form. */
7
- export declare function flowsListHtml(store: Store, flows: readonly FlowRow[], projects: readonly string[], canCreate: boolean, problem: string | null): Html;
8
- /** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made). */
9
- export declare const FLOW_IMPORT_SCRIPT = "(function(){var form=document.querySelector('[data-flow-import]');if(!form)return;var input=form.querySelector('[data-flow-file]'),text=form.querySelector('[data-flow-document]'),note=form.querySelector('[data-flow-file-note]'),address=form.querySelector('[name=url]');function say(words){note.textContent=words;note.hidden=words==='';}var reading=null;input.addEventListener('change',function(){text.value='';say('');var file=input.files&&input.files[0];if(!file){reading=null;return;}if(file.size>262144){say('That file is too big: a flow file is at most 256 KB.');input.value='';reading=null;return;}reading=file.text().then(function(value){text.value=value;});});form.addEventListener('submit',function(event){if(reading!==null&&text.value===''){event.preventDefault();reading.then(function(){form.requestSubmit();});return;}if(text.value===''&&address.value.trim()===''){event.preventDefault();say('Choose a flow file or give its address.');}});})();";
10
- /** An import's preview: the plan in plain words, what it asks for, and one Import button after the terms. */
5
+ /** The flow page's fallback rules, plus the Flows list and the import preview (kit pieces; only layout here). */
6
+ export declare const FLOWS_CSS: string;
7
+ /**
8
+ * Flows: the top bar (a project filter, More, New flow), then each flow as a quiet row under Needs you, Running,
9
+ * Turned on and Paused. Words come only from what the store knows now: cards waiting on a decision, cards in
10
+ * progress, and the triggers (on, paused, when they last looked and what they found).
11
+ */
12
+ export declare function flowsListHtml(store: Store, flows: readonly FlowRow[], projects: readonly string[], canCreate: boolean, problem: string | null, filter?: string | null, now?: Date): Html;
13
+ /** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made), and sends a
14
+ * filter select's form when it changes. */
15
+ export declare const FLOW_IMPORT_SCRIPT = "(function(){document.querySelectorAll('select[data-k-autosubmit]').forEach(function(s){s.addEventListener('change',function(){if(s.form)s.form.requestSubmit();});});var form=document.querySelector('[data-flow-import]');if(!form)return;var input=form.querySelector('[data-flow-file]'),text=form.querySelector('[data-flow-document]'),note=form.querySelector('[data-flow-file-note]'),named=form.querySelector('[data-flow-file-name]'),address=form.querySelector('[name=url]');function say(words){note.textContent=words;note.hidden=words==='';}var reading=null;input.addEventListener('change',function(){text.value='';say('');var file=input.files&&input.files[0];if(named)named.textContent=file?file.name:'No file chosen';if(!file){reading=null;return;}if(file.size>262144){say('That file is too big: a flow file is at most 256 KB.');input.value='';if(named)named.textContent='No file chosen';reading=null;return;}reading=file.text().then(function(value){text.value=value;});});form.addEventListener('submit',function(event){if(reading!==null&&text.value===''){event.preventDefault();reading.then(function(){form.requestSubmit();});return;}if(text.value===''&&address.value.trim()===''){event.preventDefault();say('Choose a flow file or give its address.');}});})();";
16
+ /** An import's preview, a reading page: the plan in plain words, what it asks for, and one Import button after the terms. */
11
17
  export declare function flowImportHtml(input: {
12
18
  plan: FlowImportPlan | null;
13
19
  file: FlowFile;