toolroll 0.9.86 → 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 (171) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/agents-board.d.ts +12 -0
  3. package/dist/agents-board.js +26 -0
  4. package/dist/approval-rules-ui.d.ts +4 -1
  5. package/dist/approval-rules-ui.js +31 -15
  6. package/dist/assignment-ui.js +1 -1
  7. package/dist/backup-ui.d.ts +5 -4
  8. package/dist/backup-ui.js +65 -32
  9. package/dist/browser/terminal.css +1 -1
  10. package/dist/browser/terminal.js +57 -54
  11. package/dist/browser/workspace.css +2 -2
  12. package/dist/browser/workspace.js +35 -70
  13. package/dist/browser-crew.js +1 -1
  14. package/dist/browser-workspace.d.ts +329 -183
  15. package/dist/browser-workspace.js +10 -9
  16. package/dist/chat-controls.d.ts +1 -1
  17. package/dist/chat-controls.js +1 -1
  18. package/dist/chat-settings-page.d.ts +29 -9
  19. package/dist/chat-settings-page.js +59 -28
  20. package/dist/check-levels-ui.d.ts +2 -1
  21. package/dist/check-levels-ui.js +67 -58
  22. package/dist/contracts/cli.d.ts +43 -0
  23. package/dist/contracts/cli.js +4 -0
  24. package/dist/contracts/console-api.d.ts +1 -1
  25. package/dist/contracts/console-api.js +1 -1
  26. package/dist/credentials-ui.d.ts +5 -3
  27. package/dist/credentials-ui.js +58 -31
  28. package/dist/design/kit.css +685 -0
  29. package/dist/design/kit.d.ts +208 -0
  30. package/dist/design/kit.js +177 -0
  31. package/dist/design/tokens.css +19 -1
  32. package/dist/discord-settings.js +8 -4
  33. package/dist/export-ui.d.ts +6 -4
  34. package/dist/export-ui.js +18 -8
  35. package/dist/flow-changed.d.ts +14 -0
  36. package/dist/flow-changed.js +53 -0
  37. package/dist/flow-gallery-ui.d.ts +41 -4
  38. package/dist/flow-gallery-ui.js +95 -63
  39. package/dist/flow-starters-ui.d.ts +3 -9
  40. package/dist/flow-starters-ui.js +12 -23
  41. package/dist/flow-unattended-words.d.ts +20 -0
  42. package/dist/flow-unattended-words.js +24 -0
  43. package/dist/flow-unattended.d.ts +9 -5
  44. package/dist/flow-unattended.js +45 -23
  45. package/dist/flows-cli.js +7 -3
  46. package/dist/flows-ui.d.ts +12 -6
  47. package/dist/flows-ui.js +141 -30
  48. package/dist/integrations-ui.d.ts +6 -4
  49. package/dist/integrations-ui.js +34 -40
  50. package/dist/integrations.d.ts +2 -2
  51. package/dist/integrations.js +4 -4
  52. package/dist/kits-ui.d.ts +8 -10
  53. package/dist/kits-ui.js +29 -33
  54. package/dist/kits.js +1 -1
  55. package/dist/knowledge-ui.js +76 -19
  56. package/dist/ledger-view.d.ts +2 -0
  57. package/dist/ledger-view.js +41 -38
  58. package/dist/models-ui.d.ts +8 -3
  59. package/dist/models-ui.js +82 -59
  60. package/dist/monitoring-ui.d.ts +4 -2
  61. package/dist/monitoring-ui.js +44 -13
  62. package/dist/needs-you.d.ts +1 -1
  63. package/dist/needs-you.js +8 -0
  64. package/dist/operate-remote-arguments.js +1 -0
  65. package/dist/operate-remote.js +1 -1
  66. package/dist/operate.d.ts +1 -1
  67. package/dist/operate.js +38 -4
  68. package/dist/passwordless.d.ts +1 -1
  69. package/dist/passwordless.js +1 -1
  70. package/dist/people-audit-ui.d.ts +2 -1
  71. package/dist/people-audit-ui.js +25 -23
  72. package/dist/policy-ui.d.ts +3 -2
  73. package/dist/policy-ui.js +27 -21
  74. package/dist/policy.d.ts +3 -0
  75. package/dist/policy.js +14 -1
  76. package/dist/project-delete-ui.d.ts +4 -2
  77. package/dist/project-delete-ui.js +135 -126
  78. package/dist/project-knowledge.d.ts +10 -10
  79. package/dist/publish.d.ts +11 -0
  80. package/dist/publish.js +16 -3
  81. package/dist/pull-request-ui.d.ts +2 -1
  82. package/dist/pull-request-ui.js +39 -28
  83. package/dist/recipe-ui.d.ts +4 -2
  84. package/dist/recipe-ui.js +14 -4
  85. package/dist/release-adapters.d.ts +3 -0
  86. package/dist/release-adapters.js +22 -2
  87. package/dist/release-cli.js +81 -9
  88. package/dist/release.d.ts +21 -0
  89. package/dist/release.js +40 -4
  90. package/dist/releases.js +1 -1
  91. package/dist/request-budget-ui.d.ts +4 -2
  92. package/dist/request-budget-ui.js +30 -19
  93. package/dist/retention-ui.d.ts +4 -3
  94. package/dist/retention-ui.js +15 -12
  95. package/dist/scope.d.ts +3 -0
  96. package/dist/scope.js +6 -0
  97. package/dist/serve.d.ts +0 -1
  98. package/dist/serve.js +51 -7
  99. package/dist/server/agents-computers.d.ts +9 -0
  100. package/dist/server/agents-computers.js +67 -0
  101. package/dist/server/chat.js +16 -16
  102. package/dist/server/chrome.d.ts +1 -1
  103. package/dist/server/chrome.js +19 -9
  104. package/dist/server/console.css +2 -2
  105. package/dist/server/flows.js +64 -36
  106. package/dist/server/home.d.ts +35 -0
  107. package/dist/server/home.js +104 -0
  108. package/dist/server/http.d.ts +3 -0
  109. package/dist/server/http.js +5 -0
  110. package/dist/server/kit-standalone.d.ts +22 -0
  111. package/dist/server/kit-standalone.js +69 -0
  112. package/dist/server/kit.d.ts +405 -0
  113. package/dist/server/kit.js +361 -0
  114. package/dist/server/pages.d.ts +2 -1
  115. package/dist/server/pages.js +213 -174
  116. package/dist/server/people-tokens.js +26 -85
  117. package/dist/server/render-history.d.ts +51 -0
  118. package/dist/server/render-history.js +228 -0
  119. package/dist/server/render-new-task.d.ts +29 -0
  120. package/dist/server/render-new-task.js +96 -0
  121. package/dist/server/render-next.d.ts +63 -0
  122. package/dist/server/render-next.js +109 -0
  123. package/dist/server/render-pages.d.ts +22 -190
  124. package/dist/server/render-pages.js +123 -575
  125. package/dist/server/render-people.d.ts +24 -3
  126. package/dist/server/render-people.js +93 -72
  127. package/dist/server/render-projects.d.ts +103 -0
  128. package/dist/server/render-projects.js +247 -0
  129. package/dist/server/render-results.d.ts +5 -24
  130. package/dist/server/render-results.js +48 -185
  131. package/dist/server/render-settings.d.ts +5 -74
  132. package/dist/server/render-settings.js +102 -493
  133. package/dist/server/render-tasks.d.ts +35 -5
  134. package/dist/server/render-tasks.js +114 -85
  135. package/dist/server/route-table.js +20 -5
  136. package/dist/server/settings.js +276 -255
  137. package/dist/server/task-views.d.ts +8 -0
  138. package/dist/server/task-views.js +85 -2
  139. package/dist/server/tasks.d.ts +8 -0
  140. package/dist/server/tasks.js +95 -40
  141. package/dist/server/terminals.js +1 -1
  142. package/dist/server/welcome.d.ts +22 -0
  143. package/dist/server/welcome.js +69 -0
  144. package/dist/settings-groups-ui.d.ts +279 -0
  145. package/dist/settings-groups-ui.js +615 -0
  146. package/dist/settings-kit.d.ts +42 -6
  147. package/dist/settings-kit.js +69 -7
  148. package/dist/setup-guide.d.ts +2 -2
  149. package/dist/skills-ui.js +59 -7
  150. package/dist/slack-settings.js +12 -5
  151. package/dist/spend-ui.d.ts +45 -26
  152. package/dist/spend-ui.js +124 -56
  153. package/dist/sso-ui.d.ts +5 -2
  154. package/dist/sso-ui.js +63 -31
  155. package/dist/storage-ui.d.ts +3 -2
  156. package/dist/storage-ui.js +56 -50
  157. package/dist/store.d.ts +112 -3
  158. package/dist/store.js +292 -20
  159. package/dist/surface.js +5 -1
  160. package/dist/teams-settings.d.ts +2 -1
  161. package/dist/teams-settings.js +24 -41
  162. package/dist/telegram-settings.d.ts +0 -4
  163. package/dist/telegram-settings.js +52 -25
  164. package/dist/toolroll-update-ui.d.ts +8 -4
  165. package/dist/toolroll-update-ui.js +55 -45
  166. package/dist/tools-ui.d.ts +5 -3
  167. package/dist/tools-ui.js +107 -42
  168. package/dist/work-index.js +11 -7
  169. package/dist/workspace-ui.d.ts +1 -1
  170. package/dist/workspace-ui.js +7 -4
  171. package/package.json +1 -1
@@ -1,13 +1,23 @@
1
1
  import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate, type GalleryTool } from "./flow-gallery.js";
2
2
  import { type FlowDefinition, type FlowStage } from "./flows.js";
3
- import { type Html } from "./html.js";
3
+ import { type Html, type HtmlValue } from "./html.js";
4
4
  export declare const GALLERY_CSS: string;
5
5
  /** A flow's main path, as a person reads it: from the start, each step's first way on (a sort's first answer, a choose's
6
6
  * first option that goes somewhere, else next; a check's failure when passing just ends), never back to a step shown. */
7
7
  export declare function mainPath(definition: FlowDefinition): FlowStage[];
8
- /** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
8
+ /** A flow's steps in order, joined by arrows. */
9
9
  export declare function stepStrip(definition: FlowDefinition): Html;
10
- /** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
10
+ /** One row of New flow: name · promise · one line · one outline action (the same shape for templates, kits, recipes and starters). */
11
+ export declare function templateRow(options: {
12
+ name: string;
13
+ promise: string;
14
+ line?: HtmlValue;
15
+ action?: HtmlValue;
16
+ attrs?: Html;
17
+ }): Html;
18
+ /** The groups New flow filters by: the template groups, then kits, recipes and starter flows. */
19
+ export declare const NEW_FLOW_GROUPS: ReadonlyArray<readonly [key: string, label: string]>;
20
+ /** The templates as rows, grouped (or one group): each with one "Use this". `repo` carries a chosen project to the template's page. */
11
21
  export declare function galleryHtml(input: {
12
22
  repo: string | null;
13
23
  canUse: boolean;
@@ -15,8 +25,35 @@ export declare function galleryHtml(input: {
15
25
  id: string;
16
26
  state: "connected" | "open" | "taken";
17
27
  }[] | null;
28
+ group?: string;
29
+ }): Html;
30
+ /**
31
+ * New flow: describe it (the shared composer; the words go to the lead as a chat draft, the project on its bar), then
32
+ * start from a template, filtered by group. Kits, recipes and starter flows arrive as their own rows.
33
+ */
34
+ export declare function newFlowHtml(input: {
35
+ repo: string | null;
36
+ projects: readonly {
37
+ path: string;
38
+ name: string;
39
+ }[];
40
+ canUse: boolean;
41
+ group: string;
42
+ connections: readonly {
43
+ id: string;
44
+ state: "connected" | "open" | "taken";
45
+ }[] | null;
46
+ ask: {
47
+ value: string;
48
+ error: string | null;
49
+ };
50
+ kits: Html;
51
+ recipes: Html;
52
+ starters: Html;
53
+ said: string | null;
54
+ problem: string | null;
18
55
  }): Html;
19
- /** A template's page: its questions, then what it will do and never do, then Create. */
56
+ /** A template's page, a reading page: its steps and tools, its questions, what it will do and never do, then Create. */
20
57
  export declare function galleryUseHtml(input: {
21
58
  template: GalleryTemplate;
22
59
  projects: readonly {
@@ -1,40 +1,30 @@
1
1
  /**
2
- * The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
3
- * its promise, its steps in order, what it needs and "Use this". A template's page asks only what it
4
- * needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
2
+ * New flow (Flows → New): describe it to the lead, or start from a template. Templates are rows grouped by what
3
+ * they're for, each with its promise, its steps in order, what it needs and one "Use this"; the starter kits ("With
4
+ * an agent"), the recipes ("One-time work") and the starter flows join them as groups. A template's page asks only
5
+ * what it needs, previews in plain words what it will do and never do, and creates it: the same answers it previewed.
5
6
  * A template's tools show their marks and whether the project has them; one it lacks offers Connect first.
6
7
  */
7
8
  import { brandMarkHtml } from "./brand-mark.js";
8
9
  import { BLANK, GALLERY, GALLERY_GROUPS, galleryAdmitsAnyone, galleryDiagram, galleryToolsOf, OUTDATED_COMMANDS, SEND_RESULT, sendsAlready } from "./flow-gallery.js";
9
10
  import { choiceTargets } from "./flows.js";
10
11
  import { html, joinHtml, postForm, reauth } from "./html.js";
11
- export const GALLERY_CSS = `.gallery{max-width:1120px;min-width:0}.gallery h2{margin:28px 0 10px;font-size:.9375rem}.gallery h2:first-of-type{margin-top:8px}` +
12
- `.gallery-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}` +
13
- `.gallery-card{display:flex;flex-direction:column;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}` +
14
- `.gallery-card h3{margin:0;font-size:.9375rem}.gallery-card p{margin:0;overflow-wrap:anywhere}.gallery-card .gallery-promise{line-height:1.5;flex:1}` +
15
- // A template's steps in order, joined by arrows; a person's own steps carry the attention chip.
16
- `.gallery-steps{display:flex;flex-wrap:wrap;align-items:center;gap:4px 6px;list-style:none;padding:0;margin:0;font-size:12px;font-weight:500;line-height:1.5;color:var(--so-ink)}` +
17
- `.gallery-steps li{display:inline-flex;align-items:center;gap:6px;min-width:0;overflow-wrap:anywhere}.gallery-steps li>span[aria-hidden]{color:var(--so-muted)}` +
18
- `.gallery-steps b{font-weight:500;font-size:11.5px;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-signal-soft);color:var(--so-signal)}` +
19
- `.gallery-needs{display:flex;flex-wrap:wrap;gap:4px;list-style:none;padding:0;margin:0}.gallery-needs li{font-size:11.5px;font-weight:500;line-height:18px;padding:1px 6px;border-radius:5px;background:var(--so-neutral-soft,var(--so-raised));color:var(--so-neutral-ink,var(--so-muted))}` +
20
- // An outline action in a list (the Quiet List Rule), over the workspace's ink default for .button-link.
21
- `.gallery .gallery-card a.button-link{align-self:flex-start;min-height:36px;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
22
- `.gallery-blank{margin-top:20px}` +
23
- // A template's tools: the mark Settings → Integrations shows, its name and where the project stands.
24
- `.gallery-tools{display:grid;gap:6px;list-style:none;padding:0;margin:0}.gallery-tools li{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}.gallery-tools li>span:not(.brand-mark){min-width:0;overflow-wrap:anywhere}` +
25
- `.gallery-tools strong{font-weight:500;overflow-wrap:anywhere}.gallery-connect{display:grid;gap:10px;padding:12px 0;border-top:1px solid var(--so-line)}.gallery-connect:last-child{border-bottom:1px solid var(--so-line);margin-bottom:4px}` +
26
- `.gallery-connect form{display:grid;gap:8px;margin:0}.gallery-connect p{margin:0;overflow-wrap:anywhere}.gallery-connect .gallery-tool-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.gallery-connect button{justify-self:start;min-height:36px;white-space:nowrap;background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}` +
27
- `.gallery-use{max-width:720px;min-width:0}.gallery-use .gallery-steps{margin:4px 0 12px}.gallery-use form{display:grid;gap:12px;margin:0}.gallery-use label{display:grid;gap:4px;font-weight:500}` +
28
- `.gallery-use label small{font-weight:400;color:var(--so-muted)}.gallery-use label.gallery-check{display:flex;align-items:flex-start;gap:8px;font-weight:500}.gallery-use label.gallery-check input{margin-top:3px;width:16px;height:16px;min-height:0;flex:none}.gallery-use label.gallery-check small{display:block}` +
29
- `.gallery-use input:not([type=hidden]),.gallery-use select{box-sizing:border-box;width:100%;max-width:100%;min-height:32px;padding:0 10px;border:1px solid var(--so-input-line);border-radius:8px;background:var(--so-paper);color:var(--so-ink);font:inherit}` +
30
- `.gallery-preview{display:grid;gap:8px;padding:16px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);min-width:0}.gallery-preview h2{margin:0;font-size:.9375rem}` +
31
- `.gallery-preview ul{margin:0;padding-left:18px;line-height:1.55}.gallery-preview p{margin:0;overflow-wrap:anywhere}.gallery-preview .gallery-never{font-weight:600}` +
32
- `.gallery-preview details ol{margin:6px 0 0;padding-left:20px;white-space:pre-line;font-size:.875rem;line-height:1.5;color:var(--so-muted)}.gallery-preview details li{margin:0 0 6px;overflow-wrap:anywhere}` +
33
- `.gallery-preview summary{cursor:pointer;min-height:36px;display:flex;align-items:center;font-weight:500}` +
34
- `.gallery-actions{display:flex;flex-wrap:wrap;gap:8px;margin:0}.gallery-actions button{min-height:36px;white-space:nowrap}` +
35
- `.gallery-use .gallery-actions button[value=create]:not(.secondary){background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}.gallery-heading{margin:36px 0 0;font-size:1.05rem}` +
36
- `@media(max-width:760px){.gallery-grid{grid-template-columns:minmax(0,1fr)}.gallery-connect button{min-height:44px;justify-self:stretch}.gallery .gallery-card a.button-link{min-height:44px;align-self:stretch;justify-content:center}` +
37
- `.gallery-use input:not([type=hidden]),.gallery-use select{font-size:16px;min-height:44px}.gallery-actions button{flex:1 1 auto;min-height:44px}}`;
12
+ import { acts, btn, chip, composer, count, disclose, doc, docHeader, dock, field, group, infoLine, input as kInput, note, notice, phoneHeader, segLinks, select, sendButton } from "./server/kit.js";
13
+ export const GALLERY_CSS = `.fl-new .k-sec{gap:12px}.fl-th{display:grid;justify-items:start;gap:10px}.fl-th .k-seg{flex-wrap:wrap}.fl-tsel{justify-self:stretch}.fl-tsel form{display:flex;gap:8px;margin:0}.fl-tsel .k-selw{flex:1}` +
14
+ `.fl-tlist{display:grid}.fl-tlist .k-grp{padding:0;margin-top:16px}.fl-tlist .k-grp:first-child{margin-top:0}` +
15
+ `.fl-trow{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:center;padding:12px 0;border-bottom:1px solid var(--so-line)}` +
16
+ `.fl-tmain{display:grid;gap:3px;min-width:0}.fl-tmain>b{font:500 13.5px/1.35 var(--so-font-sans);color:var(--so-ink)}.fl-tp{color:var(--so-muted);font-size:12.5px;line-height:1.45;overflow-wrap:anywhere}` +
17
+ `.fl-tline{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px;font-size:12px;margin-top:3px;color:var(--so-muted);min-width:0;overflow-wrap:anywhere}.fl-ta .k-btn{min-width:108px}.fl-ta form{margin:0}` +
18
+ // A template's steps in order, joined by arrows.
19
+ `.gallery-steps{display:inline-flex;flex-wrap:wrap;align-items:center;gap:4px 6px;list-style:none;padding:0;margin:0;font-size:12px;line-height:1.5;color:var(--so-ink)}` +
20
+ `.gallery-steps li{display:inline-flex;align-items:center;gap:6px;min-width:0;overflow-wrap:anywhere}.gallery-steps li>span[aria-hidden]{color:var(--so-muted)}.gallery-steps b{font-weight:500}` +
21
+ `.gallery-tools{display:inline-flex;flex-wrap:wrap;gap:4px 10px;list-style:none;padding:0;margin:0}.gallery-tools li{display:inline-flex;align-items:center;gap:6px;min-width:0}` +
22
+ `.fl-blank{padding-top:4px}.fl-describe-note{margin-top:2px}` +
23
+ // A template's page: the tools to connect, the form, and what it will do.
24
+ `.fl-use form{display:grid;gap:18px;margin:0}.fl-tool{display:grid;gap:10px;padding:12px 0;border-top:1px solid var(--so-line)}.fl-tool:last-child{border-bottom:1px solid var(--so-line)}.fl-tool form{gap:10px}` +
25
+ `.fl-tool-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:0;font-size:13px;font-weight:500}.fl-check{display:flex;align-items:flex-start;gap:8px;font-size:13px;font-weight:500;cursor:pointer}.fl-check input{margin-top:3px;width:16px;height:16px;flex:none}.fl-check .k-note{display:block;font-weight:400}` +
26
+ `.fl-will{display:grid;gap:10px;padding:16px 0;border-top:1px solid var(--so-line);border-bottom:1px solid var(--so-line)}.fl-will ul{margin:0;padding-left:18px;font-size:13px;line-height:1.55}.fl-will ol{margin:6px 0 0;padding-left:20px;white-space:pre-line;font-size:12.5px;line-height:1.5;color:var(--so-muted)}.fl-will li{overflow-wrap:anywhere}.fl-never{margin:0;font-size:13px;font-weight:600}` +
27
+ `@media(max-width:760px){.fl-page .k-btn.k-docked,.fl-new .k-btn.k-docked,.fl-use .k-btn.k-docked,.fl-kit .k-btn.k-docked{display:none}.fl-trow{grid-template-columns:minmax(0,1fr);gap:10px;padding:14px 0}.fl-ta .k-btn{width:100%}.fl-ta:empty{display:none}.fl-check input{width:20px;height:20px}}`;
38
28
  /** A flow's main path, as a person reads it: from the start, each step's first way on (a sort's first answer, a choose's
39
29
  * first option that goes somewhere, else next; a check's failure when passing just ends), never back to a step shown. */
40
30
  export function mainPath(definition) {
@@ -51,32 +41,64 @@ export function mainPath(definition) {
51
41
  }
52
42
  /** A step's short name in the strip: a person's own steps from their side. */
53
43
  const stepName = (stage) => stage.kind === "choose" ? "You choose" : stage.kind === "approval" ? "You approve" : stage.kind === "send" ? "Sent to you" : stage.title;
54
- /** A flow's steps in order, joined by arrows; a person's decisions carry the accent. */
44
+ /** A flow's steps in order, joined by arrows. */
55
45
  export function stepStrip(definition) {
56
46
  return html `<ol class="gallery-steps" aria-label="Steps">${mainPath(definition).map((one, i) => {
57
47
  const name = stepName(one);
58
- return html `<li>${i === 0 ? "" : html `<span aria-hidden="true">→</span>`}${one.kind === "choose" || one.kind === "approval" ? html `<b data-person>${name}</b>` : name}</li>`;
48
+ return html `<li>${i === 0 ? "" : html `<span aria-hidden="true">→</span>`}${one.kind === "choose" || one.kind === "approval" ? html `<b>${name}</b>` : name}</li>`;
59
49
  })}</ol>`;
60
50
  }
61
- const needsList = (template) => template.needs.length === 0 ? (template.tools ?? []).length > 0 ? "" : html `<ul class="gallery-needs"><li>Nothing to connect</li></ul>`
62
- : html `<ul class="gallery-needs" aria-label="Needs">${template.needs.map(one => html `<li>${one}</li>`)}</ul>`;
63
- /** A tool's state in words, as Settings → Integrations badges it; nothing without a project. */
64
- const STATE = { connected: ["connected", "Connected"], open: ["not-set-up", "Not connected"], taken: ["not-set-up", "Added another way"] };
65
- const toolState = (tool) => tool.state === null ? "" : html `<span class="integration-state integration-state--${STATE[tool.state][0]}"><i aria-hidden="true"></i>${STATE[tool.state][1]}</span>`;
51
+ /** What a template needs, in words: its needs, else its tools, else nothing to connect. */
52
+ const needsWords = (template) => template.needs.length > 0 ? `Needs ${template.needs.join(", ")}` : (template.tools ?? []).length > 0 ? "" : "Nothing to connect";
53
+ /** A tool's state in words, as Settings → Integrations says it; nothing without a project. */
54
+ const STATE = { connected: "Connected", open: "Not connected", taken: "Added another way" };
55
+ const toolState = (tool) => tool.state === null ? "" : chip(STATE[tool.state], tool.state === "connected" ? "done" : "", { dot: tool.state === "connected" });
66
56
  const toolMark = (tool) => brandMarkHtml(tool.id.replace(/-desktop$/, ""), tool.label, tool.state === "connected");
67
57
  const toolsList = (tools) => tools.length === 0 ? ""
68
- : html `<ul class="gallery-tools" aria-label="Tools">${tools.map(one => html `<li data-tool="${one.id}" data-state="${one.state ?? "unknown"}">${toolMark(one)}<strong>${one.label}</strong>${toolState(one)}</li>`)}</ul>`;
69
- /** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
58
+ : html `<ul class="gallery-tools" aria-label="Tools">${tools.map(one => html `<li data-tool="${one.id}" data-state="${one.state ?? "unknown"}">${toolMark(one)}<span>${one.label}${one.state === "open" ? " (not connected)" : ""}</span></li>`)}</ul>`;
59
+ /** One row of New flow: name · promise · one line · one outline action (the same shape for templates, kits, recipes and starters). */
60
+ export function templateRow(options) {
61
+ return html `<div class="fl-trow"${options.attrs ?? ""}><div class="fl-tmain"><b>${options.name}</b><span class="fl-tp">${options.promise}</span>${options.line ? html `<span class="fl-tline">${options.line}</span>` : ""}</div><div class="fl-ta">${options.action ?? ""}</div></div>`;
62
+ }
63
+ /** The groups New flow filters by: the template groups, then kits, recipes and starter flows. */
64
+ export const NEW_FLOW_GROUPS = [["all", "All"], ...GALLERY_GROUPS.map(one => [one.id, one.label]), ["kits", "With an agent"], ["recipes", "One-time work"], ["starters", "Starter flows"]];
65
+ /** The templates as rows, grouped (or one group): each with one "Use this". `repo` carries a chosen project to the template's page. */
70
66
  export function galleryHtml(input) {
71
67
  const href = (id) => `/flows/new/${id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}`;
72
- const card = (template) => {
68
+ const rowOf = (template) => {
73
69
  const definition = galleryDiagram(template);
74
- return html `<article class="gallery-card" data-template="${template.id}"><h3>${template.name}</h3><p class="gallery-promise">${template.promise}</p>${stepStrip(definition)}${toolsList(galleryToolsOf(template, definition, input.repo === null ? null : input.connections ?? null))}${needsList(template)}${input.canUse && html `<a class="button-link" href="${href(template.id)}" aria-label="Use ${template.name}">Use this</a>`}</article>`;
70
+ const needs = needsWords(template);
71
+ return templateRow({ name: template.name, promise: template.promise, attrs: html ` data-template="${template.id}"`,
72
+ line: html `${stepStrip(definition)}${toolsList(galleryToolsOf(template, definition, input.repo === null ? null : input.connections ?? null))}${needs ? html `<span>· ${needs}</span>` : ""}`,
73
+ action: input.canUse ? btn("Use this", { sm: true, href: href(template.id), attrs: { "aria-label": `Use ${template.name}` } }) : "" });
75
74
  };
76
- const groups = GALLERY_GROUPS.map(group => html `<h2 id="gallery-${group.id}">${group.label}</h2><div class="gallery-grid" data-group="${group.id}">${GALLERY.filter(one => one.group === group.id).map(card)}</div>`);
77
- return html `<section class="gallery">${groups}${input.canUse ? html `<p class="gallery-blank meta"><a href="${href(BLANK.id)}">Start from a blank flow</a></p>` : html `<p class="meta">An approver creates flows.</p>`}</section>`;
75
+ const shown = GALLERY_GROUPS.filter(one => input.group === undefined || input.group === "all" || input.group === one.id);
76
+ return html `${shown.map(one => { const templates = GALLERY.filter(template => template.group === one.id); return html `${group(one.label, count(templates.length))}<div data-group="${one.id}">${templates.map(rowOf)}</div>`; })}`;
77
+ }
78
+ /**
79
+ * New flow: describe it (the shared composer; the words go to the lead as a chat draft, the project on its bar), then
80
+ * start from a template, filtered by group. Kits, recipes and starter flows arrive as their own rows.
81
+ */
82
+ export function newFlowHtml(input) {
83
+ const groupKey = NEW_FLOW_GROUPS.some(([key]) => key === input.group) ? input.group : "all";
84
+ const keep = (key) => `/flows/new?${new URLSearchParams({ ...(key === "all" ? {} : { group: key }), ...(input.repo === null ? {} : { repo: input.repo }) }).toString()}`.replace(/\?$/, "");
85
+ const current = input.projects.find(one => one.path === input.repo)?.name ?? input.projects[0]?.name ?? "";
86
+ const describe = input.canUse ? html `<section class="k-sec" aria-labelledby="fl-dh"><h3 id="fl-dh">Describe what you want</h3>\
87
+ <form method="get" action="/flows/new" id="fl-ask-form">${composer({ id: "fl-ask", name: "describe", label: "What should happen", rows: 3, value: input.ask.value, error: input.ask.error,
88
+ placeholder: "Every night, run npm run e2e. When it fails, find the cause, fix it and open a pull request.",
89
+ project: { current, items: input.projects.map(one => one.name), name: "project" }, send: sendButton({ label: "Draft the flow" }),
90
+ note: "Opens in chat: your lead drafts it for you to check. Nothing runs until you create it." })}</form></section>` : "";
91
+ const show = (key) => groupKey === "all" || groupKey === key;
92
+ const filter = html `<span class="k-desk-only">${segLinks("Templates", groupKey, NEW_FLOW_GROUPS.map(([key, label]) => ({ key, label, href: keep(key) })))}</span>\
93
+ <span class="k-phone-only fl-tsel"><form method="get" action="/flows/new">${input.repo === null ? "" : html `<input type="hidden" name="repo" value="${input.repo}">`}${select({ name: "group", label: "Templates", items: NEW_FLOW_GROUPS, current: groupKey, wide: true, attrs: { "data-k-autosubmit": "" } })}<noscript>${btn("Show", { sm: true, type: "submit" })}</noscript></form></span>`;
94
+ const templates = GALLERY_GROUPS.some(one => show(one.id)) ? galleryHtml({ repo: input.repo, canUse: input.canUse, connections: input.connections, group: groupKey }) : "";
95
+ const blank = input.canUse && groupKey === "all" ? html `<p class="k-note fl-blank"><a href="/flows/new/${BLANK.id}${input.repo === null ? "" : `?repo=${encodeURIComponent(input.repo)}`}">Start from a blank flow</a></p>` : "";
96
+ const list = html `${templates}${show("kits") ? input.kits : ""}${show("recipes") ? input.recipes : ""}${show("starters") ? input.starters : ""}${blank}`;
97
+ const said = input.problem ? notice(input.problem) : input.said ? infoLine(input.said) : "";
98
+ return html `<div class="fl-new" data-kit-page>${phoneHeader("New flow", { back: "/flows" })}${doc(docHeader({ title: "New flow", back: ["/flows", "Flows"], meta: input.canUse ? "Describe it in a sentence, or start from a template." : "An approver creates flows.", same: true }), said, describe, html `<section class="k-sec" aria-labelledby="fl-th"><div class="fl-th"><h3 id="fl-th">Start from a template</h3>${filter}</div><div class="fl-tlist">${list}</div></section>`)}\
99
+ ${input.canUse ? dock(btn("Draft the flow", { variant: "primary", type: "submit", attrs: { form: "fl-ask-form" } })) : ""}</div>`;
78
100
  }
79
- /** A template's page: its questions, then what it will do and never do, then Create. */
101
+ /** A template's page, a reading page: its steps and tools, its questions, what it will do and never do, then Create. */
80
102
  export function galleryUseHtml(input) {
81
103
  const { template } = input;
82
104
  const tools = input.tools ?? input.preview?.tools ?? [];
@@ -84,32 +106,42 @@ export function galleryUseHtml(input) {
84
106
  const missing = tools.filter(one => one.state !== "connected" && one.state !== "taken");
85
107
  // Each tool with its state; one the project lacks offers its own Connect (the Tools page's, with the same password), and comes back here.
86
108
  const connect = (tool) => {
87
- const head = html `<p class="gallery-tool-head">${toolMark(tool)} <strong>${tool.label}</strong> ${toolState(tool)}</p>`;
109
+ const head = html `<p class="fl-tool-h">${toolMark(tool)} ${tool.label} ${toolState(tool)}</p>`;
88
110
  // Added another way, it is there: nothing to connect.
89
111
  if (tool.state === "connected" || tool.state === "taken" || input.csrf === "")
90
- return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="${String(tool.state)}">${head}</div>`;
112
+ return html `<div class="fl-tool" data-tool="${tool.id}" data-state="${String(tool.state)}">${head}</div>`;
91
113
  const how = tool.id === "figma-desktop" ? "Open the Figma desktop app and turn on its Dev Mode MCP server in Preferences." : `You sign in on ${tool.label}, then come back here.`;
92
- return html `<div class="gallery-connect" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `<p class="meta">${how}</p>${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button name="service" value="${tool.id}">Connect ${tool.label}</button>`, { hidden: { repo: input.repo, shown: input.repo, template: template.id } })}</div>`;
114
+ const fieldId = `fl-connect-${tool.id}`;
115
+ return html `<div class="fl-tool" data-tool="${tool.id}" data-state="open">${head}${postForm("/settings/tools/connect", html `${note(how)}${reauth(field({ id: fieldId, label: "Your Toolroll password", control: kInput({ id: fieldId, name: "password", type: "password", attrs: { autocomplete: "current-password", required: true } }) }))}${acts(btn(`Connect ${tool.label}`, { type: "submit", attrs: { name: "service", value: tool.id } }))}`, { hidden: { repo: input.repo, shown: input.repo, template: template.id } })}</div>`;
93
116
  };
94
- const toolsBlock = tools.length === 0 ? "" : html `<section class="gallery-tools-use" aria-label="Tools">${tools.map(connect)}</section>`;
117
+ const toolsBlock = tools.length === 0 ? "" : html `<section class="k-sec" aria-label="Tools"><h3>Tools</h3><div>${tools.map(connect)}</div></section>`;
95
118
  // Made without a tool, the preview names the zone that needs it.
96
119
  const zones = (tool) => joinHtml(tool.zones.map(zone => html `“${zone}”`), " and ");
97
- const needing = html `${missing.filter(one => one.zones.length > 0).map(one => html `<p class="meta" data-needs-tool="${one.id}">${zones(one)} need${one.zones.length === 1 ? "s" : ""} ${one.label}, which isn't connected.</p>`)}${tools.filter(one => one.state === "taken" && one.zones.length > 0).map(one => html `<p class="meta" data-tool-taken="${one.id}">${zones(one)} use${one.zones.length === 1 ? "s" : ""} ${one.label}, added another way.</p>`)}`;
98
- const field = (ask) => {
99
- const value = input.answers[ask.key] ?? ask.default;
100
- const hint = ask.hint === undefined ? "" : html ` <small>${ask.hint}</small>`;
101
- if (ask.key === "outdated")
102
- return html `<label>${ask.label}<select name="outdated">${Object.entries(OUTDATED_COMMANDS).map(([id, one]) => html `<option value="${id}"${id === value && html ` selected`}>${one.label}</option>`)}</select></label>`;
103
- return html `<label>${ask.label}${hint}<input name="${ask.key}" value="${value}" required maxlength="${ask.key === "command" ? 500 : ask.key === "team" ? 12 : 100}"${ask.key === "command" ? html ` spellcheck="false" autocapitalize="off"` : ask.key === "team" ? html ` spellcheck="false" autocapitalize="characters"` : ""}></label>`;
120
+ const needing = html `${missing.filter(one => one.zones.length > 0).map(one => html `<p class="k-note" data-needs-tool="${one.id}">${zones(one)} need${one.zones.length === 1 ? "s" : ""} ${one.label}, which isn't connected.</p>`)}${tools.filter(one => one.state === "taken" && one.zones.length > 0).map(one => html `<p class="k-note" data-tool-taken="${one.id}">${zones(one)} use${one.zones.length === 1 ? "s" : ""} ${one.label}, added another way.</p>`)}`;
121
+ const ask = (one, index) => {
122
+ const value = input.answers[one.key] ?? one.default;
123
+ const id = `fl-ask-${index}`;
124
+ if (one.key === "outdated")
125
+ return field({ id, label: one.label, control: select({ id, name: "outdated", items: Object.entries(OUTDATED_COMMANDS).map(([key, command]) => [key, command.label]), current: value, wide: true }) });
126
+ return field({ id, label: one.label, ...(one.hint === undefined ? {} : { hint: one.hint }), control: kInput({ id, name: one.key, value, hint: one.hint !== undefined,
127
+ attrs: { required: true, maxlength: one.key === "command" ? 500 : one.key === "team" ? 12 : 100, ...(one.key === "command" ? { spellcheck: "false", autocapitalize: "off" } : one.key === "team" ? { spellcheck: "false", autocapitalize: "characters" } : {}) } }) });
104
128
  };
105
129
  const project = input.projects.length > 1
106
- ? html `<label>Project<select name="repo">${input.projects.map(one => html `<option value="${one.path}"${one.path === input.repo && html ` selected`}>${one.name}</option>`)}</select></label>`
130
+ ? field({ id: "fl-use-project", label: "Project", control: select({ id: "fl-use-project", name: "repo", items: input.projects.map(one => [one.path, one.name]), current: input.repo, wide: true }) })
107
131
  : html `<input type="hidden" name="repo" value="${input.repo}">`;
108
- const preview = input.preview === null ? "" : html `<section class="gallery-preview" aria-labelledby="gallery-will"><h2 id="gallery-will">What it will do</h2><ul>${input.preview.built.does.map(one => html `<li>${one}</li>`)}</ul><p class="gallery-never">${input.preview.built.never}</p>${needing}${input.preview.startsFrom.length === 0 ? html `<p class="meta">Cards start when you add them.</p>` : input.preview.startsFrom.map(one => html `<p class="meta">Starts from: ${one}</p>`)}<details><summary>Every step</summary><ol>${input.preview.steps.map(one => html `<li>${one.replace(/^\d+\.\s/, "")}</li>`)}</ol></details></section>`;
109
- const sendResult = !sendsAlready(template) && html `<label class="gallery-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" && html ` checked`} data-send-result><span>${SEND_RESULT.title}<small>When a card finishes, what was done comes to you in your chat apps.</small></span></label>`;
110
- // A flow that brings in items from anyone takes the password to make (chat-management.md §0a.3).
111
- const step = input.preview !== null && galleryAdmitsAnyone(template) ? reauth(html `<label>Your password <span class="meta">(it brings in items from anyone)</span><input type="password" name="password" autocomplete="current-password"></label>`) : "";
112
- const actions = html `${step}<p class="gallery-actions">${input.preview !== null && html `<button type="submit" name="intent" value="create"${missing.length > 0 && html ` class="secondary" data-without-tools`}>Create flow</button>`}<button type="submit" name="intent" value="preview" class="secondary">${input.preview === null ? "Preview" : "Update preview"}</button></p>`;
113
- const form = postForm(`/flows/new/${template.id}`, html `${project}${template.asks.map(field)}<label>Name<input name="name" value="${input.name}" maxlength="80"></label>${sendResult}${preview}${actions}`, { attrs: { "data-gallery-use": true }, hidden: { previewed: input.preview?.digest ?? "" } });
114
- return html `<section class="gallery-use" data-gallery-template="${template.id}">${input.problem !== null && html `<p class="problem" role="alert">${input.problem}</p>`}${input.said ? html `<p role="status">${input.said}</p>` : ""}<p>${template.promise}</p>${input.diagram !== null && stepStrip(input.diagram)}${needsList(template)}${toolsBlock}${form}<p class="meta"><a href="/flows/new">All templates</a></p></section>`;
132
+ const preview = input.preview === null ? "" : html `<section class="k-sec fl-will" aria-labelledby="fl-will"><h3 id="fl-will">What it will do</h3><ul>${input.preview.built.does.map(one => html `<li>${one}</li>`)}</ul><p class="fl-never">${input.preview.built.never}</p>${needing}\
133
+ ${input.preview.startsFrom.length === 0 ? note("Cards start when you add them.") : input.preview.startsFrom.map(one => note(`Starts from: ${one}`))}${disclose("Every step", html `<ol>${input.preview.steps.map(one => html `<li>${one.replace(/^\d+\.\s/, "")}</li>`)}</ol>`)}</section>`;
134
+ const sendResult = !sendsAlready(template) && html `<label class="fl-check"><input type="checkbox" name="${SEND_RESULT.key}" value="yes"${input.answers[SEND_RESULT.key] === "yes" && html ` checked`} data-send-result><span>${SEND_RESULT.title}<span class="k-note">When a card finishes, what was done comes to you in your chat apps.</span></span></label>`;
135
+ // A flow that brings in items from anyone takes the password to make (chat-management.md §0a.3): a step-up, which Passwordless stands in for.
136
+ const anyone = input.preview !== null && galleryAdmitsAnyone(template);
137
+ const step = anyone ? reauth(field({ id: "fl-use-password", label: "Your password", hint: "It brings in items from anyone.", control: kInput({ id: "fl-use-password", name: "password", type: "password", hint: true, attrs: { autocomplete: "current-password" } }) })) : "";
138
+ const creates = input.preview !== null;
139
+ const createButton = (docked) => btn("Create flow", { variant: missing.length > 0 ? "outline" : "primary", type: "submit", ...(docked ? { docked: true } : {}),
140
+ attrs: { name: "intent", value: "create", ...(missing.length > 0 ? { "data-without-tools": "" } : {}) } });
141
+ const actions = html `${step}${acts(creates ? createButton(true) : "", btn(creates ? "Update preview" : "Preview", { variant: creates ? "outline" : "primary", type: "submit", attrs: { name: "intent", value: "preview" } }))}${creates ? note(anyone ? "Creating it asks for your password: it brings in items from anyone." : "Creating it needs no password.") : ""}`;
142
+ const form = postForm(`/flows/new/${template.id}`, html `${project}${template.asks.map(ask)}${field({ id: "fl-use-name", label: "Name", control: kInput({ id: "fl-use-name", name: "name", value: input.name, attrs: { maxlength: 80 } }) })}${sendResult}${preview}${actions}`, { attrs: { "data-gallery-use": true, id: "fl-use-form" }, hidden: { previewed: input.preview?.digest ?? "" } });
143
+ const needs = needsWords(template);
144
+ const steps = input.diagram === null && !needs ? "" : html `<section class="k-sec" aria-label="Steps">${input.diagram !== null && stepStrip(input.diagram)}${needs ? note(needs) : ""}</section>`;
145
+ return html `<div class="fl-use" data-kit-page data-gallery-template="${template.id}">${phoneHeader(template.name, { back: "/flows/new" })}${doc(docHeader({ title: template.name, back: ["/flows/new", "New flow"], meta: template.promise, same: true }), input.problem !== null ? notice(input.problem) : "", input.said ? infoLine(input.said) : "", steps, toolsBlock, form)}\
146
+ ${creates ? dock(btn("Create flow", { variant: "primary", type: "submit", attrs: { form: "fl-use-form", name: "intent", value: "create" } })) : dock(btn("Preview", { variant: "primary", type: "submit", attrs: { form: "fl-use-form", name: "intent", value: "preview" } }))}</div>`;
115
147
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Settings → Flows: the starter flows for a project, each switched on with one yes. A row says what it's for,
3
- * what it will do and what it never does — the whole of what the yes agrees to — then one button. A starter
2
+ * Starter flows (New flow → Starter flows): a project's starter flows, each switched on with one yes. A row says what
3
+ * it's for, what it will do and what it never does (the whole of what the yes agrees to), then one button. A starter
4
4
  * that's on links to its flow; one that can't run here says why.
5
5
  */
6
6
  import type { StarterView } from "./flow-starters.js";
@@ -8,14 +8,8 @@ import { type Html } from "./html.js";
8
8
  export declare const STARTERS_CSS: string;
9
9
  export declare function startersHtml(input: {
10
10
  repo: string;
11
- projects: readonly {
12
- path: string;
13
- name: string;
14
- }[];
11
+ projectName: string;
15
12
  starters: readonly StarterView[];
16
- csrf: string;
17
13
  canSwitch: boolean;
18
14
  suggested: string | null;
19
- said: string | null;
20
- problem: string | null;
21
15
  }): Html;
@@ -1,32 +1,21 @@
1
1
  import { html, postForm } from "./html.js";
2
- export const STARTERS_CSS = `.starters{max-width:760px;min-width:0}.starters h2{margin:24px 0 4px;font-size:.9375rem}` +
3
- `.starter{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 16px;align-items:start;padding:14px 0;border-top:1px solid var(--so-line);min-width:0}` +
4
- `.starter:first-of-type{border-top:0}.starter>:not(.starter-action){grid-column:1}.starter h3{margin:0;font-size:.9375rem;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center}` +
5
- `.starter p,.starter ul{margin:2px 0 0;min-width:0;overflow-wrap:anywhere}.starter ul{padding-left:18px;color:var(--so-muted);font-size:.875rem;line-height:1.5}` +
6
- `.starter .starter-never{font-size:.875rem;font-weight:500}.starter-action{grid-column:2;grid-row:1 / span 4;align-self:center;margin:0}` +
7
- `.starter-action button,.starter-action .button-link{min-height:44px;white-space:nowrap}.starter .starter-action button{width:auto}` +
8
- `.starter .starter-action a.starter-open{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
9
- `.starter[data-suggested="true"]{margin:0 -12px;padding:14px 12px;border:1px solid var(--so-accent,var(--so-line));border-radius:10px}` +
10
- `.starter-on{display:inline-flex;align-items:center;gap:6px;font-size:.75rem;font-weight:500;padding:0 8px;border-radius:5px;line-height:20px;color:var(--so-success);background:var(--so-success-soft)}` +
11
- `.starter-on i{width:6px;height:6px;border-radius:50%;background:currentColor}.starters .starter-blocked{color:var(--so-muted);font-size:.875rem}` +
12
- `.starters form.starters-project{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin:0 0 8px}.starters form.starters-project label{display:grid;gap:4px}` +
13
- `@media (max-width:560px){.starter{grid-template-columns:minmax(0,1fr)}.starter .starter-action{grid-column:1;grid-row:auto;margin-top:10px}` +
14
- `.starter .starter-action button,.starter .starter-action .button-link{width:100%;justify-content:center}.starters select{font-size:16px}}`;
2
+ import { templateRow } from "./flow-gallery-ui.js";
3
+ import { btn, chip, group, note } from "./server/kit.js";
4
+ export const STARTERS_CSS = `.fl-starter-does{margin:2px 0 0;padding-left:18px;font-size:12.5px;line-height:1.5;color:var(--so-muted)}.fl-starter-never{font-weight:500;color:var(--so-ink)}` +
5
+ `.fl-trow[data-suggested="true"]{border-left:2px solid var(--so-ink);padding-left:12px}`;
15
6
  export function startersHtml(input) {
16
- const note = input.problem ? html `<p class="problem" role="alert">${input.problem}</p>` : input.said ? html `<p role="status">${input.said}</p>` : "";
17
- const selector = input.projects.length > 1
18
- ? html `<form class="starters-project" method="get" action="/settings/flows"><label>Project<select name="repo">${input.projects.map(one => html `<option value="${one.path}"${one.path === input.repo && html ` selected`}>${one.name}</option>`)}</select></label><button>Show</button></form>` : "";
19
7
  // The one asked about ("Do this every time…") comes first, marked.
20
8
  const ordered = [...input.starters].sort((a, b) => Number(b.id === input.suggested) - Number(a.id === input.suggested));
21
9
  const rows = ordered.map(one => {
22
- const action = one.on !== null ? html `<p class="starter-action"><a class="button-link starter-open" href="/flows/${one.on.flow}">Open flow</a></p>`
10
+ const action = one.on !== null ? btn("Open flow", { sm: true, href: `/flows/${one.on.flow}` })
23
11
  : one.blocked !== null || !input.canSwitch ? ""
24
- : postForm("/settings/flows/on", html `<button type="submit">Switch on</button>`, { attrs: { class: "starter-action" }, hidden: { repo: input.repo, starter: one.id } });
12
+ : postForm("/settings/flows/on", btn("Switch on", { sm: true, type: "submit", attrs: { "aria-label": `Switch on ${one.name}` } }), { hidden: { repo: input.repo, starter: one.id } });
25
13
  // What the yes agrees to, in full, before it; nothing to agree to once it's on or can't be.
26
- const agrees = one.on === null && one.blocked === null && html `<ul>${one.does.map(line => html `<li>${line}</li>`)}</ul><p class="starter-never">${one.never}</p>`;
27
- const head = html `<h3>${one.name}${one.on === null ? "" : html ` <span class="starter-on"><i aria-hidden="true"></i>On</span>`}</h3>`;
28
- return html `<div class="starter" id="starter-${one.id}" data-starter="${one.id}" data-suggested="${String(one.id === input.suggested && one.on === null)}">${head}<p>${one.summary}</p>${agrees}${one.on === null && one.blocked !== null && html `<p class="starter-blocked">${one.blocked}</p>`}${action}</div>`;
14
+ const line = one.on !== null ? chip("On", "done")
15
+ : one.blocked !== null ? html `<span>${one.blocked}</span>`
16
+ : html `<span><ul class="fl-starter-does">${one.does.map(words => html `<li>${words}</li>`)}</ul><span class="fl-starter-never">${one.never}</span></span>`;
17
+ return templateRow({ name: one.name, promise: one.summary, line, action, attrs: html ` id="starter-${one.id}" data-starter="${one.id}" data-suggested="${String(one.id === input.suggested && one.on === null)}"` });
29
18
  });
30
- const nobody = !input.canSwitch && input.starters.some(one => one.on === null) ? html `<p class="meta">An approver switches these on.</p>` : "";
31
- return html `<section class="starters">${note}${selector}<h2>Starter flows</h2>${rows}${nobody}<p class="meta"><a href="/flows">All flows</a></p></section>`;
19
+ const nobody = !input.canSwitch && input.starters.some(one => one.on === null) ? note("An approver switches these on.") : "";
20
+ return html `${group(`Starter flows for ${input.projectName}`)}<div data-group="starters">${rows}</div>${nobody}`;
32
21
  }
@@ -1,6 +1,26 @@
1
1
  /** The words a "runs on its own" grant signs (flow-unattended.ts), kept free of server imports so the flow page's
2
2
  * signing form says each limit exactly as the signature will. */
3
3
  export declare const dollarWords: (micro: number) => string;
4
+ /** The most runs a day a grant may allow. */
5
+ export declare const UNATTENDED_MAX_RUNS = 50;
6
+ /** The largest per-attempt budget a grant may name: $100. */
7
+ export declare const UNATTENDED_MAX_BUDGET_MICROUSD = 100000000;
8
+ /** A per-attempt budget as typed ("5", "$7.50", "" for no cap), the one way the server and the flow page read it. */
9
+ export declare function unattendedBudgetOf(typed: string): {
10
+ ok: true;
11
+ value: number | null;
12
+ } | {
13
+ ok: false;
14
+ message: string;
15
+ };
16
+ /** Runs a day as typed ("3", "" for no limit), the one way the server and the flow page read it. */
17
+ export declare function unattendedRunsOf(typed: string): {
18
+ ok: true;
19
+ value: number | null;
20
+ } | {
21
+ ok: false;
22
+ message: string;
23
+ };
4
24
  /** "Oct 19": the end's day, where space is short (the exact time goes beside it, on hover). */
5
25
  export declare const untilDayWords: (iso: string) => string;
6
26
  /** "Oct 19, 14:05 UTC": the expiry as every surface says it. */
@@ -1,6 +1,30 @@
1
1
  /** The words a "runs on its own" grant signs (flow-unattended.ts), kept free of server imports so the flow page's
2
2
  * signing form says each limit exactly as the signature will. */
3
3
  export const dollarWords = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
4
+ /** The most runs a day a grant may allow. */
5
+ export const UNATTENDED_MAX_RUNS = 50;
6
+ /** The largest per-attempt budget a grant may name: $100. */
7
+ export const UNATTENDED_MAX_BUDGET_MICROUSD = 100_000_000;
8
+ /** A per-attempt budget as typed ("5", "$7.50", "" for no cap), the one way the server and the flow page read it. */
9
+ export function unattendedBudgetOf(typed) {
10
+ const text = typed.trim().replace(/^\$/, "");
11
+ if (text === "")
12
+ return { ok: true, value: null };
13
+ const value = Number(text);
14
+ if (!/^\d+(\.\d{1,2})?$/.test(text) || value <= 0 || Math.round(value * 1_000_000) > UNATTENDED_MAX_BUDGET_MICROUSD)
15
+ return { ok: false, message: `Give the budget in dollars, up to ${dollarWords(UNATTENDED_MAX_BUDGET_MICROUSD)}, or leave it empty.` };
16
+ return { ok: true, value: Math.round(value * 1_000_000) };
17
+ }
18
+ /** Runs a day as typed ("3", "" for no limit), the one way the server and the flow page read it. */
19
+ export function unattendedRunsOf(typed) {
20
+ const text = typed.trim();
21
+ if (text === "")
22
+ return { ok: true, value: null };
23
+ const value = Number(text);
24
+ if (!/^\d+$/.test(text) || !Number.isInteger(value) || value < 1 || value > UNATTENDED_MAX_RUNS)
25
+ return { ok: false, message: `Give runs a day as 1 to ${UNATTENDED_MAX_RUNS}, or leave it empty.` };
26
+ return { ok: true, value };
27
+ }
4
28
  /** "Oct 19": the end's day, where space is short (the exact time goes beside it, on hover). */
5
29
  export const untilDayWords = (iso) => new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: "UTC" }).format(new Date(iso));
6
30
  /** "Oct 19, 14:05 UTC": the expiry as every surface says it. */
@@ -8,10 +8,8 @@ export declare const FLOW_ACCEPTANCE: {
8
8
  report: AcceptanceCriterion[];
9
9
  task: AcceptanceCriterion[];
10
10
  };
11
- /** The most runs a day a grant may allow. */
12
- export declare const UNATTENDED_MAX_RUNS = 50;
13
- /** The largest per-attempt budget a grant may name: $100. */
14
- export declare const UNATTENDED_MAX_BUDGET_MICROUSD = 100000000;
11
+ /** The limits a grant may name, and their one parser, live with its words (the flow page validates with them too). */
12
+ export { UNATTENDED_MAX_BUDGET_MICROUSD, UNATTENDED_MAX_RUNS } from "./flow-unattended-words.js";
15
13
  /** The code a script schedule runs, as signed: the script's version and content digest, and for one that runs a project
16
14
  * file, that file's path and, sorted by path, the digest of its bytes and of every project file it statically imports. */
17
15
  export type SignedScript = {
@@ -77,7 +75,7 @@ export type UnattendedGrant = {
77
75
  * revision signed, and neither project protected. Shared by every dispatch belt. */
78
76
  export declare const grantStandsSql: (digest: string, now: string, repo: string) => string;
79
77
  /** Why a grant ended, as its row and ledger line say it. A renewal's ledger line is the new signature's. */
80
- export type UnattendedEnd = "expired" | "revoked" | "renewed" | "schedule changed" | "script changed" | "project protected" | "access changed";
78
+ export type UnattendedEnd = "expired" | "revoked" | "renewed" | "flow changed" | "schedule changed" | "script changed" | "project protected" | "access changed";
81
79
  /**
82
80
  * The one road for what ends grants from outside this module (store.ts calls it): a flow's schedule added, paused,
83
81
  * resumed or removed; a project's approval rules protecting work; an account's access or standing changing. Every live
@@ -222,6 +220,12 @@ export type UnattendedView = {
222
220
  optional: string | null;
223
221
  }[];
224
222
  schedulesDigest: string;
223
+ /** No live grant, and the latest one was ended by a change (the flow, a script, a schedule, the project's rules or
224
+ * the signer's access) before it would have run out: why, and when, so the page says it no longer runs on its own. */
225
+ ended: {
226
+ why: string;
227
+ at: string;
228
+ } | null;
225
229
  };
226
230
  export declare function unattendedViewOf(store: Store, flow: FlowRow, viewer: {
227
231
  name: string;