toolroll 0.9.86 → 0.9.88

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 (173) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/dist/acp-check.d.ts +9 -0
  3. package/dist/acp-check.js +40 -28
  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/backup-ui.d.ts +5 -4
  10. package/dist/backup-ui.js +65 -32
  11. package/dist/browser/terminal.css +1 -1
  12. package/dist/browser/terminal.js +57 -54
  13. package/dist/browser/workspace.css +2 -2
  14. package/dist/browser/workspace.js +35 -70
  15. package/dist/browser-crew.js +1 -1
  16. package/dist/browser-workspace.d.ts +329 -183
  17. package/dist/browser-workspace.js +10 -9
  18. package/dist/chat-controls.d.ts +1 -1
  19. package/dist/chat-controls.js +1 -1
  20. package/dist/chat-settings-page.d.ts +29 -9
  21. package/dist/chat-settings-page.js +59 -28
  22. package/dist/check-levels-ui.d.ts +2 -1
  23. package/dist/check-levels-ui.js +67 -58
  24. package/dist/contracts/cli.d.ts +43 -0
  25. package/dist/contracts/cli.js +4 -0
  26. package/dist/contracts/console-api.d.ts +1 -1
  27. package/dist/contracts/console-api.js +1 -1
  28. package/dist/credentials-ui.d.ts +5 -3
  29. package/dist/credentials-ui.js +58 -31
  30. package/dist/design/kit.css +722 -0
  31. package/dist/design/kit.d.ts +208 -0
  32. package/dist/design/kit.js +177 -0
  33. package/dist/design/tokens.css +31 -1
  34. package/dist/discord-settings.js +8 -4
  35. package/dist/export-ui.d.ts +6 -4
  36. package/dist/export-ui.js +18 -8
  37. package/dist/flow-changed.d.ts +14 -0
  38. package/dist/flow-changed.js +53 -0
  39. package/dist/flow-gallery-ui.d.ts +41 -4
  40. package/dist/flow-gallery-ui.js +95 -63
  41. package/dist/flow-starters-ui.d.ts +3 -9
  42. package/dist/flow-starters-ui.js +12 -23
  43. package/dist/flow-unattended-words.d.ts +20 -0
  44. package/dist/flow-unattended-words.js +24 -0
  45. package/dist/flow-unattended.d.ts +9 -5
  46. package/dist/flow-unattended.js +45 -23
  47. package/dist/flows-cli.js +7 -3
  48. package/dist/flows-ui.d.ts +12 -6
  49. package/dist/flows-ui.js +141 -30
  50. package/dist/integrations-ui.d.ts +6 -4
  51. package/dist/integrations-ui.js +34 -40
  52. package/dist/integrations.d.ts +2 -2
  53. package/dist/integrations.js +4 -4
  54. package/dist/kits-ui.d.ts +8 -10
  55. package/dist/kits-ui.js +29 -33
  56. package/dist/kits.js +1 -1
  57. package/dist/knowledge-ui.js +76 -19
  58. package/dist/ledger-view.d.ts +2 -0
  59. package/dist/ledger-view.js +41 -38
  60. package/dist/models-ui.d.ts +8 -3
  61. package/dist/models-ui.js +82 -59
  62. package/dist/monitoring-ui.d.ts +4 -2
  63. package/dist/monitoring-ui.js +44 -13
  64. package/dist/needs-you.d.ts +1 -1
  65. package/dist/needs-you.js +8 -0
  66. package/dist/operate-remote-arguments.js +1 -0
  67. package/dist/operate-remote.js +1 -1
  68. package/dist/operate.d.ts +1 -1
  69. package/dist/operate.js +38 -4
  70. package/dist/passwordless.d.ts +1 -1
  71. package/dist/passwordless.js +1 -1
  72. package/dist/people-audit-ui.d.ts +2 -1
  73. package/dist/people-audit-ui.js +25 -23
  74. package/dist/policy-ui.d.ts +3 -2
  75. package/dist/policy-ui.js +27 -21
  76. package/dist/policy.d.ts +3 -0
  77. package/dist/policy.js +14 -1
  78. package/dist/project-delete-ui.d.ts +4 -2
  79. package/dist/project-delete-ui.js +135 -126
  80. package/dist/project-knowledge.d.ts +10 -10
  81. package/dist/publish.d.ts +11 -0
  82. package/dist/publish.js +16 -3
  83. package/dist/pull-request-ui.d.ts +2 -1
  84. package/dist/pull-request-ui.js +39 -28
  85. package/dist/recipe-ui.d.ts +4 -2
  86. package/dist/recipe-ui.js +14 -4
  87. package/dist/release-adapters.d.ts +3 -0
  88. package/dist/release-adapters.js +22 -2
  89. package/dist/release-cli.js +81 -9
  90. package/dist/release.d.ts +23 -0
  91. package/dist/release.js +46 -4
  92. package/dist/releases.js +1 -1
  93. package/dist/request-budget-ui.d.ts +4 -2
  94. package/dist/request-budget-ui.js +30 -19
  95. package/dist/retention-ui.d.ts +4 -3
  96. package/dist/retention-ui.js +15 -12
  97. package/dist/scope.d.ts +3 -0
  98. package/dist/scope.js +6 -0
  99. package/dist/serve.d.ts +0 -1
  100. package/dist/serve.js +51 -7
  101. package/dist/server/agents-computers.d.ts +9 -0
  102. package/dist/server/agents-computers.js +67 -0
  103. package/dist/server/chat.js +16 -16
  104. package/dist/server/chrome.d.ts +1 -1
  105. package/dist/server/chrome.js +19 -9
  106. package/dist/server/console.css +24 -12
  107. package/dist/server/flows.js +64 -36
  108. package/dist/server/home.d.ts +35 -0
  109. package/dist/server/home.js +104 -0
  110. package/dist/server/http.d.ts +3 -0
  111. package/dist/server/http.js +5 -0
  112. package/dist/server/kit-standalone.d.ts +22 -0
  113. package/dist/server/kit-standalone.js +70 -0
  114. package/dist/server/kit.d.ts +419 -0
  115. package/dist/server/kit.js +369 -0
  116. package/dist/server/pages.d.ts +2 -1
  117. package/dist/server/pages.js +213 -174
  118. package/dist/server/people-tokens.js +26 -85
  119. package/dist/server/render-history.d.ts +51 -0
  120. package/dist/server/render-history.js +228 -0
  121. package/dist/server/render-new-task.d.ts +29 -0
  122. package/dist/server/render-new-task.js +96 -0
  123. package/dist/server/render-next.d.ts +63 -0
  124. package/dist/server/render-next.js +109 -0
  125. package/dist/server/render-pages.d.ts +22 -190
  126. package/dist/server/render-pages.js +123 -575
  127. package/dist/server/render-people.d.ts +24 -3
  128. package/dist/server/render-people.js +93 -72
  129. package/dist/server/render-projects.d.ts +103 -0
  130. package/dist/server/render-projects.js +247 -0
  131. package/dist/server/render-results.d.ts +5 -24
  132. package/dist/server/render-results.js +48 -185
  133. package/dist/server/render-settings.d.ts +5 -74
  134. package/dist/server/render-settings.js +102 -493
  135. package/dist/server/render-tasks.d.ts +35 -5
  136. package/dist/server/render-tasks.js +114 -85
  137. package/dist/server/route-table.js +20 -5
  138. package/dist/server/settings.js +276 -255
  139. package/dist/server/task-views.d.ts +8 -0
  140. package/dist/server/task-views.js +85 -2
  141. package/dist/server/tasks.d.ts +8 -0
  142. package/dist/server/tasks.js +95 -40
  143. package/dist/server/terminals.js +1 -1
  144. package/dist/server/welcome.d.ts +22 -0
  145. package/dist/server/welcome.js +69 -0
  146. package/dist/settings-groups-ui.d.ts +279 -0
  147. package/dist/settings-groups-ui.js +615 -0
  148. package/dist/settings-kit.d.ts +42 -6
  149. package/dist/settings-kit.js +69 -7
  150. package/dist/setup-guide.d.ts +2 -2
  151. package/dist/skills-ui.js +59 -7
  152. package/dist/slack-settings.js +12 -5
  153. package/dist/spend-ui.d.ts +45 -26
  154. package/dist/spend-ui.js +124 -56
  155. package/dist/sso-ui.d.ts +5 -2
  156. package/dist/sso-ui.js +63 -31
  157. package/dist/storage-ui.d.ts +3 -2
  158. package/dist/storage-ui.js +56 -50
  159. package/dist/store.d.ts +112 -3
  160. package/dist/store.js +292 -20
  161. package/dist/surface.js +5 -1
  162. package/dist/teams-settings.d.ts +2 -1
  163. package/dist/teams-settings.js +24 -41
  164. package/dist/telegram-settings.d.ts +0 -4
  165. package/dist/telegram-settings.js +52 -25
  166. package/dist/toolroll-update-ui.d.ts +8 -4
  167. package/dist/toolroll-update-ui.js +55 -45
  168. package/dist/tools-ui.d.ts +5 -3
  169. package/dist/tools-ui.js +107 -42
  170. package/dist/work-index.js +11 -7
  171. package/dist/workspace-ui.d.ts +1 -1
  172. package/dist/workspace-ui.js +7 -4
  173. package/package.json +1 -1
@@ -0,0 +1,208 @@
1
+ /**
2
+ * The revamp kit's words and rules, shared by the server's HTML kit (src/server/kit.ts) and the React kit
3
+ * (src/browser/kit/), so both draw the same thing from the same facts. No DOM, no server state: the rules
4
+ * are in src/design/KIT.md, the look in src/design/kit.css.
5
+ *
6
+ * The approval helpers here decide only what a page SHOWS (a password field, one click, or no button). The server
7
+ * still proves every act at the moment it happens (scope.ts freshness, passwordless.ts, people-tokens.ts); a page
8
+ * that shows one click for an act the server then refuses says so in place.
9
+ */
10
+ import { type Headline } from "../task-status.js";
11
+ /** The five status words. */
12
+ export type StatusKey = "needs" | "running" | "queued" | "finished" | "failed";
13
+ /** A chip's tone: "" is neutral; "ask" names a specific ask inside a Needs-you group (ink words, amber dot). */
14
+ export type ChipTone = "" | "needs" | "ask" | "live" | "done" | "failed";
15
+ export declare const STATUS: Readonly<Record<StatusKey, {
16
+ word: string;
17
+ tone: ChipTone;
18
+ }>>;
19
+ /** A task's headline (task-status.ts) as the kit's chip: its own word, in the tone of the status it belongs to. A
20
+ * row that waits on a person never shows a failure tone; something that waits on someone else is never amber. */
21
+ export declare function chipOfHeadline(headline: Headline): {
22
+ word: string;
23
+ tone: ChipTone;
24
+ status: StatusKey;
25
+ };
26
+ /** Which of the five a headline is. */
27
+ export declare function statusOfHeadline(headline: Headline): StatusKey;
28
+ /** Resource states (a computer that builds, not work): never amber. */
29
+ export declare const RESOURCE: Readonly<Record<"idle" | "building" | "offline", {
30
+ word: string;
31
+ tone: ChipTone;
32
+ }>>;
33
+ /**
34
+ * none = one click (answer, accept, retry, merge, remove a hold);
35
+ * standard = a password unless Passwordless is on or a password was typed in the last 15 minutes here;
36
+ * step-up = a password unless Passwordless is on;
37
+ * way-in = anything that creates or widens a way in (invites, tokens, sign-in methods, widening access):
38
+ * always the password typed now.
39
+ */
40
+ export type ApprovalKind = "none" | "standard" | "step-up" | "way-in";
41
+ export declare const APPROVAL_KINDS: readonly ApprovalKind[];
42
+ /** The facts a page has about this person and act, as the server read them for this page. */
43
+ export type ApprovalFacts = {
44
+ /** Passwordless is on for this signed-in browser (html.ts passwordlessRendering). */
45
+ passwordless: boolean;
46
+ /** A recently typed password would stand in for this act now (scope.ts freshnessFor preview): the act is allowlisted and
47
+ * the password was typed on this session within the window. Only a standard act honours it. */
48
+ remembered: boolean;
49
+ /** When the remembered password stops counting, as words ("10:47"), for the terms line. */
50
+ rememberedUntil?: string | null;
51
+ /** An automatic-approval rule covers this act: the page shows no button at all. Never for way-in. */
52
+ automatic?: boolean;
53
+ };
54
+ /** What the page shows: a password field, one click, or no button (approved automatically). */
55
+ export type ApprovalAsk = "password" | "click" | "automatic";
56
+ export declare function approvalAsk(kind: ApprovalKind, facts: ApprovalFacts): ApprovalAsk;
57
+ /** How a click without a password is justified, for the record line after an approval. */
58
+ export declare function approvalHow(kind: ApprovalKind, facts: ApprovalFacts): "password" | "remembered" | "passwordless" | "none" | "automatic";
59
+ /** The terms line under an approval: which rule applies, in words. Empty for a one-click act. */
60
+ export declare function termsOf(kind: ApprovalKind, facts: ApprovalFacts): string;
61
+ /** A duration in seconds as the timing rows say it: "38s", "6m 12s", "1h 25m". */
62
+ export declare function dur(seconds: number): string;
63
+ /** "3 flows", "1 flow". */
64
+ export declare const plural: (n: number, one: string, many: string) => string;
65
+ /** A step's state on the timing axis. */
66
+ export type TimingState = "ok" | "run" | "q" | "bad" | "wait";
67
+ export declare const TIMING_WORDS: Readonly<Record<TimingState, string>>;
68
+ export type TimingRow = {
69
+ name: string;
70
+ sub?: string;
71
+ st: TimingState;
72
+ start: number;
73
+ dur: number;
74
+ };
75
+ /** Each row's bar as percentages of one shared axis; a queued row has no bar until it starts. */
76
+ export declare function timingBars(rows: readonly TimingRow[], scale?: number): Array<{
77
+ left: number;
78
+ width: number;
79
+ } | null>;
80
+ /** One step on a progress line: done (a check), now (waits on you: amber), live (running: blue), failed, or to do. */
81
+ export type StepState = "done" | "now" | "live" | "failed" | "todo";
82
+ export type StepItem = {
83
+ label: string;
84
+ state: StepState;
85
+ sub?: string;
86
+ };
87
+ /** The words a screen reader hears for a step's state. */
88
+ export declare const STEP_WORDS: Readonly<Record<StepState, string>>;
89
+ /** How far the line is filled, 0–1: up to the current step (the first that isn't done), or all of it. */
90
+ export declare function stepsFill(steps: readonly StepItem[]): number;
91
+ /** One shape of a 24×24 stroke icon: its element and attributes (data, so each side draws it with its own escaping). */
92
+ export type IconShape = readonly ["path", {
93
+ readonly d: string;
94
+ }] | readonly ["circle", {
95
+ readonly cx: number;
96
+ readonly cy: number;
97
+ readonly r: number;
98
+ }] | readonly ["rect", {
99
+ readonly x: number;
100
+ readonly y: number;
101
+ readonly width: number;
102
+ readonly height: number;
103
+ readonly rx: number;
104
+ }];
105
+ /** The icons the kit draws (lucide shapes). */
106
+ export declare const KIT_ICONS: {
107
+ readonly home: readonly [IconShape, IconShape];
108
+ readonly tasks: readonly [IconShape, IconShape, IconShape, IconShape, IconShape];
109
+ readonly flows: readonly [readonly ["rect", {
110
+ readonly x: 3;
111
+ readonly y: 3;
112
+ readonly width: 8;
113
+ readonly height: 8;
114
+ readonly rx: 2;
115
+ }], IconShape, readonly ["rect", {
116
+ readonly x: 13;
117
+ readonly y: 13;
118
+ readonly width: 8;
119
+ readonly height: 8;
120
+ readonly rx: 2;
121
+ }]];
122
+ readonly agents: readonly [IconShape, readonly ["rect", {
123
+ readonly x: 4;
124
+ readonly y: 8;
125
+ readonly width: 16;
126
+ readonly height: 12;
127
+ readonly rx: 2;
128
+ }], IconShape, IconShape, IconShape, IconShape];
129
+ readonly projects: readonly [IconShape];
130
+ readonly settings: readonly [IconShape, IconShape, IconShape, IconShape, IconShape, IconShape, IconShape, IconShape, IconShape];
131
+ readonly pen: readonly [IconShape];
132
+ readonly arrow: readonly [IconShape, IconShape];
133
+ readonly plus: readonly [IconShape, IconShape];
134
+ readonly send: readonly [IconShape, IconShape];
135
+ readonly stop: readonly [readonly ["rect", {
136
+ readonly x: 6;
137
+ readonly y: 6;
138
+ readonly width: 12;
139
+ readonly height: 12;
140
+ readonly rx: 2;
141
+ }]];
142
+ readonly x: readonly [IconShape, IconShape];
143
+ readonly back: readonly [IconShape];
144
+ readonly menu: readonly [IconShape, IconShape, IconShape];
145
+ readonly search: readonly [IconShape, IconShape];
146
+ readonly chevron: readonly [IconShape];
147
+ readonly check: readonly [IconShape];
148
+ readonly alert: readonly [IconShape, IconShape, IconShape];
149
+ readonly more: readonly [IconShape, IconShape, IconShape];
150
+ readonly file: readonly [IconShape, IconShape];
151
+ readonly minus: readonly [IconShape];
152
+ readonly fit: readonly [IconShape, IconShape, IconShape, IconShape];
153
+ readonly bolt: readonly [IconShape];
154
+ readonly code: readonly [IconShape, IconShape];
155
+ readonly sun: readonly [IconShape, IconShape, IconShape, IconShape, IconShape, IconShape, IconShape, IconShape, IconShape];
156
+ readonly cpu: readonly [readonly ["rect", {
157
+ readonly x: 4;
158
+ readonly y: 4;
159
+ readonly width: 16;
160
+ readonly height: 16;
161
+ readonly rx: 2;
162
+ }], readonly ["rect", {
163
+ readonly x: 9;
164
+ readonly y: 9;
165
+ readonly width: 6;
166
+ readonly height: 6;
167
+ readonly rx: 1;
168
+ }], IconShape, IconShape, IconShape, IconShape, IconShape, IconShape, IconShape, IconShape];
169
+ readonly bell: readonly [IconShape, IconShape];
170
+ readonly shield: readonly [IconShape];
171
+ readonly folder: readonly [IconShape];
172
+ readonly people: readonly [IconShape, IconShape, IconShape, IconShape];
173
+ readonly coin: readonly [IconShape, IconShape, IconShape];
174
+ readonly monitor: readonly [readonly ["rect", {
175
+ readonly x: 2;
176
+ readonly y: 3;
177
+ readonly width: 20;
178
+ readonly height: 14;
179
+ readonly rx: 2;
180
+ }], IconShape, IconShape];
181
+ readonly list: readonly [IconShape, IconShape, IconShape, IconShape, IconShape, IconShape];
182
+ readonly board: readonly [readonly ["rect", {
183
+ readonly x: 3;
184
+ readonly y: 3;
185
+ readonly width: 18;
186
+ readonly height: 18;
187
+ readonly rx: 2;
188
+ }], IconShape, IconShape];
189
+ readonly panel: readonly [readonly ["rect", {
190
+ readonly x: 3;
191
+ readonly y: 3;
192
+ readonly width: 18;
193
+ readonly height: 18;
194
+ readonly rx: 2;
195
+ }], IconShape];
196
+ };
197
+ export type KitIcon = keyof typeof KIT_ICONS;
198
+ /** The eight Settings groups, in order: [label, slug, href]. */
199
+ export declare const SETTINGS_GROUPS: ReadonlyArray<readonly [label: string, slug: string, href: string]>;
200
+ /** Each group's icon on the Settings overview (wave D). */
201
+ export declare const SETTINGS_GROUP_ICONS: Readonly<Record<string, KitIcon>>;
202
+ /** How a Settings row says what waits on this person: "1 item needs you", "2 items need you". */
203
+ export declare const needsWords: (n: number) => string;
204
+ /** The not-found page's default words. */
205
+ export declare const NOT_FOUND: {
206
+ title: (path: string) => string;
207
+ text: string;
208
+ };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * The revamp kit's words and rules, shared by the server's HTML kit (src/server/kit.ts) and the React kit
3
+ * (src/browser/kit/), so both draw the same thing from the same facts. No DOM, no server state: the rules
4
+ * are in src/design/KIT.md, the look in src/design/kit.css.
5
+ *
6
+ * The approval helpers here decide only what a page SHOWS (a password field, one click, or no button). The server
7
+ * still proves every act at the moment it happens (scope.ts freshness, passwordless.ts, people-tokens.ts); a page
8
+ * that shows one click for an act the server then refuses says so in place.
9
+ */
10
+ import { HEADLINE_TONE } from "../task-status.js";
11
+ export const STATUS = {
12
+ needs: { word: "Needs you", tone: "needs" },
13
+ running: { word: "Running", tone: "live" },
14
+ queued: { word: "Queued", tone: "" },
15
+ finished: { word: "Finished", tone: "done" },
16
+ failed: { word: "Failed", tone: "failed" },
17
+ };
18
+ /** A task's headline (task-status.ts) as the kit's chip: its own word, in the tone of the status it belongs to. A
19
+ * row that waits on a person never shows a failure tone; something that waits on someone else is never amber. */
20
+ export function chipOfHeadline(headline) {
21
+ const status = statusOfHeadline(headline);
22
+ return { word: headline === "Needs you" ? STATUS.needs.word : headline, tone: STATUS[status].tone, status };
23
+ }
24
+ /** Which of the five a headline is. */
25
+ export function statusOfHeadline(headline) {
26
+ switch (HEADLINE_TONE[headline]) {
27
+ case "attention":
28
+ case "ready": return "needs";
29
+ case "live": return "running";
30
+ case "success": return "finished";
31
+ case "danger": return "failed";
32
+ case "neutral": return "queued";
33
+ }
34
+ }
35
+ /** Resource states (a computer that builds, not work): never amber. */
36
+ export const RESOURCE = {
37
+ idle: { word: "Idle", tone: "" },
38
+ building: { word: "Building", tone: "live" },
39
+ offline: { word: "Offline", tone: "failed" },
40
+ };
41
+ export const APPROVAL_KINDS = ["none", "standard", "step-up", "way-in"];
42
+ export function approvalAsk(kind, facts) {
43
+ if (kind === "way-in")
44
+ return "password";
45
+ if (facts.automatic === true)
46
+ return "automatic";
47
+ if (kind === "none")
48
+ return "click";
49
+ if (facts.passwordless)
50
+ return "click";
51
+ if (kind === "standard" && facts.remembered)
52
+ return "click";
53
+ return "password";
54
+ }
55
+ /** How a click without a password is justified, for the record line after an approval. */
56
+ export function approvalHow(kind, facts) {
57
+ const ask = approvalAsk(kind, facts);
58
+ if (ask === "password")
59
+ return "password";
60
+ if (ask === "automatic")
61
+ return "automatic";
62
+ if (kind === "none")
63
+ return "none";
64
+ return facts.passwordless ? "passwordless" : "remembered";
65
+ }
66
+ /** The terms line under an approval: which rule applies, in words. Empty for a one-click act. */
67
+ export function termsOf(kind, facts) {
68
+ if (kind === "way-in") {
69
+ if (facts.passwordless)
70
+ return "Passwordless is on, but this creates a new way in, so it still asks for your password.";
71
+ if (facts.remembered)
72
+ return "A remembered password doesn’t count here: this creates a new way in.";
73
+ return "This creates a new way in, so it always asks for your password.";
74
+ }
75
+ if (facts.automatic === true)
76
+ return "Approved automatically by your rule.";
77
+ if (kind === "none")
78
+ return "";
79
+ if (facts.passwordless)
80
+ return "No password needed: Passwordless is on for your account.";
81
+ if (kind === "standard" && facts.remembered)
82
+ return facts.rememberedUntil ? `No password needed: remembered on this computer until ${facts.rememberedUntil}.` : "No password needed: remembered on this computer.";
83
+ return "Asks for your password.";
84
+ }
85
+ // ---- time and counts --------------------------------------------------------------------------------------------
86
+ /** A duration in seconds as the timing rows say it: "38s", "6m 12s", "1h 25m". */
87
+ export function dur(seconds) {
88
+ const n = Math.max(0, Math.round(seconds));
89
+ if (n < 60)
90
+ return `${n}s`;
91
+ if (n < 3600)
92
+ return `${Math.floor(n / 60)}m ${String(n % 60).padStart(2, "0")}s`;
93
+ return `${Math.floor(n / 3600)}h ${String(Math.floor((n % 3600) / 60)).padStart(2, "0")}m`;
94
+ }
95
+ /** "3 flows", "1 flow". */
96
+ export const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
97
+ export const TIMING_WORDS = { ok: "Done", run: "Now", q: "Queued", bad: "Failed", wait: "Needs you" };
98
+ /** Each row's bar as percentages of one shared axis; a queued row has no bar until it starts. */
99
+ export function timingBars(rows, scale) {
100
+ const sc = scale ?? Math.max(1, ...rows.map(row => row.start + (row.st === "q" ? 0 : row.dur))) * 1.02;
101
+ return rows.map(row => row.st === "q" ? null : { left: (Math.max(0, row.start) / sc) * 100, width: (Math.max(row.dur, sc / 400) / sc) * 100 });
102
+ }
103
+ /** The words a screen reader hears for a step's state. */
104
+ export const STEP_WORDS = { done: "done", now: "waiting on you", live: "in progress", failed: "failed", todo: "not started" };
105
+ /** How far the line is filled, 0–1: up to the current step (the first that isn't done), or all of it. */
106
+ export function stepsFill(steps) {
107
+ if (steps.length < 2)
108
+ return steps.length === 1 && steps[0].state === "done" ? 1 : 0;
109
+ const at = steps.findIndex(step => step.state !== "done");
110
+ return at === -1 ? 1 : at / (steps.length - 1);
111
+ }
112
+ const path = (d) => ["path", { d }];
113
+ const circle = (cx, cy, r) => ["circle", { cx, cy, r }];
114
+ /** The icons the kit draws (lucide shapes). */
115
+ export const KIT_ICONS = {
116
+ // The app shell's destinations (the rail and the phone menu) and its New task pen.
117
+ home: [path("m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"), path("M9 22V12h6v10")],
118
+ tasks: [path("m3 17 2 2 4-4"), path("m3 7 2 2 4-4"), path("M13 6h8"), path("M13 12h8"), path("M13 18h8")],
119
+ flows: [["rect", { x: 3, y: 3, width: 8, height: 8, rx: 2 }], path("M7 11v4a2 2 0 0 0 2 2h4"), ["rect", { x: 13, y: 13, width: 8, height: 8, rx: 2 }]],
120
+ agents: [path("M12 8V4H8"), ["rect", { x: 4, y: 8, width: 16, height: 12, rx: 2 }], path("M2 14h2"), path("M20 14h2"), path("M15 13v2"), path("M9 13v2")],
121
+ projects: [path("M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z")],
122
+ settings: [path("M21 4h-7"), path("M10 4H3"), path("M21 12h-9"), path("M8 12H3"), path("M21 20h-5"), path("M12 20H3"), path("M14 2v4"), path("M8 10v4"), path("M16 18v4")],
123
+ pen: [path("M21.17 6.81a1 1 0 0 0-3.99-3.99L3.84 16.17a2 2 0 0 0-.5.83l-1.32 4.35a.5.5 0 0 0 .62.62l4.35-1.32a2 2 0 0 0 .83-.5z")],
124
+ arrow: [path("M5 12h14"), path("m12 5 7 7-7 7")],
125
+ plus: [path("M12 5v14"), path("M5 12h14")],
126
+ send: [path("M12 19V5"), path("m5 12 7-7 7 7")],
127
+ stop: [["rect", { x: 6, y: 6, width: 12, height: 12, rx: 2 }]],
128
+ x: [path("M18 6 6 18"), path("m6 6 12 12")],
129
+ back: [path("m15 18-6-6 6-6")],
130
+ menu: [path("M4 6h16"), path("M4 12h16"), path("M4 18h16")],
131
+ search: [circle(11, 11, 7), path("m20 20-3.5-3.5")],
132
+ chevron: [path("m6 9 6 6 6-6")],
133
+ check: [path("M20 6 9 17l-5-5")],
134
+ alert: [circle(12, 12, 9), path("M12 8v4"), path("M12 16h.01")],
135
+ more: [circle(5, 12, 1), circle(12, 12, 1), circle(19, 12, 1)],
136
+ file: [path("M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"), path("M14 3v5h5")],
137
+ // Wave C (flows): the canvas's zoom bar and the flow page's tools.
138
+ minus: [path("M5 12h14")],
139
+ fit: [path("M15 3h6v6"), path("M9 21H3v-6"), path("M21 3l-7 7"), path("M3 21l7-7")],
140
+ bolt: [path("M13 2 4 14h7l-1 8 9-12h-7z")],
141
+ code: [path("m16 18 6-6-6-6"), path("m8 6-6 6 6 6")],
142
+ // The Settings groups' icons (wave D; SETTINGS_GROUP_ICONS).
143
+ sun: [circle(12, 12, 4), path("M12 2v2"), path("M12 20v2"), path("m4.93 4.93 1.41 1.41"), path("m17.66 17.66 1.41 1.41"), path("M2 12h2"), path("M20 12h2"), path("m6.34 17.66-1.41 1.41"), path("m19.07 4.93-1.41 1.41")],
144
+ cpu: [["rect", { x: 4, y: 4, width: 16, height: 16, rx: 2 }], ["rect", { x: 9, y: 9, width: 6, height: 6, rx: 1 }], path("M15 2v2"), path("M15 20v2"), path("M2 15h2"), path("M2 9h2"), path("M20 15h2"), path("M20 9h2"), path("M9 2v2"), path("M9 20v2")],
145
+ bell: [path("M10.27 21a2 2 0 0 0 3.46 0"), path("M3.26 15.33A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.67C19.41 13.96 18 12.5 18 8A6 6 0 0 0 6 8c0 4.5-1.41 5.96-2.74 7.33")],
146
+ shield: [path("M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z")],
147
+ folder: [path("M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z")],
148
+ people: [path("M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"), circle(9, 7, 4), path("M22 21v-2a4 4 0 0 0-3-3.87"), path("M16 3.13a4 4 0 0 1 0 7.75")],
149
+ coin: [circle(12, 12, 10), path("M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8"), path("M12 18V6")],
150
+ monitor: [["rect", { x: 2, y: 3, width: 20, height: 14, rx: 2 }], path("M8 21h8"), path("M12 17v4")],
151
+ list: [path("M8 6h13"), path("M8 12h13"), path("M8 18h13"), path("M3 6h.01"), path("M3 12h.01"), path("M3 18h.01")],
152
+ board: [["rect", { x: 3, y: 3, width: 18, height: 18, rx: 2 }], path("M9 3v18"), path("M15 3v18")],
153
+ panel: [["rect", { x: 3, y: 3, width: 18, height: 18, rx: 2 }], path("M15 3v18")],
154
+ };
155
+ // ---- settings frame ---------------------------------------------------------------------------------------------
156
+ /** The eight Settings groups, in order: [label, slug, href]. */
157
+ export const SETTINGS_GROUPS = [
158
+ ["General", "general", "/settings/general"],
159
+ ["Agents and models", "agents", "/settings/agents"],
160
+ ["Chat apps and alerts", "alerts", "/settings/alerts"],
161
+ ["Approvals and safety", "approval", "/settings/approval"],
162
+ ["Projects", "projects", "/settings/projects"],
163
+ ["People and sign-in", "access", "/settings/access"],
164
+ ["Spend", "spend", "/spend"],
165
+ ["System", "system", "/settings/system"],
166
+ ];
167
+ /** Each group's icon on the Settings overview (wave D). */
168
+ export const SETTINGS_GROUP_ICONS = {
169
+ general: "sun", agents: "cpu", alerts: "bell", approval: "shield", projects: "folder", access: "people", spend: "coin", system: "monitor",
170
+ };
171
+ /** How a Settings row says what waits on this person: "1 item needs you", "2 items need you". */
172
+ export const needsWords = (n) => n === 1 ? "1 item needs you" : `${n} items need you`;
173
+ /** The not-found page's default words. */
174
+ export const NOT_FOUND = {
175
+ title: (path) => `There’s no page at ${path}.`,
176
+ text: "It may have moved. Home has what needs you; Tasks has every task.",
177
+ };
@@ -1,4 +1,4 @@
1
- /* @generated by youi from the Signal design language v1.0.0. Do not edit: change the tokens in design-language and run `youi sync`. */
1
+ /* @generated by youi from the Toolroll design language v1.0.0. Do not edit: change the tokens in the design repo and run `youiyoux sync`. */
2
2
  /* Built with Terrazzo (DTCG 2025.10 resolver). */
3
3
  :root {
4
4
  color-scheme: light;
@@ -8,10 +8,18 @@
8
8
  --so-attention: var(--so-signal);
9
9
  --so-attention-soft: var(--so-signal-soft);
10
10
  --so-code-bg: #fafafa;
11
+ /* Composer: the filled surface of the message box, one step off the page. */
12
+ --so-composer: #f2f2f2;
13
+ /* Composer Control: the filled circle and pill chips inside the composer (+, project, toggle chips) and the empty send. */
14
+ --so-composer-control: #dcdcdc;
11
15
  /* Vermilion: failure and irreversible acts. */
12
16
  --so-danger: #c4320a;
13
17
  --so-danger-soft: #feebe7;
14
18
  --so-ease-out: cubic-bezier(0.23, 1, 0.32, 1);
19
+ /* Field: the filled surface of a text field, select or secondary text box (the field family: no outline; the label names it, focus draws the ink ring). */
20
+ --so-field: #ececec;
21
+ /* Field Line: the boundary that identifies a control (fields, outline buttons, switches, radio cards), 3:1 or better on paper. */
22
+ --so-field-line: #8f8f8f;
15
23
  --so-font-mono: "Geist Mono", ui-monospace, "SF Mono", "SFMono-Regular", "Menlo", "Consolas", monospace;
16
24
  --so-font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
17
25
  /* Frame Grey: the frame the sidebar sits on, and the page background of server pages. */
@@ -33,6 +41,10 @@
33
41
  --so-nav-hover: #e4e4e4;
34
42
  /* Nav Ink: idle sidebar rows and their hover fill on the frame. */
35
43
  --so-nav-ink: #525252;
44
+ /* Needs You Amber: what waits on this person, and nothing else (the Needs-you chip's dot, a waiting box's outline, the dock's dot). Never a warning, quota, link or decoration. */
45
+ --so-needs: var(--so-warning);
46
+ /* The wash behind a Needs-you chip. */
47
+ --so-needs-soft: var(--so-warning-soft);
36
48
  --so-neutral-ink: #525252;
37
49
  /* Neutral Badge: the badge for anything that is a fact and not a claim on the person, such as queued, running or built today in a project peek, or a count in a fold. */
38
50
  --so-neutral-soft: #f0f0f0;
@@ -40,6 +52,8 @@
40
52
  --so-on-attention: var(--so-on-signal);
41
53
  --so-on-signal: #ffffff;
42
54
  --so-overlay: rgb(0 0 0 / .32);
55
+ /* Overlay Edge: the border of a popover, sheet, drawer or menu (floating things get a border, not a shadow). */
56
+ --so-overlay-line: #b8b8b8;
43
57
  /* Paper: every sheet, card, field, menu and dialog. */
44
58
  --so-paper: #ffffff;
45
59
  --so-phone-block: 14px;
@@ -56,6 +70,10 @@
56
70
  --so-radius-sm: 6px;
57
71
  /* Raised: segmented-tab tracks, notices, row hover fills, and the muted well behind a "next step" block or a neutral problem line. */
58
72
  --so-raised: #f5f5f5;
73
+ /* Ink: all body text, and the colour of every act: primary buttons, the New task button, switches and radio cards when on. */
74
+ --so-ring: var(--so-ink);
75
+ /* Kit Scrim: what a blocking kit overlay dims the page with, lighter than the console's older overlay. */
76
+ --so-scrim: rgb(0 0 0 / .22);
59
77
  --so-scroll: #cfcfcf;
60
78
  /* Selection: text selection highlight, with ink text on it. */
61
79
  --so-selection: #d4d4d4;
@@ -150,8 +168,12 @@
150
168
  --so-accent-hover: #ffffff;
151
169
  --so-accent-text: #ededed;
152
170
  --so-code-bg: #111111;
171
+ --so-composer: #242424;
172
+ --so-composer-control: #333333;
153
173
  --so-danger: #ff977d;
154
174
  --so-danger-soft: rgb(255 151 125 / .12);
175
+ --so-field: #232323;
176
+ --so-field-line: #6b6b6b;
155
177
  --so-ground: #0b0b0b;
156
178
  --so-info: #70b8ff;
157
179
  --so-info-soft: rgb(112 184 255 / .12);
@@ -169,9 +191,11 @@
169
191
  --so-on-accent: #0a0a0a;
170
192
  --so-on-signal: #0a0a0a;
171
193
  --so-overlay: rgb(0 0 0 / .6);
194
+ --so-overlay-line: #4a4a4a;
172
195
  --so-paper: #161616;
173
196
  --so-pill-shadow: 0px 0px 0px 1px #2a2a2a, 0px 1px 2px 0px rgb(0 0 0 / .5);
174
197
  --so-raised: #1c1c1c;
198
+ --so-scrim: rgb(0 0 0 / .5);
175
199
  --so-scroll: #333333;
176
200
  --so-selection: #3a3a3a;
177
201
  --so-shadow-overlay: 0px 0px 0px 1px #2e2e2e, 0px 24px 48px -12px rgb(0 0 0 / .7);
@@ -195,8 +219,12 @@
195
219
  --so-accent-hover: #ffffff;
196
220
  --so-accent-text: #ededed;
197
221
  --so-code-bg: #111111;
222
+ --so-composer: #242424;
223
+ --so-composer-control: #333333;
198
224
  --so-danger: #ff977d;
199
225
  --so-danger-soft: rgb(255 151 125 / .12);
226
+ --so-field: #232323;
227
+ --so-field-line: #6b6b6b;
200
228
  --so-ground: #0b0b0b;
201
229
  --so-info: #70b8ff;
202
230
  --so-info-soft: rgb(112 184 255 / .12);
@@ -214,9 +242,11 @@
214
242
  --so-on-accent: #0a0a0a;
215
243
  --so-on-signal: #0a0a0a;
216
244
  --so-overlay: rgb(0 0 0 / .6);
245
+ --so-overlay-line: #4a4a4a;
217
246
  --so-paper: #161616;
218
247
  --so-pill-shadow: 0px 0px 0px 1px #2a2a2a, 0px 1px 2px 0px rgb(0 0 0 / .5);
219
248
  --so-raised: #1c1c1c;
249
+ --so-scrim: rgb(0 0 0 / .5);
220
250
  --so-scroll: #333333;
221
251
  --so-selection: #3a3a3a;
222
252
  --so-shadow-overlay: 0px 0px 0px 1px #2e2e2e, 0px 24px 48px -12px rgb(0 0 0 / .7);
@@ -1,13 +1,17 @@
1
1
  import { html } from "./html.js";
2
2
  import { loadDiscordCredentials } from "./discord-api.js";
3
- import { chatSettingsHtml } from "./chat-settings-page.js";
3
+ import { chatSecretRow, chatSettingsHtml } from "./chat-settings-page.js";
4
4
  export function discordSettingsHtml(store, dir, options = {}) {
5
5
  return chatSettingsHtml(store, dir, options, {
6
6
  provider: "discord",
7
7
  label: "Discord",
8
8
  credentials: loadDiscordCredentials(dir),
9
- setup: html `<details open><summary>Create your Discord app</summary><ol><li><a href="https://discord.com/developers/applications" target="_blank" rel="noopener noreferrer">Create an application</a>, then copy its token from the Bot page.</li><li>Keep the Interactions Endpoint URL empty and privileged intents off.</li><li>Under Installation, enable Guild Install with the bot scope and no server permissions. Use its install link to add it to your server, then open a direct message to the bot.</li></ol></details>`,
10
- connect: password => html `<label>Bot token<input style="min-height:44px" type="password" name="bot-token" autocomplete="off" required></label>${password}<p class="meta">The token stays on this installation. Next, pair your own Discord account.</p><button type="submit">Connect Discord</button>`,
11
- access: html `<p>Private messages and replies use your Toolroll permissions. Shared channels are not supported. Secure review links use this installation’s configured HTTPS console address.</p><p>The worker connects out to Discord. It does not need a public webhook or a Tailscale account.</p>`,
9
+ intro: "Manage projects and review results in a private Discord conversation.",
10
+ steps: html `<ol class="k-note"><li><a class="k-link" href="https://discord.com/developers/applications" target="_blank" rel="noopener noreferrer">Create an application</a>, then copy its token from the Bot page.</li><li>Keep the Interactions Endpoint URL empty and privileged intents off.</li><li>Under Installation, enable Guild Install with the bot scope and no server permissions. Use its install link to add it to your server, then open a direct message to the bot.</li></ol>`,
11
+ fields: chatSecretRow({ id: "discord-bot-token", name: "bot-token", label: "Bot token" }),
12
+ fieldsNote: "The token stays on this installation. Next, pair your own Discord account.",
13
+ pairWhere: "in a direct message to your Toolroll Discord app",
14
+ tryLine: html `Try “What needs my attention?” or “Show the evidence for the latest result.”`,
15
+ access: html `<p class="k-note">Private messages and replies use your Toolroll permissions. Shared channels are not supported. Secure review links use this installation’s configured HTTPS console address.</p><p class="k-note">Toolroll connects out to Discord. It does not need a public webhook or a Tailscale account.</p>`,
12
16
  });
13
17
  }
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Settings → Data (v105): an instance operator downloads everything Standing
3
- * Orders knows as one .zip. One form: the password, typed again, and a button.
2
+ * Settings → Data (v105): an instance operator downloads everything Toolroll
3
+ * knows as one .zip: the password (when the rules ask for it) and a button.
4
+ * Built from the revamp kit; the Settings frame adds the title.
4
5
  */
5
6
  import { type Html } from "./html.js";
6
- export declare const EXPORT_CSS: string;
7
- export declare function dataExportHtml(notice: {
7
+ /** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
8
+ export declare const EXPORT_CSS = "";
9
+ export declare function dataExportHtml(said: {
8
10
  problem?: string | null;
9
11
  }): Html;
package/dist/export-ui.js CHANGED
@@ -1,11 +1,21 @@
1
1
  /**
2
- * Settings → Data (v105): an instance operator downloads everything Standing
3
- * Orders knows as one .zip. One form: the password, typed again, and a button.
2
+ * Settings → Data (v105): an instance operator downloads everything Toolroll
3
+ * knows as one .zip: the password (when the rules ask for it) and a button.
4
+ * Built from the revamp kit; the Settings frame adds the title.
4
5
  */
5
- import { html, postForm, reauth } from "./html.js";
6
- export const EXPORT_CSS = `.data-export{max-width:720px;min-width:0}.data-export label{display:grid;gap:4px;margin:14px 0 0;font-size:.875rem}` +
7
- `.data-export input[type=password]{width:100%;max-width:360px;box-sizing:border-box}.data-export button[type=submit]{margin-top:14px;min-height:44px;white-space:nowrap}`;
8
- export function dataExportHtml(notice) {
9
- const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : "";
10
- return html `<section class="data-export">${note}<p>Everything Toolroll knows, as one .zip: projects, tasks, runs and cost, the action ledger, evidence packs, chats, flows, subagents and settings. Passwords, keys and tokens are never included.</p>${postForm("/settings/data", html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Download export</button>`)}<p class="meta">The same export from the command line: <code>toolroll export --out &lt;path&gt; [--zip]</code></p></section>`;
6
+ import { html, joinHtml } from "./html.js";
7
+ import { codeLine, note, notice, settingsSection } from "./server/kit.js";
8
+ import { kitSectionForm } from "./settings-kit.js";
9
+ /** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
10
+ export const EXPORT_CSS = "";
11
+ export function dataExportHtml(said) {
12
+ return settingsSection({ id: "data-export", title: "Download everything",
13
+ lead: "Everything Toolroll knows, as one .zip: projects, tasks, runs and cost, the action ledger, evidence packs, chats, flows, subagents and settings. Passwords, keys and tokens are never included.",
14
+ body: joinHtml([
15
+ said.problem ? notice(said.problem) : "",
16
+ // The answer is a download, not a page: post as a navigation.
17
+ kitSectionForm({ proof: "data", action: "/settings/data", id: "data-form", download: true, save: { label: "Download export" }, body: html `` }),
18
+ note("The same export from the command line:"),
19
+ codeLine("toolroll export --out <path> [--zip]"),
20
+ ]) });
11
21
  }
@@ -0,0 +1,14 @@
1
+ import type { Store } from "./store.js";
2
+ export type FlowChange = {
3
+ flow: number;
4
+ } | {
5
+ script: {
6
+ repo: string;
7
+ name: string;
8
+ };
9
+ };
10
+ /**
11
+ * Run `write` (the change itself) and end what it ends, all in one transaction. `write` answers whether it changed
12
+ * anything; when it didn't, nothing else happens. Returns what `write` returned.
13
+ */
14
+ export declare function flowChanged<T>(store: Store, change: FlowChange, by: string, now: Date, write: () => T, changed?: (result: T) => boolean): T;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The one road for a change to what a flow runs: its drawing saved, or a script one of its zones runs saved or removed.
3
+ * Every approval a person gave for the flow as it was ends here, in the same transaction as the write, with its ledger
4
+ * line, so the flow page, `flows show` and chat all say it ended and why:
5
+ * - "Runs on its own" (flow-unattended.ts): its grants end ("flow changed" / "script changed"), and the work they
6
+ * pre-authorized falls back to a person.
7
+ * - Browser use (flow-browser.ts): its approval seals the drawing and each browser zone's script, so the change itself
8
+ * stops it standing (browserStanding re-proves at every launch). Here it is said once in the ledger, for each
9
+ * flow whose approval stood before the write and doesn't after; the page shows it as expired with the reason.
10
+ * Nothing here widens anything: it only ends approvals.
11
+ */
12
+ import { browserViewOf } from "./flow-browser.js";
13
+ import { endUnattendedGrants } from "./flow-unattended.js";
14
+ /** The flows a change reaches: the flow itself, or every flow in the script's project with a zone that runs it. */
15
+ function flowsOf(store, change) {
16
+ if ("flow" in change) {
17
+ const flow = store.getFlow(change.flow);
18
+ return flow === null ? [] : [flow];
19
+ }
20
+ return store.listFlows([change.script.repo]).filter(flow => {
21
+ try {
22
+ const drawn = JSON.parse(flow.definitionJson);
23
+ return (drawn.stages ?? []).some(stage => stage.kind === "check" && stage.script === change.script.name);
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ });
29
+ }
30
+ /**
31
+ * Run `write` (the change itself) and end what it ends, all in one transaction. `write` answers whether it changed
32
+ * anything; when it didn't, nothing else happens. Returns what `write` returned.
33
+ */
34
+ export function flowChanged(store, change, by, now, write, changed = result => result === true) {
35
+ return store.transact(() => {
36
+ const before = new Map(flowsOf(store, change).map(flow => [flow.id, browserViewOf(store, flow)?.status === "allowed"]));
37
+ const result = write();
38
+ if (!changed(result))
39
+ return result;
40
+ if ("flow" in change)
41
+ endUnattendedGrants(store, { flow: change.flow }, by, "flow changed", now);
42
+ else
43
+ endUnattendedGrants(store, { script: change.script }, by, "script changed", now);
44
+ for (const [id, stood] of before) {
45
+ const flow = store.getFlow(id);
46
+ if (!stood || flow === null || browserViewOf(store, flow)?.status === "allowed")
47
+ continue;
48
+ store.recordAction({ at: now.toISOString(), actor: by, repo: flow.repo, taskId: null, runId: null, source: "policy", action: "flow browser use: ended", outcome: "ended",
49
+ detail: `${flow.name} (flow #${flow.id}) · ${"flow" in change ? "the flow changed" : `the ${change.script.name} script changed`}` });
50
+ }
51
+ return result;
52
+ });
53
+ }