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
@@ -86,6 +86,7 @@ export declare const typedOnly: (row: ProofRow) => boolean;
86
86
  * `data-so-typed`, so the identity-provider swap leaves it a real password field (keepTypedFields). Never a value. */
87
87
  export declare function proofField(row: ProofRow, options?: {
88
88
  label?: string;
89
+ optional?: boolean;
89
90
  }): Html;
90
91
  /**
91
92
  * The identity-provider swap (render-people.ts ssoStepUps) with typed-only proof fields kept: those handlers take only
@@ -116,12 +117,6 @@ export type StepUpForm = {
116
117
  /** What the dialog says the person is agreeing to (the action's own words by default). */
117
118
  confirm?: string;
118
119
  };
119
- /**
120
- * A section's form, posted to its existing route with its existing fields, and its sticky Save. When the row needs a
121
- * proof, the proof field sits in the form's own HTML — inline (as today, which is how it works without JavaScript) and
122
- * moved into a `<dialog>` inside the same form by settings-kit-script.ts, which opens it on submit, keeps the pressed
123
- * button, and posts nothing on Cancel. The server still proves everything.
124
- */
125
120
  export declare function stepUpForm(form: StepUpForm): Html;
126
121
  /** A section: a heading, a one-line state, a problem the handler sent back (role=alert), then its form. */
127
122
  export declare function kitSection(section: {
@@ -135,3 +130,44 @@ export declare function kitSection(section: {
135
130
  /** The kit's look: one 760px column, sections with no nested wells, the sticky Save, the dialog. The needs-you colour
136
131
  * falls back to the warning token until S5 lands `--so-needs-you`. */
137
132
  export declare const SETTINGS_KIT_CSS: string;
133
+ export type SectionForm = {
134
+ /** The proof inventory row this form posts under. */
135
+ proof: string;
136
+ /** The exact route (one of the row's). */
137
+ action: string;
138
+ /** Unique on the page; the refusal slot is `<id>-err`. */
139
+ id: string;
140
+ /** The section's own fields. */
141
+ body: HtmlValue;
142
+ /** Hidden terms posted back unchanged (the shown project, a nonce, a digest…). */
143
+ hidden?: Readonly<Record<string, string | number | null | undefined>>;
144
+ /** The button: "Save" by default; a name=value it posts. */
145
+ save?: {
146
+ label?: string;
147
+ name?: string;
148
+ value?: string;
149
+ variant?: "primary" | "outline" | "danger";
150
+ };
151
+ /** A settings section: Save shows only once something in it changed, and saves only what changed with the rest. An
152
+ * act (Invite, Remove) leaves this off: its button always shows. */
153
+ gate?: boolean;
154
+ /** The proof is needed only when the form posts this value (the row's `when`): [field, value]. */
155
+ proofWhen?: readonly [field: string, value: string];
156
+ /** Post as a page navigation: the answer is a page of its own (a one-time secret, a review). */
157
+ native?: boolean;
158
+ /** The answer is a file the browser saves (implies native): the page stays, so the button never stays busy. */
159
+ download?: boolean;
160
+ /** What saying yes does, for the password popover (the row's own words by default). */
161
+ confirm?: string;
162
+ /** What the handler sent back for this section, in place: done, or refused (role=alert). */
163
+ said?: string | null;
164
+ problem?: string | null;
165
+ };
166
+ /**
167
+ * A settings section's form in the revamp kit's look, posted to its existing route with its existing fields and
168
+ * proof. Without the kit's script it is a plain form: the proof field (when the row asks for one) shows in place and
169
+ * Save posts. With it (src/browser/kit/behavior.ts, section forms): Save appears once something changed, the proof is
170
+ * asked in a password popover only when the rules need it, one validated snapshot is posted, a second press does
171
+ * nothing while one is pending, and a refusal shows next to Save with the edits kept. The server proves everything.
172
+ */
173
+ export declare function kitSectionForm(form: SectionForm): Html;
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { attributes, html, htmlString, joinHtml, postForm, reauth, replaceMarkup } from "./html.js";
17
17
  import { isSettingsFrame } from "./settings-kit-script.js";
18
+ import { requestContext } from "./server/request-context.js";
18
19
  import { contractReads, PROOF_INVENTORY, proofRow } from "./settings-proofs.js";
19
20
  export { isSettingsFrame };
20
21
  const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', "#39": "'", "#x27": "'", nbsp: " " };
@@ -30,6 +31,17 @@ export function headingText(page) {
30
31
  export function settingsCrumbs(pathname, title, page) {
31
32
  if (!isSettingsFrame(pathname) || pathname === "/settings")
32
33
  return undefined;
34
+ // A page in the revamp kit's Settings frame (server/kit.ts settingsShell) names its own trail: Settings, its group, itself.
35
+ const kit = /<nav class="k-tb-ctx k-crumbs" aria-label="Breadcrumb">([\s\S]*?)<\/nav>/.exec(page);
36
+ if (kit !== null) {
37
+ const trail = [{ label: "Settings", href: "/settings" }];
38
+ for (const step of kit[1].matchAll(/<a href="([^"]*)">([\s\S]*?)<\/a>|<b aria-current="page">([\s\S]*?)<\/b>/g)) {
39
+ const label = (headingText(`<h1>${step[2] ?? step[3] ?? ""}</h1>`) ?? "").slice(0, 80);
40
+ if (label !== "")
41
+ trail.push({ label, href: step[1] ?? null });
42
+ }
43
+ return trail;
44
+ }
33
45
  const name = headingText(page) ?? (title.charAt(0).toUpperCase() + title.slice(1));
34
46
  return [{ label: "Settings", href: "/settings" }, { label: name, href: null }];
35
47
  }
@@ -96,7 +108,10 @@ export function proofField(row, options = {}) {
96
108
  const label = options.label ?? "Your Toolroll password";
97
109
  if (typedOnly(row))
98
110
  return html `<label>${label}<input type="password" name="${row.field}" data-so-typed autocomplete="current-password" required></label>`;
99
- const field = html `<label>${label}<input type="password" name="${row.field}" autocomplete="current-password" required></label>`;
111
+ // A row that needs its proof only for some posts (its `when`) leaves the field optional: the handler decides.
112
+ const field = options.optional === true && row.when !== undefined
113
+ ? html `<label>${label}<input type="password" name="${row.field}" autocomplete="current-password"></label>`
114
+ : html `<label>${label}<input type="password" name="${row.field}" autocomplete="current-password" required></label>`;
100
115
  return row.proof === "step-up" ? reauth(field) : field;
101
116
  }
102
117
  const TYPED_FIELD = /<input type="password" name="([a-z-]+)" data-so-typed autocomplete="current-password" required>/g;
@@ -128,11 +143,11 @@ export function proofNote(row) {
128
143
  * moved into a `<dialog>` inside the same form by settings-kit-script.ts, which opens it on submit, keeps the pressed
129
144
  * button, and posts nothing on Cancel. The server still proves everything.
130
145
  */
131
- export function stepUpForm(form) {
132
- const row = proofRow(form.proof);
133
- if (!row.routes.includes(form.action))
134
- throw new Error(`${row.id} doesn't post to ${form.action}`);
135
- const hidden = form.hidden ?? {};
146
+ /** The row for a form posting to `action` with these hidden terms; throws on a form the row doesn't describe. */
147
+ function rowForForm(proof, action, hidden) {
148
+ const row = proofRow(proof);
149
+ if (!row.routes.includes(action))
150
+ throw new Error(`${row.id} doesn't post to ${action}`);
136
151
  for (const name of Object.keys(hidden))
137
152
  if (row.field === name || row.secrets.includes(name))
138
153
  throw new Error(`${row.id}: ${name} is never a hidden value`);
@@ -141,7 +156,12 @@ export function stepUpForm(form) {
141
156
  throw new Error(`${row.id} posts ${row.select.field}=${row.select.value}`);
142
157
  for (const other of variantsOf(row))
143
158
  if (other.select !== undefined && String(hidden[other.select.field] ?? "") === other.select.value)
144
- throw new Error(`${form.action} with ${other.select.field}=${other.select.value} is ${other.id}, not ${row.id}`);
159
+ throw new Error(`${action} with ${other.select.field}=${other.select.value} is ${other.id}, not ${row.id}`);
160
+ return row;
161
+ }
162
+ export function stepUpForm(form) {
163
+ const hidden = form.hidden ?? {};
164
+ const row = rowForForm(form.proof, form.action, hidden);
145
165
  const proof = proofField(row);
146
166
  const asks = htmlString(proof) !== "";
147
167
  const note = proofNote(row);
@@ -184,3 +204,45 @@ export const SETTINGS_KIT_CSS = ":root{--so-settings-width:760px}" +
184
204
  ".kit-dialog::backdrop{background:rgb(0 0 0 / .35)}.kit-dialog h2{margin:0 0 6px;font-size:17px}.kit-dialog-slot .kit-proof{display:block}" +
185
205
  ".kit-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin:16px 0 0}" +
186
206
  "@media (max-width:900px){.kit-save-bar button,.kit-dialog-actions button,.kit-crumbs a{min-height:44px}.kit-crumbs a{display:inline-flex;align-items:center}}";
207
+ /**
208
+ * A settings section's form in the revamp kit's look, posted to its existing route with its existing fields and
209
+ * proof. Without the kit's script it is a plain form: the proof field (when the row asks for one) shows in place and
210
+ * Save posts. With it (src/browser/kit/behavior.ts, section forms): Save appears once something changed, the proof is
211
+ * asked in a password popover only when the rules need it, one validated snapshot is posted, a second press does
212
+ * nothing while one is pending, and a refusal shows next to Save with the edits kept. The server proves everything.
213
+ */
214
+ export function kitSectionForm(form) {
215
+ const hidden = form.hidden ?? {};
216
+ const row = rowForForm(form.proof, form.action, hidden);
217
+ if (form.proofWhen !== undefined && row.when === undefined)
218
+ throw new Error(`${row.id} always needs its proof`);
219
+ const proof = proofField(row, { label: "Your password", optional: form.proofWhen !== undefined });
220
+ const asks = htmlString(proof) !== "";
221
+ const save = form.save ?? {};
222
+ const button = html `<button${attributes({ type: "submit", class: `k-btn k-btn-${save.variant ?? "primary"} k-btn-sm`, name: save.name ?? null, value: save.value ?? null, "data-k-save": true })}><span class="k-face">${save.label ?? "Save"}</span></button>`;
223
+ const note = proofNote(row);
224
+ return postForm(form.action, joinHtml([
225
+ form.body,
226
+ asks ? html `<div class="k-sproof" data-k-proof>${proof}</div>` : "",
227
+ html `<div class="k-ssave"><span class="k-ssave-said" aria-live="polite">${form.said ?? ""}</span>${button}</div>`,
228
+ html `<div class="k-ssave-err" id="${form.id}-err">${form.problem ? html `<p class="k-notice" role="alert"><span>${form.problem}</span></p>`
229
+ : unconfirmedHere(form.id) ? html `<p class="k-notice info" role="status">Couldn’t confirm whether this saved. This page shows what is stored now; check it before saving again.</p>` : ""}</div>`,
230
+ ]), { hidden, attrs: {
231
+ id: form.id, class: "k-sform", "data-k-sform": row.id, "data-k-gate": form.gate === true ? true : null, "data-k-native": form.native === true || form.download === true ? true : null, "data-k-download": form.download === true ? true : null,
232
+ "data-k-proof-if": form.proofWhen === undefined ? null : `${form.proofWhen[0]}=${form.proofWhen[1]}`,
233
+ "data-k-confirm": form.confirm ?? row.action, "data-k-note": note,
234
+ } });
235
+ }
236
+ /** Whether this request is the page again after a save of form `id` couldn't be confirmed (section-forms.ts asks for it
237
+ * with ?unconfirmed=<id>). It only picks which of this page's own forms shows the fixed note; the value is never drawn. */
238
+ function unconfirmedHere(id) {
239
+ const at = requestContext.getStore()?.returnTo;
240
+ if (at === undefined)
241
+ return false;
242
+ try {
243
+ return new URL(at, "http://toolroll.local").searchParams.get("unconfirmed") === id;
244
+ }
245
+ catch {
246
+ return false;
247
+ }
248
+ }
@@ -56,6 +56,7 @@ export const PROOF_INVENTORY = [
56
56
  { id: "updates", action: "Update Toolroll", routes: ["/settings/updates", "/settings/updates/seen", "/settings/updates/cancel"], form: "updates", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: [] },
57
57
  { id: "storage-cleanup", action: "Choose when checkouts are removed", routes: ["/settings/storage"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: [] },
58
58
  { id: "storage-clean", action: "Clean up checkouts", routes: ["/settings/storage/clean"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["preview"], note: "`preview` is the digest of what the page showed; a changed list is refused" },
59
+ { id: "tasks-put-away", action: "Put away old tasks", routes: ["/work/put-away"], form: "tasksPutAway", field: "password", proof: "step-up", ...ACCEPT, role: "approver", secrets: [], terms: ["digest", "days", "task"], note: "`digest` is the preview the page showed; a changed list, or a ticked task outside it, is refused" },
59
60
  { id: "storage-discard", action: "Discard a checkout", routes: ["/settings/storage/discard"], form: "storage", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["path"] },
60
61
  { id: "project-live-view", action: "Show a project's commands in the live view", routes: ["/settings/project/live-view"], form: "projectLiveView", field: "password", proof: "step-up", ...ACCEPT, role: "any", handlerRole: "an approver of the project", secrets: [], terms: ["repo", "nonce"] },
61
62
  { id: "project-delete", action: "Delete a project", routes: ["/settings/project/delete"], form: "projectDelete", field: "password", proof: "step-up", ...ACCEPT, role: "operator", secrets: [], terms: ["repo", "step"], note: "the project's name typed too" },
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
15
15
  } | null;
16
16
  /** Browser installation may write only this known file inside its project. */
17
17
  export declare function previewProjectInstructions(repo: string): {
18
+ content?: never;
18
19
  ok: false;
19
20
  message: string;
20
21
  plan?: never;
21
- content?: never;
22
22
  fingerprint?: never;
23
23
  installed?: never;
24
24
  } | {
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
30
30
  installed: boolean;
31
31
  };
32
32
  export declare function addProjectInstructions(repo: string, fingerprint: string): {
33
+ content?: never;
33
34
  ok: false;
34
35
  message: string;
35
36
  plan?: never;
36
- content?: never;
37
37
  fingerprint?: never;
38
38
  installed?: never;
39
39
  } | import("./skills.js").InstallResult;
package/dist/skills-ui.js CHANGED
@@ -1,12 +1,22 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { whenUtc } from "./when-html.js";
3
3
  import { html, joinHtml, postForm } from "./html.js";
4
+ import { acts, btn, chip, disclose, facts, formRow, input, note, notice, select, settingsRecord, settingsSection, textarea } from "./server/kit.js";
4
5
  // Hidden fields as they always were: a missing value is an empty field.
5
6
  const fields = (data) => Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v ?? "")]));
6
7
  export const SKILLS_CSS = `.skills{max-width:780px;min-width:0;overflow-wrap:anywhere}.skills h2{font-size:1.1rem}.skills .card{padding:18px;margin:14px 0;min-width:0}.skills form,.skills label{display:grid;gap:10px}.skills input,.skills textarea,.skills select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.skills textarea{resize:vertical;min-height:130px}.skills button{justify-self:start;white-space:nowrap;min-height:44px}.skills summary{cursor:pointer;min-height:44px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}.skills pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:360px;overflow:auto;font-size:.9rem}.skills .skill-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.skills .skill-heading h2{margin:0}.skills .skill-state{font-size:.85rem;flex-shrink:0}.skills .skill-source{font-size:.85rem}.skills .skill-controls{display:flex;gap:12px;flex-wrap:wrap}.skills .skill-controls form{margin:0}.skills details details{margin-top:10px}.skills [hidden]{display:none!important}.skills .skill-files{max-height:200px;overflow:auto}.skills .problem{padding:12px}@media(max-width:600px){.skills input,.skills textarea,.skills select{font-size:16px}.skills .card{padding:14px}.skills .skill-heading{align-items:flex-start}}`;
7
8
  function source(skill) {
8
9
  return html `<p class="skill-source">${skill.source} · ${skill.sha.slice(0, 10)}</p><p>${skill.files.length} files · ${Math.ceil(skill.files.reduce((n, f) => n + Buffer.from(f.base64, "base64").length, 0) / 1024)} KB</p><pre>${Buffer.from(skill.files.find((f) => f.path === "SKILL.md").base64, "base64").toString("utf8")}</pre><details><summary>Package files</summary><ul class="skill-files">${skill.files.map((f) => html `<li>${f.path}</li>`)}</ul></details>`;
9
10
  }
11
+ /** A skill's package as the settings page shows it: where it came from, its size, its SKILL.md, its files. */
12
+ function sourceKit(skill) {
13
+ const kb = Math.ceil(skill.files.reduce((n, f) => n + Buffer.from(f.base64, "base64").length, 0) / 1024);
14
+ return joinHtml([
15
+ facts([["Source", html `${skill.source} · <span class="k-mono">${skill.sha.slice(0, 10)}</span>`], ["Size", `${skill.files.length} files · ${kb} KB`]]),
16
+ html `<pre class="k-scode">${Buffer.from(skill.files.find((f) => f.path === "SKILL.md").base64, "base64").toString("utf8")}</pre>`,
17
+ disclose("Package files", html `<ul class="k-note">${skill.files.map((f) => html `<li class="k-mono">${f.path}</li>`)}</ul>`),
18
+ ]);
19
+ }
10
20
  export function skillsHtml(view, csrf, canManage, options = {}) {
11
21
  const manage = canManage && !!csrf, base = {
12
22
  repo: view.repo,
@@ -20,7 +30,27 @@ export function skillsHtml(view, csrf, canManage, options = {}) {
20
30
  view.selection[skill.name]?.enabled === true;
21
31
  const replaced = view.selection[skill.name]?.enabled &&
22
32
  view.selection[skill.name]?.sha !== skill.sha;
23
- return html `<article class="card" id="skill-${skill.sha}"><div class="skill-heading"><h2>${skill.name}</h2><span class="skill-state">${enabled ? "Enabled" : "In library"}</span></div><p>${skill.description}</p>${skill.requirements ? html `<p><strong>Requires:</strong> ${skill.requirements} <span class="meta">Availability is checked when the agent tests or uses it.</span></p>` : ""}${skill.warnings.map((w) => html `<p class="meta">${w}</p>`)}<details${options.focus === skill.sha ? html ` open` : ""}><summary>Review skill</summary>${source(skill)}${manage ? form(enabled ? "disable" : "enable", html `${replaced ? html `<p>Replaces the enabled version of this skill for new tasks.</p>` : ""}<button>${enabled ? "Disable skill" : "Enable skill"}</button>`, { sha: skill.sha }) : ""}</details>${manage ? html `<details${options.draft?.["sha"] === skill.sha ? html ` open` : ""}><summary>Test skill</summary>${form("test", html `<p>${options.agent ?? "Uses this project’s configured report agent"}. Runs a read-only sample through the normal task and approval flow.</p><label>Sample request<textarea name="sample" required maxlength="800" placeholder="Review this project’s home page and suggest three clearer button labels.">${options.draft?.["sha"] === skill.sha ? (options.draft?.["sample"] ?? "") : ""}</textarea></label><p class="meta">The report shows what worked and what needs tools or permission. Enabled status alone does not prove use.</p><button>Create test</button>`, { sha: skill.sha, nonce: randomUUID() })}</details>` : ""}</article>`;
33
+ const toggle = manage ? form(enabled ? "disable" : "enable", joinHtml([
34
+ replaced ? note("Replaces the enabled version of this skill for new tasks.") : "",
35
+ acts(btn(enabled ? "Disable skill" : "Enable skill", { sm: true, type: "submit" })),
36
+ ]), { sha: skill.sha }) : "";
37
+ const testing = manage ? disclose("Test skill", form("test", joinHtml([
38
+ note(`${options.agent ?? "Uses this project’s configured report agent"}. Runs a read-only sample through the normal task and approval flow.`),
39
+ formRow("Sample request", "The report shows what worked and what needs tools or permission. Enabled status alone does not prove use.", textarea({ id: `skill-sample-${skill.sha.slice(0, 12)}`, name: "sample", rows: 3, value: options.draft?.["sha"] === skill.sha ? (options.draft?.["sample"] ?? "") : "",
40
+ attrs: { required: true, maxlength: 800, placeholder: "Review this project’s home page and suggest three clearer button labels.", "aria-label": "Sample request" } }), "top"),
41
+ acts(btn("Create test", { sm: true, type: "submit" })),
42
+ ]), { sha: skill.sha, nonce: randomUUID() }), options.draft?.["sha"] === skill.sha) : "";
43
+ return settingsRecord({
44
+ title: html `<span>${skill.name}</span>${chip(enabled ? "Enabled" : "In library", enabled ? "done" : "")}`,
45
+ sub: skill.description,
46
+ attrs: { id: `skill-${skill.sha}` },
47
+ more: joinHtml([
48
+ skill.requirements ? note(`Requires: ${skill.requirements}. Availability is checked when the agent tests or uses it.`) : "",
49
+ ...skill.warnings.map((w) => note(w)),
50
+ disclose("Review skill", joinHtml([sourceKit(skill), toggle]), options.focus === skill.sha),
51
+ testing,
52
+ ]),
53
+ });
24
54
  };
25
55
  const names = [...new Set(view.library.map((s) => s.name))];
26
56
  const cards = names
@@ -29,19 +59,41 @@ export function skillsHtml(view, csrf, canManage, options = {}) {
29
59
  versions.find((s) => s.sha === view.selection[name]?.sha) ??
30
60
  versions.at(-1);
31
61
  const older = versions.filter((s) => s !== primary);
32
- return html `<div data-skill-group data-skill-query="${name + " " + primary.description}">${renderSkill(primary)}${older.length ? html `<details><summary>Other versions (${older.length})</summary>${older.map(renderSkill)}</details>` : ""}</div>`;
62
+ return html `<div data-skill-group data-skill-query="${name + " " + primary.description}">${renderSkill(primary)}${older.length ? disclose(`Other versions (${older.length})`, joinHtml(older.map(renderSkill))) : ""}</div>`;
33
63
  });
34
64
  const search = names.length > 5
35
- ? html `<label>Search library<input type="search" data-skill-search placeholder="Name or description"></label><p data-skill-no-matches hidden>No matching skills.</p>`
65
+ ? html `${formRow("Search library", null, input({ id: "skill-search", name: "skill-search", type: "search", attrs: { "data-skill-search": true, placeholder: "Name or description", "aria-label": "Search library" } }), "wide")}<p class="k-note" data-skill-no-matches hidden>No matching skills.</p>`
36
66
  : "";
37
67
  const d = options.draft ?? {};
38
68
  const add = manage
39
- ? html `<details class="card"${options.error && options.draft?.["method"] ? html ` open` : ""}><summary>Add skill</summary>${postForm("/settings/skills/import", html `<label>Add from<select name="method" data-skill-method><option value="paste"${d["method"] === "paste" ? html ` selected` : ""}>Paste SKILL.md</option><option value="github"${d["method"] === "github" ? html ` selected` : ""}>GitHub folder</option><option value="folder"${d["method"] === "folder" ? html ` selected` : ""}>Local folder</option></select></label><div data-skill-input="paste"><label>SKILL.md<textarea name="content" rows="8" maxlength="24576" placeholder="---&#10;name: design-review&#10;description: Review interface copy and layout.&#10;---&#10;Write your instructions here.">${d["content"]}</textarea></label></div><div data-skill-input="github"><label>Public GitHub folder<input name="url" type="url" value="${d["url"]}" placeholder="https://github.com/owner/repo/tree/main/skills/review"></label></div><div data-skill-input="folder"><label>Skill folder<input type="file" webkitdirectory multiple data-skill-folder></label><input type="hidden" name="files" value=""><p class="meta">Choose the folder containing SKILL.md. Up to 64 files, 1 MB total.</p><noscript>Folder uploads require JavaScript. You can paste SKILL.md or use a GitHub folder.</noscript></div><p class="meta">Added to your library first. Review its source and requirements before enabling it.</p><p data-skill-error role="alert" hidden></p><button>Add to library</button>`, { attrs: { "data-skill-import": true }, hidden: fields(base) })}</details>`
69
+ ? disclose("Add a skill", postForm("/settings/skills/import", joinHtml([
70
+ formRow("Add from", null, select({ id: "skill-method", name: "method", label: "Add from", current: d["method"] ?? "paste", attrs: { "data-skill-method": true },
71
+ items: [["paste", "Paste SKILL.md"], ["github", "GitHub folder"], ["folder", "Local folder"]] })),
72
+ html `<div data-skill-input="paste">${formRow("SKILL.md", null, textarea({ id: "skill-content", name: "content", rows: 8, value: d["content"] ?? "",
73
+ attrs: { maxlength: 24576, placeholder: "---\nname: design-review\ndescription: Review interface copy and layout.\n---\nWrite your instructions here.", "aria-label": "SKILL.md" } }), "top")}</div>`,
74
+ html `<div data-skill-input="github">${formRow("Public GitHub folder", null, input({ id: "skill-url", name: "url", type: "url", value: d["url"] ?? "",
75
+ attrs: { placeholder: "https://github.com/owner/repo/tree/main/skills/review", "aria-label": "Public GitHub folder" } }), "wide")}</div>`,
76
+ html `<div data-skill-input="folder">${formRow("Skill folder", "Choose the folder containing SKILL.md. Up to 64 files, 1 MB total.", html `<input class="k-input" id="skill-folder" type="file" webkitdirectory multiple data-skill-folder aria-label="Skill folder">`)}<input type="hidden" name="files" value="">\
77
+ <noscript>${note("Folder uploads need JavaScript. You can paste SKILL.md or use a GitHub folder.")}</noscript></div>`,
78
+ note("Added to your library first. Review its source and requirements before enabling it."),
79
+ html `<p class="k-err" data-skill-error role="alert" hidden></p>`,
80
+ acts(btn("Add to library", { variant: "primary", type: "submit" })),
81
+ ]), { attrs: { "data-skill-import": true }, hidden: fields(base) }), Boolean(options.error && options.draft?.["method"]))
40
82
  : "";
41
83
  const history = view.history.length
42
- ? html `<details><summary>Change history</summary>${view.history.map((h) => html `<div class="card"><p>Selection ${h.revision} · ${h.actor} · ${whenUtc(h.at)}</p><ul>${h.enabled.length ? h.enabled.map((s) => html `<li>${s.name} · ${s.version}</li>`) : html `<li>No skills enabled</li>`}</ul>${manage && h.revision !== view.revision ? form("restore", html `<button>Restore selection</button>`, { restore: h.revision }) : html `<span class="meta">Current selection</span>`}</div>`)}</details>`
84
+ ? disclose("Change history", joinHtml(view.history.map((h) => settingsRecord({
85
+ title: `Selection ${h.revision}`,
86
+ sub: h.enabled.length ? h.enabled.map((s) => `${s.name} · ${s.version}`).join(", ") : "No skills enabled",
87
+ when: html `${h.actor} · ${whenUtc(h.at)}`,
88
+ action: manage && h.revision !== view.revision ? form("restore", btn("Restore selection", { sm: true, type: "submit" }), { restore: h.revision }) : note("Current selection"),
89
+ }))))
43
90
  : "";
44
- return html `<section class="skills">${options.error ? html `<p class="problem" role="alert">${options.error}</p>` : ""}<p>${active ? `${active} skill${active === 1 ? "" : "s"} enabled for new tasks.` : "No skills enabled for this project."} Active tasks keep their saved versions.</p>${add}${search}${cards.length ? cards : html `<p>Your library is empty. Add a skill to review and enable it here.</p>`}${history}</section>`;
91
+ const library = settingsSection({
92
+ id: "library", title: "Library",
93
+ lead: `${active ? `${active} skill${active === 1 ? "" : "s"} enabled for new tasks.` : "No skills enabled for this project."} Active tasks keep their saved versions.`,
94
+ body: joinHtml([add, search, cards.length ? joinHtml(cards) : note("Your library is empty. Add a skill to review and enable it here."), history]),
95
+ });
96
+ return joinHtml([options.error ? notice(options.error) : "", library]);
45
97
  }
46
98
  export function skillsSnapshotHtml(snapshot) {
47
99
  if (!snapshot || !snapshot.packages.length)
@@ -49,7 +101,7 @@ export function skillsSnapshotHtml(snapshot) {
49
101
  return html `<details class="skills"><summary>Skills supplied (${snapshot.packages.length})</summary><p class="meta">Exact versions supplied to this run. This does not confirm the agent read or applied them.</p>${snapshot.packages.map((s) => html `<details><summary>${s.name} · ${s.sha.slice(0, 10)}</summary>${source(s)}</details>`)}</details>`;
50
102
  }
51
103
  export function skillsScript() {
52
- return `(()=>{const search=document.querySelector('[data-skill-search]');if(search)search.addEventListener('input',()=>{let found=0;document.querySelectorAll('[data-skill-group]').forEach(el=>{el.hidden=!el.dataset.skillQuery.toLowerCase().includes(search.value.trim().toLowerCase());if(!el.hidden)found++;});document.querySelector('[data-skill-no-matches]').hidden=found>0;});const form=document.querySelector('[data-skill-import]');if(!form)return;const method=form.querySelector('[data-skill-method]'),error=form.querySelector('[data-skill-error]');function sync(){form.querySelectorAll('[data-skill-input]').forEach(el=>{el.hidden=el.dataset.skillInput!==method.value;el.querySelectorAll('input,textarea').forEach(i=>i.disabled=el.hidden);});}method.addEventListener('change',sync);sync();form.addEventListener('submit',async event=>{if(method.value!=='folder')return;if(form.dataset.prepared==='yes'){delete form.dataset.prepared;return;}event.preventDefault();error.hidden=true;const button=form.querySelector('button');button.disabled=true;try{const files=Array.from(form.querySelector('[data-skill-folder]').files);if(!files.length||files.length>64||files.reduce((n,f)=>n+f.size,0)>1048576||files.some(f=>f.size>262144))throw Error('Choose one skill folder with up to 64 files and 1 MB total.');const packed=[];for(const file of files){const parts=file.webkitRelativePath.split('/');parts.shift();const data=new Uint8Array(await file.arrayBuffer());let binary='';for(const byte of data)binary+=String.fromCharCode(byte);packed.push({path:parts.join('/'),base64:btoa(binary)});}form.querySelector('[name=files]').value=JSON.stringify(packed);form.dataset.prepared='yes';button.disabled=false;form.requestSubmit();}catch(e){error.textContent=e.message||'The folder could not be read. Select it again.';error.hidden=false;button.disabled=false;}});})();`;
104
+ return `(()=>{const search=document.querySelector('[data-skill-search]');if(search)search.addEventListener('input',()=>{let found=0;document.querySelectorAll('[data-skill-group]').forEach(el=>{el.hidden=!el.dataset.skillQuery.toLowerCase().includes(search.value.trim().toLowerCase());if(!el.hidden)found++;});document.querySelector('[data-skill-no-matches]').hidden=found>0;});const form=document.querySelector('[data-skill-import]');if(!form)return;const method=form.querySelector('[data-skill-method]'),error=form.querySelector('[data-skill-error]');function sync(){form.querySelectorAll('[data-skill-input]').forEach(el=>{el.hidden=el.dataset.skillInput!==method.value;el.querySelectorAll('input,textarea').forEach(i=>i.disabled=el.hidden);});}method.addEventListener('change',sync);sync();form.addEventListener('submit',async event=>{if(method.value!=='folder')return;if(form.dataset.prepared==='yes'){delete form.dataset.prepared;return;}event.preventDefault();error.hidden=true;error.textContent='';const button=form.querySelector('button');button.disabled=true;try{const files=Array.from(form.querySelector('[data-skill-folder]').files);if(!files.length||files.length>64||files.reduce((n,f)=>n+f.size,0)>1048576||files.some(f=>f.size>262144))throw Error('Choose one skill folder with up to 64 files and 1 MB total.');const packed=[];for(const file of files){const parts=file.webkitRelativePath.split('/');parts.shift();const data=new Uint8Array(await file.arrayBuffer());let binary='';for(const byte of data)binary+=String.fromCharCode(byte);packed.push({path:parts.join('/'),base64:btoa(binary)});}form.querySelector('[name=files]').value=JSON.stringify(packed);form.dataset.prepared='yes';button.disabled=false;form.requestSubmit();}catch(e){error.textContent=e.message||'The folder could not be read. Select it again.';error.hidden=false;button.disabled=false;}});})();`;
53
105
  }
54
106
  export function skillTestFeedbackHtml(test, csrf, canManage, error = "", draft = "") {
55
107
  return html `<section class="skills" id="skill-test-feedback"><h2>Improve this test</h2>${test.sourceRun ? html `<p>Revises test run #${test.sourceRun}.</p>` : ""}${test.feedback ? html `<details><summary>Feedback for this test</summary><p>${test.feedback}</p></details>` : ""}${test.revisions.length ? html `<p>${joinHtml(test.revisions.map((id, i) => html `<a href="/t/${encodeURIComponent(id)}">Test revision ${i + 1}</a>`), " · ")}</p>` : ""}${error ? html `<p role="alert">${error}</p>` : ""}${canManage && csrf ? postForm("/settings/skills/revise", html `<label>What should change?<textarea name="feedback" required maxlength="500" rows="3" placeholder="Try a clearer action label and explain why it fits the user’s goal.">${draft}</textarea></label><p class="meta">Creates another report task with this skill version and your feedback. Review its scope before it runs.</p><button>Create revision</button>`, { hidden: fields({ repo: test.repo, run: test.run, nonce: randomUUID() }) }) : ""}</section>`;
@@ -1,13 +1,20 @@
1
- import { html } from "./html.js";
1
+ import { html, joinHtml } from "./html.js";
2
2
  import { loadSlackCredentials } from "./slack-api.js";
3
- import { chatSettingsHtml } from "./chat-settings-page.js";
3
+ import { chatSecretRow, chatSettingsHtml } from "./chat-settings-page.js";
4
4
  export function slackSettingsHtml(store, dir, options = {}) {
5
5
  return chatSettingsHtml(store, dir, options, {
6
6
  provider: "slack",
7
7
  label: "Slack",
8
8
  credentials: loadSlackCredentials(dir),
9
- setup: html `<details open><summary>Create your Slack app</summary><ol><li>Download the <a href="/settings/slack/manifest">app manifest</a>, then <a href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer">create an app</a> in your workspace using that manifest.</li><li>Under Basic Information, create an app-level token with <code>connections:write</code>.</li><li>Under OAuth &amp; Permissions, install the app and copy its bot token.</li></ol></details>`,
10
- connect: password => html `<label>App token<input style="min-height:44px" type="password" name="app-token" autocomplete="off" placeholder="xapp-…" required></label><label>Bot token<input style="min-height:44px" type="password" name="bot-token" autocomplete="off" placeholder="xoxb-…" required></label>${password}<p class="meta">Tokens stay on this installation. Next, pair your own Slack account.</p><button type="submit">Connect Slack</button>`,
11
- access: html `<p>Private messages and threads use your Toolroll permissions. Shared channels are not supported. Secure review links use this installation’s configured HTTPS console address.</p><p>Socket Mode connects out to Slack. It does not need a public webhook or a Tailscale account.</p>`,
9
+ intro: "Manage projects and review results in a private Slack conversation.",
10
+ steps: html `<ol class="k-note"><li>Download the <a class="k-link" href="/settings/slack/manifest">app manifest</a>, then <a class="k-link" href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer">create an app</a> in your workspace using that manifest.</li><li>Under Basic Information, create an app-level token with <code class="k-mono">connections:write</code>.</li><li>Under OAuth &amp; Permissions, install the app and copy its bot token.</li></ol>`,
11
+ fields: joinHtml([
12
+ chatSecretRow({ id: "slack-app-token", name: "app-token", label: "App token", placeholder: "xapp-…" }),
13
+ chatSecretRow({ id: "slack-bot-token", name: "bot-token", label: "Bot token", placeholder: "xoxb-…" }),
14
+ ]),
15
+ fieldsNote: "Tokens stay on this installation. Next, pair your own Slack account.",
16
+ pairWhere: "in a direct message to your Toolroll Slack app",
17
+ tryLine: html `Try “What needs my attention?” or “Show the evidence for the latest result.”`,
18
+ access: html `<p class="k-note">Private messages and threads 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">Socket Mode connects out to Slack. It does not need a public webhook or a Tailscale account.</p>`,
12
19
  });
13
20
  }
@@ -1,50 +1,69 @@
1
1
  /**
2
- * The Spend page (docs/plans/spend-dashboard.md, slice 1): what agent work cost this month, with API dollars, plan
3
- * usage and unknown cost kept apart and never added together, broken down by project, model, role and person, the
4
- * runs that cost most, and what isn't recorded here. An instance operator's page. Monthly limits show read-only here
5
- * and change on Settings → Spend, with a step-up.
2
+ * The Spend page (docs/plans/spend-dashboard.md, slice 1), in the kit's Settings frame (group "spend"): the monthly
3
+ * limits first (the one that needs you on top), then what agent work cost in a month, with API dollars, plan usage
4
+ * and unknown cost kept apart and never added together, broken down by project and model (and, on demand, role,
5
+ * person and the costliest runs), the plan windows, and what isn't recorded here. An instance operator's page.
6
+ * A limit changes in place: Edit limit opens its form under the row (`?edit=<target>`), saved with a step-up.
6
7
  */
7
8
  import { type Html } from "./html.js";
9
+ import { type ApprovalFacts } from "./design/kit.js";
8
10
  import { type BudgetState, type SpendItem } from "./spend.js";
9
11
  import type { SpendReport } from "./spend-report.js";
12
+ import type { BrowserLimits } from "./browser-workspace.js";
13
+ /** The page's own layout: the limit rows and meters, the summary figures and the quiet bars. Colour from tokens only. */
10
14
  export declare const SPEND_CSS: string;
11
15
  /** What this page doesn't record, in plain words (docs/plans/spend-dashboard.md §2.2). */
12
16
  export declare const NOT_COUNTED: readonly string[];
17
+ /** A limit's row: a saved limit, or a target with none yet (Everything, a project) and what it spent so far. */
18
+ export type LimitRowView = {
19
+ target: string;
20
+ label: string;
21
+ budget: BudgetState | null;
22
+ spentMicrousd: number;
23
+ };
24
+ /** The open limit form: whose, the draft the person typed, and a refusal next to the field or under the form. */
25
+ export type LimitEdit = {
26
+ target: string;
27
+ usd?: string | null;
28
+ stop?: boolean | null;
29
+ fieldError?: string | null;
30
+ problem?: string | null;
31
+ };
13
32
  export type SpendView = {
14
33
  month: string;
15
34
  previous: string;
16
35
  next: string | null;
17
36
  report: SpendReport;
18
- /** This month's limits; null for a past month (a limit is this month's). */
37
+ /** This month's saved limits (what the budget gate counts today), whichever month is shown. */
19
38
  budgets: BudgetState[] | null;
20
- /** The plan windows as their providers last reported them (limits-ui.ts), or nothing. */
21
- windows: Html;
22
- subagentNames: Map<number, string>;
23
- };
24
- /** Dollars for the page: a positive amount under a cent reads "<$0.01", never a false $0. */
25
- export declare const money: (microusd: number) => string;
26
- export declare function spendHtml(view: SpendView, notice: {
27
- said?: string | null;
28
- problem?: string | null;
29
- }): Html;
30
- export type SpendSettingsView = {
31
- budgets: BudgetState[];
32
- targets: {
39
+ /** Everything and each project with no limit yet, with what it spent this month. */
40
+ unlimited?: LimitRowView[];
41
+ /** People and subagents a limit can be added for (Add a limit). */
42
+ targets?: {
33
43
  value: string;
34
44
  label: string;
35
- group: "Everything" | "Projects" | "People" | "Subagents";
45
+ group: "People" | "Subagents";
36
46
  }[];
47
+ /** The plan windows as their providers last reported them (limits-ui.ts limitsView), or nothing. */
48
+ windows: BrowserLimits | null;
37
49
  subagentNames: Map<number, string>;
38
- /** Each role's model at the installation, in words. */
39
- models: {
40
- role: string;
41
- model: string;
42
- }[];
50
+ /** The limit form that is open, if any. */
51
+ edit?: LimitEdit | null;
52
+ /** What this request's approval shows (approvalFactsNow): a step-up asks for the password unless Passwordless is on. */
53
+ facts?: ApprovalFacts;
54
+ /** This month's name (YYYY-MM): the limits are this month's. */
55
+ current?: string;
43
56
  };
44
- /** Settings → Spend: the monthly limits editor (moved from the Spend page) and a summary of the models in use. */
45
- export declare function spendSettingsHtml(view: SpendSettingsView, notice: {
57
+ /** Dollars for the page: a positive amount under a cent reads "<$0.01", never a false $0. */
58
+ export declare const money: (microusd: number) => string;
59
+ /** A limit Settings flags as Needs you: one that pauses work and is at or near its limit. */
60
+ export declare const limitNeedsYou: (budget: BudgetState) => boolean;
61
+ export declare function spendHtml(view: SpendView, said: {
46
62
  said?: string | null;
47
63
  problem?: string | null;
48
64
  }): Html;
65
+ /** Checks the amount next to its field before any password is asked or the form leaves the page; the server checks
66
+ * it again. Remove skips it. */
67
+ export declare function spendScript(): string;
49
68
  /** One row per piece of spend, for a spreadsheet (the command line's `spend --csv`). */
50
69
  export declare function spendCsv(items: readonly SpendItem[], subagentNames: Map<number, string>): string;