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.
- package/CHANGELOG.md +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +4 -3
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -6
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +35 -190
- package/dist/server/render-pages.js +154 -563
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -4
- package/package.json +1 -1
package/dist/flow-gallery-ui.js
CHANGED
|
@@ -1,40 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* its promise, its steps in order, what it needs and "Use this"
|
|
4
|
-
*
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
`.
|
|
14
|
-
`.
|
|
15
|
-
|
|
16
|
-
`.
|
|
17
|
-
|
|
18
|
-
`.gallery-steps
|
|
19
|
-
`.gallery-
|
|
20
|
-
|
|
21
|
-
`.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`.
|
|
25
|
-
`.
|
|
26
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
/** A tool's state in words, as Settings → Integrations
|
|
64
|
-
const STATE = { connected:
|
|
65
|
-
const toolState = (tool) => tool.state === null ? "" :
|
|
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)}<
|
|
69
|
-
/**
|
|
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
|
|
68
|
+
const rowOf = (template) => {
|
|
73
69
|
const definition = galleryDiagram(template);
|
|
74
|
-
|
|
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
|
|
77
|
-
return html
|
|
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,
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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="
|
|
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="
|
|
98
|
-
const
|
|
99
|
-
const value = input.answers[
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
103
|
-
return
|
|
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
|
-
?
|
|
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="
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
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
|
}
|
package/dist/flow-live.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* can't keep one open falls back to reading every few seconds. With nothing
|
|
17
17
|
* written, an open flow costs no reads; a 30-second check heals a miss.
|
|
18
18
|
*/
|
|
19
|
+
import { browserViewOf } from "./flow-browser.js";
|
|
19
20
|
import { createHash } from "node:crypto";
|
|
20
21
|
import { createLiveRooms } from "./live-bus.js";
|
|
21
22
|
// Statements belong to their database, without keeping closed Stores alive.
|
|
@@ -90,7 +91,14 @@ export function flowFingerprint(store, flow, now = new Date()) {
|
|
|
90
91
|
// including a build in another project. The page read admits the viewer;
|
|
91
92
|
// this stream only carries a hash. No evidence files or logs are read.
|
|
92
93
|
const row = fingerprintStatement(store).get({ $flow: flow, $now: now.toISOString() });
|
|
93
|
-
|
|
94
|
+
if (row === undefined)
|
|
95
|
+
return null;
|
|
96
|
+
// Browser use (flow-browser.ts): an approval, a revocation, a changed covered script or a signer who lost standing
|
|
97
|
+
// changes what the page says, so it is part of what open pages are nudged on.
|
|
98
|
+
const saved = store.getFlow(flow);
|
|
99
|
+
const browser = saved === null ? null : browserViewOf(store, saved);
|
|
100
|
+
const browserKey = browser === null ? "" : `${browser.status}:${browser.digest ?? ""}:${browser.signedAt ?? ""}:${browser.problem === null ? "" : "x"}`;
|
|
101
|
+
return createHash("sha256").update([...Object.values(row).map(String), browserKey].join("|")).digest("hex").slice(0, 16);
|
|
94
102
|
}
|
|
95
103
|
/** Rooms keyed by flow. With a bus, a write reaches every open page at once; without one, only the safety net. */
|
|
96
104
|
export function createFlowRooms(fingerprint, options = {}) {
|
|
@@ -19,6 +19,7 @@ import { adoptPublishedPullRequest } from "./publish.js";
|
|
|
19
19
|
import { failingCheckOf, mergePullRequest, owePullRequest, pullRequestBlocker, pullRequestFollowOf } from "./pull-request-flow.js";
|
|
20
20
|
import { requestResultChanges } from "./result-actions.js";
|
|
21
21
|
import { revisionSourceOf } from "./result-review.js";
|
|
22
|
+
import { cardTaskArchived } from "./task-archive.js";
|
|
22
23
|
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
23
24
|
* The MOST RECENT decision governs (approval policy §1.7): it must be a person's recorded decision, after the card's
|
|
24
25
|
* last build by the current definition AND after the exact build being merged finished (`builtAt`), so editing the
|
|
@@ -41,19 +42,23 @@ export function approvedSinceBuild(store, definition, card, builtAt = null) {
|
|
|
41
42
|
/** One pass of a Pull request zone for one card. Returns whether it changed anything. */
|
|
42
43
|
export async function pullRequestStep(store, flow, definition, stage, card, now, io) {
|
|
43
44
|
const titleOf = (id) => definition.stages.find(one => one.id === id)?.title ?? "another zone";
|
|
44
|
-
|
|
45
|
+
// Every write after GitHub answers re-proves, in its own transaction, that the card's work wasn't put away meanwhile
|
|
46
|
+
// (v145): a put away that committed first leaves the card where it is, saying nothing to anyone.
|
|
47
|
+
const unlessHeld = (write) => store.transact(() => cardTaskArchived(store, card.id) ? false : write());
|
|
48
|
+
const waiting = (said) => unlessHeld(() => {
|
|
45
49
|
if (card.waiting === said)
|
|
46
50
|
return false;
|
|
47
51
|
store.updateFlowCard(card.id, { waiting: said }, now);
|
|
48
52
|
return true;
|
|
49
|
-
};
|
|
50
|
-
const passed = (said, output) => {
|
|
53
|
+
});
|
|
54
|
+
const passed = (said, output) => unlessHeld(() => {
|
|
51
55
|
store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: output }, waiting: null }, now);
|
|
52
56
|
if (stage.next === null)
|
|
53
57
|
return waiting(`${said} Move the card on when you're ready.`);
|
|
54
58
|
return store.moveFlowCard(card.id, { to: stage.next, outcome: "ok", actor: "flow", historyNote: said, expectEntry: card.entry }, now);
|
|
55
|
-
};
|
|
56
|
-
const failed = (said, revision = null) =>
|
|
59
|
+
});
|
|
60
|
+
const failed = (said, revision = null) => unlessHeld(() => failedNow(said, revision));
|
|
61
|
+
const failedNow = (said, revision = null) => {
|
|
57
62
|
store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: said } }, now);
|
|
58
63
|
if (stage.onFail === null) {
|
|
59
64
|
if (!waiting(`${said} Fix it, then move the card to try again.`))
|
|
@@ -68,13 +73,13 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
|
|
|
68
73
|
};
|
|
69
74
|
// Publishing that can't proceed is not the build's fault: rebuilding wouldn't change it, so the card waits here,
|
|
70
75
|
// saying what to do, and the people on it hear once. It carries on by itself once the cause is fixed.
|
|
71
|
-
const stuck = (said) => {
|
|
76
|
+
const stuck = (said) => unlessHeld(() => {
|
|
72
77
|
if (!waiting(said))
|
|
73
78
|
return false;
|
|
74
79
|
const people = card.owner === null ? cardFollowers(store, card) : [card.owner];
|
|
75
80
|
notifyPeople(store, card, people, null, { key: `pr-stuck:${card.entry}`, subject: `${stage.title} is waiting on you for “${card.title}”`, attention: true, body: said }, now);
|
|
76
81
|
return true;
|
|
77
|
-
};
|
|
82
|
+
});
|
|
78
83
|
// With no failure path, a card that didn't pass waits for a person to move it (a move is a fresh visit).
|
|
79
84
|
if (stage.onFail === null && card.waiting !== null && card.waiting.endsWith(" Fix it, then move the card to try again."))
|
|
80
85
|
return false;
|
|
@@ -128,6 +133,9 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
|
|
|
128
133
|
// Back to a Build zone: a revision of the same work, carrying the failure (fenced as untrusted CI output).
|
|
129
134
|
let revision = null;
|
|
130
135
|
const target = definition.stages.find(one => one.id === stage.onFail);
|
|
136
|
+
// No revision of work put away while GitHub answered (v145).
|
|
137
|
+
if (cardTaskArchived(store, card.id))
|
|
138
|
+
return false;
|
|
131
139
|
if (target?.kind === "task" && io.evidenceRoot !== undefined) {
|
|
132
140
|
const runTask = store.externalIdFor(store.getRun(runId)?.taskRef ?? -1);
|
|
133
141
|
const revised = runTask === null ? null : requestResultChanges(store, io.evidenceRoot, {
|
package/dist/flow-share.js
CHANGED
|
@@ -54,6 +54,8 @@ function stepOf(stage, ask) {
|
|
|
54
54
|
step["runIn"] = stage.runIn;
|
|
55
55
|
if (stage.secrets !== undefined)
|
|
56
56
|
step["secrets"] = stage.secrets;
|
|
57
|
+
if (stage.browser === true)
|
|
58
|
+
step["browser"] = true;
|
|
57
59
|
}
|
|
58
60
|
if (stage.routes !== undefined)
|
|
59
61
|
step["routes"] = stage.routes.map(one => ({ answer: one.answer, goesTo: one.to }));
|
|
@@ -299,7 +301,7 @@ function stageInputOf(step, index, find) {
|
|
|
299
301
|
...(toOwner ? { toOwner: true } : { approver: anyone ? null : decider }),
|
|
300
302
|
message: step["message"], close: step["close"], script: step["script"], runIn: step["runIn"],
|
|
301
303
|
routes: list("routes")?.map((one, n) => ({ answer: one["answer"], to: find(one["goesTo"], `${at}.routes[${n}].goesTo`) })),
|
|
302
|
-
secrets: step["secrets"],
|
|
304
|
+
secrets: step["secrets"], browser: step["browser"],
|
|
303
305
|
...(kind === "sort" ? { sort: { question: step["question"], answers: (list("answers") ?? []).map((one, n) => ({ answer: one["answer"], means: one["means"], to: find(one["goesTo"], `${at}.answers[${n}].goesTo`) })),
|
|
304
306
|
sureAt: typeof sureAt === "number" && sureAt > 1 ? sureAt / 100 : sureAt, notes: list("alsoNote") ?? [] } } : {}),
|
|
305
307
|
...(kind === "request" ? { request: { method: step["method"], url: step["url"], headers: object("headers") ?? {}, body: step["body"] } } : {}),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* what it will do and what it never does
|
|
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
|
-
|
|
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;
|
package/dist/flow-starters-ui.js
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
import { html, postForm } from "./html.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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 ?
|
|
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",
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
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) ?
|
|
31
|
-
return html
|
|
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
|
}
|
package/dist/flow-steps.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Runner } from "./backend.js";
|
|
2
2
|
import { type DraftRunner } from "./flow-draft.js";
|
|
3
3
|
import { type MailSender, type ToolCaller } from "./flow-actions.js";
|
|
4
|
-
import type { Store } from "./store.js";
|
|
4
|
+
import type { FlowCardRow, FlowStepKind, Store } from "./store.js";
|
|
5
5
|
import type { TurnRunner } from "./subagents.js";
|
|
6
6
|
import type { ToolLister } from "./subagent-tools.js";
|
|
7
7
|
export type StepIo = {
|
|
@@ -42,3 +42,11 @@ export type StepPass = {
|
|
|
42
42
|
};
|
|
43
43
|
/** Run every due check and update in one project, then move each card on. */
|
|
44
44
|
export declare function runFlowSteps(store: Store, repo: string, now: Date, io: StepIo): Promise<StepPass>;
|
|
45
|
+
/** Claim this card's visit to its step zone, only while the card is still there on that visit: a pass that read the card
|
|
46
|
+
* before another pass (or a person) moved it never runs the step for a visit that's over. */
|
|
47
|
+
export declare function claimStep(store: Store, card: FlowCardRow, step: {
|
|
48
|
+
stage: string;
|
|
49
|
+
kind: FlowStepKind;
|
|
50
|
+
script: string | null;
|
|
51
|
+
scriptVersion: number | null;
|
|
52
|
+
}, now: Date): boolean;
|
package/dist/flow-steps.js
CHANGED
|
@@ -36,7 +36,9 @@ import { claudeDraftRunner, DRAFT_CHARS, DRAFT_TIMEOUT, draftPrompt, wholeDraft
|
|
|
36
36
|
import { overruns, passOn, TEXT_LIMITS, writeWithin } from "./text-limits.js";
|
|
37
37
|
import { readFlowSecrets, sendingReady, runRequest, sendEmail, toolWaiting, useTool } from "./flow-actions.js";
|
|
38
38
|
import { cleanFolder, flowLaunch, runCode } from "./flow-code.js";
|
|
39
|
+
import { browserStanding } from "./flow-browser.js";
|
|
39
40
|
import { readProviderKey } from "./keys.js";
|
|
41
|
+
import { cardTaskArchived } from "./task-archive.js";
|
|
40
42
|
import { replyInChannel } from "./chat-inbox.js";
|
|
41
43
|
import { recordSent, threadOf } from "./flow-replies.js";
|
|
42
44
|
import { ASKED, subagentReady, subagentTurn } from "./subagent-work.js";
|
|
@@ -70,6 +72,9 @@ export async function runFlowSteps(store, repo, now, io) {
|
|
|
70
72
|
const stage = known.definition?.stages.find(one => one.id === card.stage);
|
|
71
73
|
if (stage === undefined)
|
|
72
74
|
continue;
|
|
75
|
+
// A card whose task is put away (v145) runs nothing — no step, subagent turn or pull request — until it's restored.
|
|
76
|
+
if (cardTaskArchived(store, card.id))
|
|
77
|
+
continue;
|
|
73
78
|
// v92: a subagent's turn — a zone it handles, or a decision it staffs (a person decides when it's paused or gone).
|
|
74
79
|
if (stage.kind === "subagent" || (stage.kind === "approval" && stage.subagent !== undefined)) {
|
|
75
80
|
await subagentStep(store, known.flow, known.definition, stage, card, now, io, pass);
|
|
@@ -145,6 +150,14 @@ export async function runFlowSteps(store, repo, now, io) {
|
|
|
145
150
|
store.updateFlowCard(card.id, { waiting }, now);
|
|
146
151
|
continue;
|
|
147
152
|
}
|
|
153
|
+
// A script that starts a browser waits, saying so, until a person approves browser use for the flow (flow-browser.ts),
|
|
154
|
+
// rather than run and crash inside the sandbox; it runs on its own once approved.
|
|
155
|
+
const browser = stage.kind === "check" && stage.browser === true ? (script === null || known.definition === null ? { allowed: false, said: "This zone's script can't be read." } : browserStanding(store, known.flow.id, stage.id, { definition: known.definition, script })) : null;
|
|
156
|
+
if (browser !== null && !browser.allowed) {
|
|
157
|
+
if (card.waiting !== browser.said)
|
|
158
|
+
store.updateFlowCard(card.id, { waiting: browser.said }, now);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
148
161
|
if (!claimStep(store, card, { stage: stage.id, kind: stage.kind, script: script?.name ?? null, scriptVersion: script?.version ?? null }, now))
|
|
149
162
|
continue;
|
|
150
163
|
pass.ran++;
|
|
@@ -174,12 +187,21 @@ export async function runFlowSteps(store, repo, now, io) {
|
|
|
174
187
|
}
|
|
175
188
|
/** Claim this card's visit to its step zone, only while the card is still there on that visit: a pass that read the card
|
|
176
189
|
* before another pass (or a person) moved it never runs the step for a visit that's over. */
|
|
177
|
-
function claimStep(store, card, step, now) {
|
|
190
|
+
export function claimStep(store, card, step, now) {
|
|
178
191
|
return store.transact(() => {
|
|
179
192
|
const fresh = store.getFlowCard(card.id);
|
|
180
193
|
if (fresh === null || fresh.state !== "active" || fresh.entry !== card.entry || fresh.stage !== step.stage)
|
|
181
194
|
return false;
|
|
182
|
-
|
|
195
|
+
// A card whose task is put away (v145) runs no step until the task is restored (both of its tasks, re-read here).
|
|
196
|
+
if (cardTaskArchived(store, card.id))
|
|
197
|
+
return false;
|
|
198
|
+
if (!store.claimFlowStep({ card: card.id, entry: card.entry, ...step }, now))
|
|
199
|
+
return false;
|
|
200
|
+
// The tasks this step was admitted for stay held while it runs, wherever the card moves meanwhile.
|
|
201
|
+
for (const task of [fresh.task, fresh.primaryTask])
|
|
202
|
+
if (task !== null)
|
|
203
|
+
store.handle.prepare("INSERT OR IGNORE INTO flow_step_task (card, entry, task_id) VALUES (?, ?, ?)").run(card.id, card.entry, task);
|
|
204
|
+
return true;
|
|
183
205
|
});
|
|
184
206
|
}
|
|
185
207
|
/** One subagent turn, when one is due for this card: claimed like any step, retried on failure, and waiting while its question is open. */
|
|
@@ -361,16 +383,31 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
361
383
|
if (added.code !== 0)
|
|
362
384
|
return { state: "retry", said: `Couldn't make a copy of${where}: ${added.stderr.trim().split("\n")[0]?.slice(0, 160) ?? "git refused"}.` };
|
|
363
385
|
}
|
|
364
|
-
|
|
386
|
+
// Browser use, proved at the moment of launch: a zone that says it starts a browser runs only with a person's standing
|
|
387
|
+
// approval (flow-browser.ts); then its script, and only its script, gets Chromium's one service.
|
|
388
|
+
const wantsBrowser = stage.browser === true;
|
|
389
|
+
// Read once: the drawing and the script this launch runs are the very objects the browser decision is about.
|
|
390
|
+
const running = flowDefinitionOf(flow);
|
|
391
|
+
const standing = !wantsBrowser ? null : running === null || running.stages.find(one => one.id === stage.id) === undefined || JSON.stringify(running.stages.find(one => one.id === stage.id)) !== JSON.stringify(stage)
|
|
392
|
+
? { allowed: false, said: "This zone changed while it was starting, so it didn't run." } : browserStanding(store, flow.id, stage.id, { definition: running, script });
|
|
393
|
+
if (standing !== null && !standing.allowed) {
|
|
394
|
+
if (copy)
|
|
395
|
+
await io.git("git", ["-C", flow.repo, "worktree", "remove", "--force", path], { timeoutMs: 60_000 }).catch(() => undefined);
|
|
396
|
+
rmSync(path, { recursive: true, force: true });
|
|
397
|
+
return { state: "failed", said: standing.said };
|
|
398
|
+
}
|
|
399
|
+
const launch = flowLaunch(store, { repo: flow.repo, folder: path, scratch: io.scratch, copy, policy: store.flowFencePolicy(flow.id), ...(standing?.allowed === true ? { browser: true } : {}), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }), ...(io.home === undefined ? {} : { home: io.home }) });
|
|
365
400
|
try {
|
|
366
401
|
if (launch !== null && "problem" in launch)
|
|
367
402
|
return { state: "failed", said: launch.problem };
|
|
403
|
+
// The project's setup never gets the browser rule: only the zone's script asked for it.
|
|
404
|
+
const setupFence = launch?.fence === undefined ? undefined : { ...launch.fence, fenceGrants: withoutBrowser(launch.fence.fenceGrants) };
|
|
368
405
|
let log = "";
|
|
369
406
|
const setup = copy ? store.liveWorktreeSetup(flow.repo) : null;
|
|
370
407
|
if (setup !== null) {
|
|
371
408
|
const shell = approvedCommandShell(setup.command);
|
|
372
409
|
const prepared = await io.shell(shell.file, shell.args, { cwd: path, timeoutMs: setup.timeoutMs, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST, processGroup: true,
|
|
373
|
-
...(launch === null ? {} : { env: launch.env, ...(
|
|
410
|
+
...(launch === null ? {} : { env: launch.env, ...(setupFence ?? {}) }) });
|
|
374
411
|
log += `$ ${setup.command}\n${prepared.stdout}${prepared.stderr}\n`;
|
|
375
412
|
if (prepared.code !== 0)
|
|
376
413
|
return { state: "failed", said: `The project's setup failed before ${script.name} ran (exit ${prepared.code}).\n${tail(`${prepared.stdout}\n${prepared.stderr}`)}`, log, exitCode: prepared.code };
|
|
@@ -385,7 +422,15 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
385
422
|
answers: (stage.routes ?? []).map(one => one.answer),
|
|
386
423
|
};
|
|
387
424
|
const env = { FLOW_NAME: flow.name, FLOW_CARD_ID: String(card.id), FLOW_CARD_TITLE: card.title, FLOW_COMMIT: commit ?? "", FLOW_SCRIPT: script.name, FLOW_PROJECT: flow.repo };
|
|
388
|
-
|
|
425
|
+
// Checked again right before the script: the flow or its script may have changed while the setup ran.
|
|
426
|
+
if (wantsBrowser) {
|
|
427
|
+
const still = running === null ? { allowed: false, said: "This flow's drawing can't be read." } : browserStanding(store, flow.id, stage.id, { definition: running, script });
|
|
428
|
+
if (!still.allowed)
|
|
429
|
+
return { state: "failed", said: still.said, log };
|
|
430
|
+
}
|
|
431
|
+
// And at every spawn attempt (a retry after a failed spawn too): revoked or changed meanwhile, nothing starts.
|
|
432
|
+
const stillApproved = !wantsBrowser || running === null ? undefined : () => browserStanding(store, flow.id, stage.id, { definition: running, script }).allowed;
|
|
433
|
+
const ran = await runCode({ script, cwd: path, root: copy ? path : flow.repo, input, env, secrets, scratch: io.scratch, shell: io.shell, launch, ...(stillApproved === undefined ? {} : { beforeSpawn: stillApproved }) });
|
|
389
434
|
log += ran.log;
|
|
390
435
|
const output = ran.output === "" ? undefined : ran.output;
|
|
391
436
|
// Where it ran goes at the end of the first line: "run-tests failed (exit 1) on main."
|
|
@@ -407,6 +452,11 @@ async function runCheck(store, flow, stage, script, card, now, io) {
|
|
|
407
452
|
rmSync(path, { recursive: true, force: true });
|
|
408
453
|
}
|
|
409
454
|
}
|
|
455
|
+
/** A fence's grants without Chromium's service (a flow's setup, beside a script zone that has it). */
|
|
456
|
+
function withoutBrowser(grants) {
|
|
457
|
+
const { browser: _scriptOnly, ...rest } = grants;
|
|
458
|
+
return rest;
|
|
459
|
+
}
|
|
410
460
|
/** An update: a comment on the issue the card came from, and closing it if the zone says so. */
|
|
411
461
|
async function updateSource(store, stage, card, io, now) {
|
|
412
462
|
const source = card.source;
|