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/flows-ui.js
CHANGED
|
@@ -24,8 +24,11 @@ import { callWords, receiptWords } from "./subagent-tools.js";
|
|
|
24
24
|
import { undoFor } from "./subagent-week.js";
|
|
25
25
|
import { flowCardTaskLine } from "./flow-card-task.js";
|
|
26
26
|
import { unattendedViewOf } from "./flow-unattended.js";
|
|
27
|
+
import { browserViewOf } from "./flow-browser.js";
|
|
27
28
|
import { readFlowSecrets } from "./flow-secrets.js";
|
|
28
29
|
import { html, joinHtml, postForm } from "./html.js";
|
|
30
|
+
import { acts, btn, chip, count, disclose, doc, docHeader, dock, empty, field, group, infoLine, input as input_, note, notice, overlay, overlayTrigger, pageHeader, phoneHeader, row, segLinks, select, status } from "./server/kit.js";
|
|
31
|
+
import { agoWords } from "./lead-voice.js";
|
|
29
32
|
const projectName = (repo) => repo.split(/[\\/]/).filter(Boolean).pop() ?? repo;
|
|
30
33
|
/** v126: what happens to deliveries turned away (refused with 503), as far as each sender is known to say: nothing
|
|
31
34
|
* here claims a loss that a sender's own retries may still make good. */
|
|
@@ -61,40 +64,149 @@ function hookView(trigger, config, dir) {
|
|
|
61
64
|
return { ready, needsSecret: sender === "sentry" || sender === "vercel", sender, makesSecret: sender === "standard" || sender === "github-style", warning, readerNote: [readerNote, repeats].filter(one => one !== null).join(" ") || null, dropped,
|
|
62
65
|
scope: sender === "sentry" || sender === "vercel" ? { project: config.project ?? null, team: config.team ?? null } : null };
|
|
63
66
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
67
|
+
/** The flow page's fallback rules, plus the Flows list and the import preview (kit pieces; only layout here). */
|
|
68
|
+
export const FLOWS_CSS = `.flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}.flows fieldset{border:0;padding:0;margin:0;display:grid;gap:10px;min-width:0}.flows legend{font-weight:600;padding:0;margin-bottom:6px}.flows textarea[hidden]{display:none}.flows button.secondary{background:transparent;color:var(--foreground);border:1px solid var(--border)}@media(max-width:600px){.flows input,.flows select{font-size:16px}}@media(max-width:760px){.flows .card{padding:10px 12px;margin:8px 0;position:relative}}` +
|
|
69
|
+
// The list: the phone's project bar under the phone header; notices and the example in the page's gutter.
|
|
70
|
+
`.fl-pbar{display:flex;gap:8px;align-items:center;padding:10px 16px;border-bottom:1px solid var(--so-line)}.fl-pbar form{flex:1;display:flex;gap:8px;margin:0;min-width:0}.fl-pbar .k-selw{flex:1;min-width:0}` +
|
|
71
|
+
`.fl-say{padding:12px 24px 0}.fl-try{display:grid;gap:10px;justify-items:start;padding:0 24px 24px}.fl-try form{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0}` +
|
|
72
|
+
// The import drawer: the file chooser is a labelled outline button over the native input.
|
|
73
|
+
`.fl-imp{display:grid;gap:18px;margin:0}.fl-file{display:flex;align-items:center;gap:10px;min-width:0}.fl-file input[type=file]{position:absolute;width:1px;height:1px;opacity:0}.fl-file label{cursor:pointer}.fl-file label:focus-within{outline:2px solid var(--so-ring);outline-offset:2px}.fl-file .k-note{min-width:0;overflow-wrap:anywhere}` +
|
|
74
|
+
`.fl-imp textarea[hidden]{display:none}.fl-terms{list-style:none;padding:0;margin:0;display:grid;gap:8px;font-size:13px;line-height:1.5}.fl-terms li{white-space:pre-line;overflow-wrap:anywhere}` +
|
|
75
|
+
`.fl-terms pre{white-space:pre-wrap;overflow-wrap:anywhere;margin:6px 0 0}.fl-pv-form{display:grid;gap:16px;margin:0}.fl-pv-form fieldset{border:0;padding:0;margin:0;display:grid;gap:14px;min-width:0}.fl-pv-form legend{font:600 13px/1.3 var(--so-font-sans);padding:0;margin-bottom:10px}` +
|
|
76
|
+
`@media(max-width:760px){.fl-say{padding:12px 16px 0}.fl-try{padding:0 16px 24px}.fl-file label{min-height:44px}}`;
|
|
77
|
+
/** "in 12 min", "in 5 h", "in 3 days": when a trigger next looks. */
|
|
78
|
+
function nextWords(at, now) {
|
|
79
|
+
const minutes = Math.max(0, Math.round((Date.parse(at) - now.getTime()) / 60_000));
|
|
80
|
+
if (!Number.isFinite(minutes) || minutes < 1)
|
|
81
|
+
return "now";
|
|
82
|
+
if (minutes < 60)
|
|
83
|
+
return `in ${minutes} min`;
|
|
84
|
+
const hours = Math.round(minutes / 60);
|
|
85
|
+
return hours < 24 ? `in ${hours} h` : `in ${Math.round(hours / 24)} day${Math.round(hours / 24) === 1 ? "" : "s"}`;
|
|
86
|
+
}
|
|
87
|
+
/** What starts a flow, in a few words: its active triggers, else the paused ones, else that cards are added by hand. */
|
|
88
|
+
function triggerWords(store, triggers) {
|
|
89
|
+
const shown = triggers.filter(one => one.state === "active");
|
|
90
|
+
const pick = shown.length > 0 ? shown : triggers.filter(one => one.state === "paused");
|
|
91
|
+
const words = pick.flatMap(one => {
|
|
92
|
+
const config = triggerConfigOf(one);
|
|
93
|
+
if (config === null)
|
|
94
|
+
return [];
|
|
95
|
+
return [config.kind === "button" ? `When someone presses ${config.label}` : triggerHeadline(config, store).name];
|
|
74
96
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
97
|
+
if (words.length === 0)
|
|
98
|
+
return "Cards added by hand";
|
|
99
|
+
return words.length === 1 ? words[0] : `${words[0]} and ${words.length - 1} more`;
|
|
100
|
+
}
|
|
101
|
+
const FLOW_LIST_GROUPS = [["needs", "Needs you"], ["running", "Running"], ["on", "Turned on"], ["paused", "Paused"]];
|
|
102
|
+
/** One flow as a quiet row: what the store knows now (cards waiting on a decision, cards in progress, its triggers). */
|
|
103
|
+
function flowListRow(store, flow, now, options) {
|
|
104
|
+
const cards = store.flowCards(flow.id, false);
|
|
105
|
+
const definition = flowDefinitionOf(flow);
|
|
106
|
+
const kindOf = (card) => definition?.stages.find(one => one.id === card.stage)?.kind ?? "";
|
|
107
|
+
const waiting = cards.filter(card => ["approval", "choose"].includes(kindOf(card)));
|
|
108
|
+
const triggers = store.flowTriggers(flow.id).filter(one => one.state !== "removed");
|
|
109
|
+
const active = triggers.filter(one => one.state === "active");
|
|
110
|
+
const button = active.flatMap(one => { const config = triggerConfigOf(one); return config?.kind === "button" ? [{ id: one.id, label: config.label }] : []; })[0];
|
|
111
|
+
const trouble = troubleWords(flowInsights(store, flow, now, 7)) ?? nightlyHealthOf(store, flow);
|
|
112
|
+
const starts = triggerWords(store, triggers);
|
|
113
|
+
const next = active.map(one => one.nextAt).filter((one) => one !== null).sort()[0];
|
|
114
|
+
const meta = html `${options.showProject ? html `<span>${projectName(flow.repo)}</span>` : ""}${next !== undefined && active.length > 0 && waiting.length === 0 ? html `<span class="k-num">Next ${nextWords(next, now)}</span>` : ""}`;
|
|
115
|
+
const start = button !== undefined && options.canAct ? btn(button.label, { sm: true, href: `/flows/${flow.id}?start=${button.id}` }) : html ``;
|
|
116
|
+
const plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
|
|
117
|
+
const tail = trouble === null ? "" : ` · ${trouble}`;
|
|
118
|
+
if (waiting.length > 0) {
|
|
119
|
+
const first = waiting[0];
|
|
120
|
+
const ask = kindOf(first) === "choose" ? chip("Choose", "ask") : chip("Approve", "ask");
|
|
121
|
+
const said = waiting.length === 1 ? `${first.title} waits for your decision` : `${plural(waiting.length, "card")} wait for your decision`;
|
|
122
|
+
return { group: "needs", row: row({ href: `/flows/${flow.id}`, chip: ask, title: flow.name, desc: `${starts} · ${said}${tail}`, meta, wait: true,
|
|
123
|
+
action: btn("Review", { sm: true, href: `/flows/${flow.id}?card=${first.id}`, attrs: { "aria-label": `Review ${first.title}` } }), attrs: { "data-flow": flow.id } }) };
|
|
124
|
+
}
|
|
125
|
+
if (cards.length > 0) {
|
|
126
|
+
return { group: "running", row: row({ href: `/flows/${flow.id}`, chip: status("running"), title: flow.name, desc: `${starts} · ${plural(cards.length, "card")} in progress${tail}`, meta, action: start, attrs: { "data-flow": flow.id } }) };
|
|
127
|
+
}
|
|
128
|
+
if (active.length > 0) {
|
|
129
|
+
const failing = active.find(one => one.failures > 0);
|
|
130
|
+
const last = active.filter(one => one.lastAt !== null).sort((a, b) => (b.lastAt ?? "").localeCompare(a.lastAt ?? ""))[0];
|
|
131
|
+
const chipNow = failing !== undefined || trouble !== null ? status("failed") : last === undefined ? chip("Not run yet", "", { dot: false }) : status("finished");
|
|
132
|
+
const lastWords = failing !== undefined ? `last check failed ${agoWords(failing.lastAt ?? failing.updatedAt, now)}${failing.lastOutcome ? `: ${failing.lastOutcome}` : ""}`
|
|
133
|
+
: last === undefined ? "not run yet" : `last ran ${agoWords(last.lastAt, now)}${last.lastOutcome ? `: ${last.lastOutcome}` : ""}`;
|
|
134
|
+
return { group: "on", row: row({ href: `/flows/${flow.id}`, chip: chipNow, title: flow.name, desc: `${starts} · ${lastWords}${tail}`, meta, action: start, attrs: { "data-flow": flow.id } }) };
|
|
135
|
+
}
|
|
136
|
+
const paused = triggers.length > 0;
|
|
137
|
+
return { group: "paused", row: row({ href: `/flows/${flow.id}`, chip: chip(paused ? "Paused" : "Off", "", { dot: false }), title: flow.name,
|
|
138
|
+
desc: paused ? `${starts} · paused${tail}` : `${starts} · nothing starts it on its own${tail}`, meta, attrs: { "data-flow": flow.id } }) };
|
|
139
|
+
}
|
|
140
|
+
/** The project field of a list form: a select when there's a choice. */
|
|
141
|
+
const projectSelect = (id, projects, current) => projects.length === 1
|
|
142
|
+
? html `<input type="hidden" name="repo" value="${projects[0]}">`
|
|
143
|
+
: field({ id, label: "Project", control: select({ id, name: "repo", items: projects.map(repo => [repo, projectName(repo)]), current: current ?? projects[0], wide: true }) });
|
|
144
|
+
/** The import form: a flow file chosen here or fetched from a gist, previewed in plain words before anything is made. */
|
|
145
|
+
function importFormHtml(projects, current) {
|
|
146
|
+
return postForm("/flows/import", html `${note("A flow file from another Toolroll, a teammate or a gist. Nothing runs until you import it, and any script it brings waits for your approval.")}\
|
|
147
|
+
${projectSelect("fl-imp-project", projects, current)}\
|
|
148
|
+
<div class="k-field"><span class="k-label">File</span><div class="fl-file"><label class="k-btn k-btn-outline k-btn-sm"><input type="file" accept=".json,application/json" data-flow-file><span class="k-face">Choose a file</span></label><span class="k-note" data-flow-file-name>No file chosen</span></div></div>\
|
|
149
|
+
<textarea name="document" hidden data-flow-document></textarea>\
|
|
150
|
+
${field({ id: "fl-imp-url", label: "Or its address", control: input_({ id: "fl-imp-url", name: "url", type: "url", attrs: { inputmode: "url", placeholder: "https://gist.github.com/…" } }) })}\
|
|
151
|
+
<p class="k-notice" role="alert" data-flow-file-note hidden></p>${acts(btn("Preview import", { type: "submit" }))}`, { attrs: { class: "fl-imp", "data-flow-import": true } });
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Flows: the top bar (a project filter, More, New flow), then each flow as a quiet row under Needs you, Running,
|
|
155
|
+
* Turned on and Paused. Words come only from what the store knows now: cards waiting on a decision, cards in
|
|
156
|
+
* progress, and the triggers (on, paused, when they last looked and what they found).
|
|
157
|
+
*/
|
|
158
|
+
export function flowsListHtml(store, flows, projects, canCreate, problem, filter = null, now = new Date()) {
|
|
159
|
+
const scope = filter !== null && projects.includes(filter) ? filter : null;
|
|
160
|
+
const shown = scope === null ? flows : flows.filter(one => one.repo === scope);
|
|
161
|
+
const making = canCreate && projects.length > 0;
|
|
162
|
+
const href = (repo) => repo === null ? "/flows" : `/flows?project=${encodeURIComponent(repo)}`;
|
|
163
|
+
const seg = projects.length > 1 ? segLinks("Project", scope ?? "all", [{ key: "all", label: "All", href: "/flows" }, ...projects.map(repo => ({ key: repo, label: projectName(repo), href: href(repo) }))]) : html ``;
|
|
164
|
+
const more = (id, phone) => overlayTrigger("More", "fl-more", { variant: phone ? "outline" : "ghost", sm: !phone, iconOnly: "more", ...(id === "" ? {} : { attrs: { id } }) });
|
|
165
|
+
const create = making ? btn("New flow", { variant: "primary", sm: true, lead: "plus", href: "/flows/new" }) : html ``;
|
|
166
|
+
const header = pageHeader("Flows", { right: html `${seg}${more("", false)}${create}` });
|
|
167
|
+
// On a phone the filter is a select (the form posts nothing: it's an address), beside More.
|
|
168
|
+
const phoneBar = html `<div class="fl-pbar k-phone-only">${projects.length > 1 ? html `<form method="get" action="/flows">${select({ name: "project", label: "Project", items: [["", "All projects"], ...projects.map(repo => [repo, projectName(repo)])], current: scope ?? "", wide: true, attrs: { "data-k-autosubmit": "" } })}<noscript>${btn("Show", { sm: true, type: "submit" })}</noscript></form>` : html `<span class="k-grow"></span>`}${more("", true)}</div>`;
|
|
169
|
+
const menu = overlay({ id: "fl-more", kind: "menu", title: "More", body: html `${making ? html `<button type="button" data-k-open="fl-import" aria-haspopup="dialog" aria-controls="fl-import">Import a flow file…</button>` : ""}<a href="/flows/new?group=kits">Starter kits</a><a href="/settings/lead/subagents">Subagents</a>` });
|
|
170
|
+
const importDrawer = making ? overlay({ id: "fl-import", kind: "drawer", title: "Import a flow file", body: importFormHtml(projects, scope) }) : html ``;
|
|
171
|
+
const say = problem !== null ? html `<div class="fl-say">${notice(problem)}</div>` : html ``;
|
|
172
|
+
let body;
|
|
173
|
+
if (flows.length === 0) {
|
|
174
|
+
const example = making ? html `<div class="fl-try">${postForm("/flows/example", html `<span class="k-note">Or see one work: Claude drafts a reply to a sample customer question for you to approve.</span>${projects.length === 1 ? html `<input type="hidden" name="repo" value="${projects[0]}">` : select({ name: "repo", label: "Project", items: projects.map(repo => [repo, projectName(repo)]), sm: true })}${btn("Try an example", { sm: true, type: "submit" })}`)}</div>` : html ``;
|
|
175
|
+
const words = { title: "No flows yet.", text: "A flow repeats work for you: run your tests every night, turn labelled issues into pull requests, draft replies to support mail." };
|
|
176
|
+
body = html `${empty(making ? { ...words, action: acts(btn("Browse templates", { sm: true, href: "/flows/new" }), overlayTrigger("Import a flow file", "fl-import", { variant: "ghost", sm: true })) } : words)}${example}`;
|
|
177
|
+
}
|
|
178
|
+
else if (shown.length === 0) {
|
|
179
|
+
body = empty({ title: `No flows in ${projectName(scope)}.`, action: btn("Show all projects", { variant: "ghost", sm: true, href: "/flows" }) });
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const rows = shown.map(flow => flowListRow(store, flow, now, { canAct: canCreate, showProject: scope === null && projects.length > 1 }));
|
|
183
|
+
body = html `<div class="fl-rows" aria-label="Flows">${FLOW_LIST_GROUPS.map(([key, label]) => {
|
|
184
|
+
const mine = rows.filter(one => one.group === key);
|
|
185
|
+
return mine.length === 0 ? "" : html `${group(label, count(mine.length))}${mine.map(one => one.row)}`;
|
|
186
|
+
})}</div>`;
|
|
187
|
+
}
|
|
188
|
+
return html `<div class="fl-page" data-kit-page>${header}${phoneBar}${say}${body}${menu}${importDrawer}${making ? dock(btn("New flow", { variant: "primary", lead: "plus", href: "/flows/new" })) : ""}</div>`;
|
|
86
189
|
}
|
|
87
|
-
/** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made)
|
|
88
|
-
|
|
89
|
-
|
|
190
|
+
/** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made), and sends a
|
|
191
|
+
* filter select's form when it changes. */
|
|
192
|
+
export const FLOW_IMPORT_SCRIPT = `(function(){document.querySelectorAll('select[data-k-autosubmit]').forEach(function(s){s.addEventListener('change',function(){if(s.form)s.form.requestSubmit();});});var form=document.querySelector('[data-flow-import]');if(!form)return;var input=form.querySelector('[data-flow-file]'),text=form.querySelector('[data-flow-document]'),note=form.querySelector('[data-flow-file-note]'),named=form.querySelector('[data-flow-file-name]'),address=form.querySelector('[name=url]');function say(words){note.textContent=words;note.hidden=words==='';}var reading=null;input.addEventListener('change',function(){text.value='';say('');var file=input.files&&input.files[0];if(named)named.textContent=file?file.name:'No file chosen';if(!file){reading=null;return;}if(file.size>${FLOW_FILE_MAX_BYTES}){say('That file is too big: a flow file is at most ${FLOW_FILE_MAX_BYTES / 1024} KB.');input.value='';if(named)named.textContent='No file chosen';reading=null;return;}reading=file.text().then(function(value){text.value=value;});});form.addEventListener('submit',function(event){if(reading!==null&&text.value===''){event.preventDefault();reading.then(function(){form.requestSubmit();});return;}if(text.value===''&&address.value.trim()===''){event.preventDefault();say('Choose a flow file or give its address.');}});})();`;
|
|
193
|
+
/** An import's preview, a reading page: the plan in plain words, what it asks for, and one Import button after the terms. */
|
|
90
194
|
export function flowImportHtml(input) {
|
|
91
195
|
const { plan, file } = input;
|
|
92
|
-
const fields = file.parameters.map(one =>
|
|
93
|
-
|
|
196
|
+
const fields = file.parameters.map((one, index) => field({ id: `fl-param-${index}`, label: one.optional === true ? `${one.about} (optional)` : one.about,
|
|
197
|
+
control: input_({ id: `fl-param-${index}`, name: `param.${one.id}`, value: input.values[one.id] ?? plan?.values[one.id] ?? one.default ?? "", attrs: { maxlength: 200, autocomplete: "off" } }) }));
|
|
198
|
+
const list = (items) => items.length === 0 ? "" : html `<ul class="fl-terms">${items.map(term => html `<li>${term}</li>`)}</ul>`;
|
|
199
|
+
const section = (title, body) => html `<section class="k-sec"><h3>${title}</h3>${body}</section>`;
|
|
94
200
|
// The steps, what they mean, the triggers and the scripts; each script's code one tap away, as on the Scripts panel.
|
|
95
|
-
const terms = plan === null ? "" : html `${plan.parts.about !== null && html `<p
|
|
96
|
-
|
|
97
|
-
|
|
201
|
+
const terms = plan === null ? "" : html `${plan.parts.about !== null && html `<p class="k-note">${plan.parts.about}</p>`}\
|
|
202
|
+
${section("Steps", html `${infoLine("Its instructions come from the file, not from you: read what each step is asked before you import.")}${list(plan.parts.steps)}`)}\
|
|
203
|
+
${plan.parts.triggers.length > 0 && section("Triggers", list(plan.parts.triggers))}\
|
|
204
|
+
${plan.parts.scripts.length + plan.parts.kept.length > 0 && section("Scripts", html `<ul class="fl-terms">${plan.scripts.add.map((one, index) => html `<li>${plan.parts.scripts[index]}${one.body !== undefined && disclose("Show the script", html `<pre class="k-mono">${one.body}</pre>`)}</li>`)}${plan.parts.kept.map(one => html `<li>${one}</li>`)}</ul>`)}\
|
|
205
|
+
${section("Good to know", list([...plan.parts.notes, ...(plan.parts.needs === null ? [] : [plan.parts.needs])]))}`;
|
|
206
|
+
const importButton = plan !== null ? btn("Import flow", { variant: "primary", type: "submit", docked: true, attrs: { name: "confirm", value: "yes" } }) : html ``;
|
|
207
|
+
const form = postForm("/flows/import", html `<textarea name="document" hidden>${input.document}</textarea>${fields.length > 0 && html `<fieldset><legend>Fill in</legend>${fields}</fieldset>`}\
|
|
208
|
+
${acts(importButton, fields.length > 0 ? btn(plan === null ? "Preview" : "Update preview", { variant: plan === null ? "primary" : "outline", type: "submit", attrs: { name: "confirm", value: "" } }) : "", btn("Cancel", { variant: "ghost", href: "/flows" }))}`, { attrs: { class: "fl-pv-form", id: "fl-pv-form" }, hidden: { repo: input.repo, previewed: input.previewed } });
|
|
209
|
+
return html `<div class="fl-page" data-kit-page>${phoneHeader(`Import ${file.name}`, { back: "/flows" })}${doc(docHeader({ title: `Import ${file.name}`, back: ["/flows", "Flows"], meta: `Into ${projectName(input.repo)}. Nothing is made until you import it.`, same: true }), input.problem !== null ? notice(input.problem) : "", terms, form)}${plan !== null ? dock(btn("Import flow", { variant: "primary", type: "submit", attrs: { form: "fl-pv-form", name: "confirm", value: "yes" } })) : ""}</div>`;
|
|
98
210
|
}
|
|
99
211
|
const historyText = (event, title, sorts = new Set()) => {
|
|
100
212
|
const who = event.actor === "flow" ? "" : ` by ${event.actor}`;
|
|
@@ -114,6 +226,8 @@ const historyText = (event, title, sorts = new Set()) => {
|
|
|
114
226
|
export function flowView(store, flow, viewer, selectedCard, setup = { dir: null, repos: [] }) {
|
|
115
227
|
const definition = flowDefinitionOf(flow);
|
|
116
228
|
const stages = definition?.stages ?? [];
|
|
229
|
+
const browserNow = browserViewOf(store, flow);
|
|
230
|
+
const browser = browserNow === null ? null : { ...browserNow, canApprove: viewer.approver && store.accountOf(viewer.name)?.projects === null };
|
|
117
231
|
const title = (id) => stages.find(one => one.id === id)?.title ?? id;
|
|
118
232
|
const sortZones = new Set(stages.filter(one => one.kind === "sort").map(one => one.id));
|
|
119
233
|
const decisions = sortZones.size === 0 ? new Map() : store.flowSortDecisions(flow.id);
|
|
@@ -179,10 +293,15 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
179
293
|
// After research: the same numbered items the message carried, each beside its screenshot.
|
|
180
294
|
...(content.items === undefined ? {} : { items: content.items.map((item, index) => ({ number: index + 1, title: item.title, lines: whyLines(item.why), source: item.source, url: item.url,
|
|
181
295
|
image: item.shot === null || content.shots === null ? null : { src: `/r/${content.shots.run}/evidence/${item.shot}`, caption: itemCaption(index + 1, item) } })) }) });
|
|
296
|
+
const browserWait = card.state === "active" && stage?.kind === "check" && stage.browser === true && browser !== null && browser.status !== "allowed"
|
|
297
|
+
&& card.waiting !== null && card.waiting.startsWith(`${stage.title} starts a browser`);
|
|
182
298
|
return {
|
|
183
299
|
id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state,
|
|
184
300
|
waiting: card.state === "active" && card.task !== null && (stage?.kind === "task" || stage?.kind === "report")
|
|
185
|
-
? canReadTask ? flowCardTaskLine(store, card.task, taskRepo, now, secretsFor(taskRepo)) : "Task unavailable"
|
|
301
|
+
? canReadTask ? flowCardTaskLine(store, card.task, taskRepo, now, secretsFor(taskRepo)) : "Task unavailable"
|
|
302
|
+
// Waiting for browser use: the page offers Allow browser itself, so the command line the CLI shows is left off.
|
|
303
|
+
: browserWait ? card.waiting.replace(/ Approve it with: toolroll flows browser \d+ on$/, "") : card.waiting,
|
|
304
|
+
...(browserWait ? { browserWait: true } : {}),
|
|
186
305
|
task: task === null ? null : { id: task, href: `/t/${encodeURIComponent(task)}`,
|
|
187
306
|
// Its running attempt's composer, for an approver who may read the task (steer-controls.ts).
|
|
188
307
|
...(() => {
|
|
@@ -252,7 +371,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
252
371
|
requestSecrets: flowSecretNames(setup.dir, flow.repo),
|
|
253
372
|
tools: projectToolsOf(store, flow.repo).map(tool => ({ name: tool.name, about: tool.spec.about, functions: tool.lastTest?.ok === true ? tool.lastTest.tools : [],
|
|
254
373
|
ready: toolStanding(tool, secretsSetFor(flow.repo, tool.spec, setup.toolHome)).ready })),
|
|
255
|
-
scripts: store.flowScripts(flow.repo).map(script => ({ name: script.name, about: script.about, body: script.body, timeoutMinutes: script.timeoutMinutes, version: script.version, savedBy: script.savedBy, savedAt: script.savedAt,
|
|
374
|
+
scripts: store.flowScripts(flow.repo).map(script => ({ name: script.name, about: script.about, body: script.body, timeoutMinutes: script.timeoutMinutes, version: script.version, digest: script.digest, savedBy: script.savedBy, savedAt: script.savedAt,
|
|
256
375
|
language: script.language, file: script.file, held: script.held !== null,
|
|
257
376
|
usedHere: stages.filter(stage => stage.kind === "check" && stage.script === script.name).map(stage => stage.title) })),
|
|
258
377
|
start: definition?.start ?? stages[0]?.id ?? "",
|
|
@@ -262,6 +381,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
262
381
|
live: flowFingerprint(store, flow.id),
|
|
263
382
|
canEdit: viewer.approver,
|
|
264
383
|
unattended: unattendedViewOf(store, flow, viewer, setup.now ?? now),
|
|
384
|
+
browser,
|
|
265
385
|
approvers: store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, flow.repo)),
|
|
266
386
|
projects: [flow.repo, ...setup.repos.filter(one => one !== flow.repo)].map(path => ({ path, name: projectName(path) })),
|
|
267
387
|
kinds: FLOW_STAGE_KINDS.map(kind => ({ kind, label: FLOW_KIND_WORDS[kind].label, about: FLOW_KIND_WORDS[kind].about })),
|
|
@@ -271,7 +391,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
271
391
|
/** The page the canvas replaces when scripts can't run: each zone and its cards, in order. */
|
|
272
392
|
export function flowFallbackHtml(view) {
|
|
273
393
|
const own = view.unattended.active;
|
|
274
|
-
return html `<section class="flows flow-fallback"><p class="meta">${view.flow.project}</p>${own !== null && html `<p data-flow-unattended>Runs on its own until ${own.untilWords}</p>`}<ol>${view.stages.map(stage => {
|
|
394
|
+
return html `<section class="flows flow-fallback"><p class="meta">${view.flow.project}</p>${own !== null && html `<p data-flow-unattended>Runs on its own until ${own.untilWords}</p>`}${view.browser !== null && html `<p data-flow-browser="${view.browser.status}">${view.browser.words}</p>`}<ol>${view.stages.map(stage => {
|
|
275
395
|
const cards = view.cards.filter(card => card.stage === stage.id && card.state === "active");
|
|
276
396
|
return html `<li><strong>${stage.title}</strong> · ${FLOW_KIND_WORDS[stage.kind].label}${cards.length > 0 && html `<ul>${cards.map(card => html `<li>${card.title}${card.waiting !== null && ` — ${card.waiting}`}</li>`)}</ul>`}</li>`;
|
|
277
397
|
})}</ol></section>`;
|
package/dist/flows.js
CHANGED
|
@@ -224,7 +224,9 @@ function canonicalStage(input, index) {
|
|
|
224
224
|
close: kind === "update" ? stage["close"] !== false : null,
|
|
225
225
|
script: kind === "check" ? said(stage["script"]) : null,
|
|
226
226
|
// Left out, a script runs where every script ran before v90: in a copy of the card's work.
|
|
227
|
-
...(kind === "check" ? { ...(runIn === undefined ? {} : { runIn }), ...(routes === undefined ? {} : { routes }), ...(secrets === undefined ? {} : { secrets })
|
|
227
|
+
...(kind === "check" ? { ...(runIn === undefined ? {} : { runIn }), ...(routes === undefined ? {} : { routes }), ...(secrets === undefined ? {} : { secrets }),
|
|
228
|
+
// v144: a script that starts a browser says so; false or none is a script that doesn't (and keeps its digest).
|
|
229
|
+
...(!given(stage["browser"]) || stage["browser"] === false ? {} : { browser: stage["browser"] }) } : {}),
|
|
228
230
|
sort: kind === "sort" ? canonicalSort(stage["sort"]) : null,
|
|
229
231
|
...(kind === "request" ? { request: { method, url: said(request["url"]), headers: Object.fromEntries(Object.entries(isRecord(request["headers"]) ? request["headers"] : {}).flatMap(([name, value]) => { const one = said(value); return one === null ? [] : [[name, one]]; })), body: method === "GET" || method === "DELETE" ? null : said(request["body"]) } } : {}),
|
|
230
232
|
...(kind === "email" ? { email: { to: said(email["to"]), subject: said(email["subject"]), body: said(email["body"]) } } : {}),
|
|
@@ -732,6 +734,7 @@ export function flowFromSteps(input, previous = null) {
|
|
|
732
734
|
...(step.runIn !== undefined ? { runIn: step.runIn } : old?.runIn === undefined ? {} : { runIn: old.runIn }),
|
|
733
735
|
...(step.routes !== undefined ? routesOf(step.routes) : old?.routes === undefined ? {} : { routes: old.routes }),
|
|
734
736
|
...(step.secrets !== undefined ? step.secrets.length === 0 ? {} : { secrets: step.secrets } : old?.secrets === undefined ? {} : { secrets: old.secrets }),
|
|
737
|
+
...((step.browser ?? old?.browser) === true ? { browser: true } : {}),
|
|
735
738
|
} : {}),
|
|
736
739
|
sort: kind === "sort" ? sortFromStep(step, old?.sort ?? null, find) : null,
|
|
737
740
|
...(kind === "request" ? { request: { method: step.method ?? old?.request?.method ?? "POST", url: step.url ?? old?.request?.url ?? "", headers: step.headers ?? old?.request?.headers ?? {}, body: step.body ?? old?.request?.body ?? null } } : {}),
|
|
@@ -912,7 +915,7 @@ export function flowTerms(definition, previous) {
|
|
|
912
915
|
lines.push("Opens a pull request for the card's built result, then waits for CI.", `Checks pass → ${stage.merge === undefined ? to(stage.next) : `merges it (${stage.merge}) and deletes its branch, only when a person approved it${approval === undefined ? "" : ` at ${approval.title}`} since it was built. Then → ${to(stage.next)}`}`, `Checks fail → ${stage.onFail === null ? "waits there, naming the failing check." : `${to(stage.onFail)} The failing check is named on the card.`}`);
|
|
913
916
|
}
|
|
914
917
|
else if (stage.kind === "check")
|
|
915
|
-
lines.push(`Runs the project's script “${stage.script}” with no AI.`, `Passes → ${to(stage.next)}${stage.onFail === null ? " Fails → waits there." : ` Fails → ${to(stage.onFail)}`}`);
|
|
918
|
+
lines.push(`Runs the project's script “${stage.script}” with no AI.`, ...(stage.browser === true ? ["Starts a browser (Playwright's Chromium): it runs only once a person approves browser use for this flow."] : []), `Passes → ${to(stage.next)}${stage.onFail === null ? " Fails → waits there." : ` Fails → ${to(stage.onFail)}`}`);
|
|
916
919
|
else if (stage.kind === "wait" && stage.wait !== undefined)
|
|
917
920
|
lines.push(stage.wait.for === "hours" ? `Waits until it's between ${stage.wait.from} and ${stage.wait.to}${stage.wait.timeZone === undefined ? "" : ` (${stage.wait.timeZone})`}; cards that arrive then go straight on. Then → ${to(stage.next)}`
|
|
918
921
|
: stage.wait.for === "time" ? `Waits ${durationWords(stage.wait.minutes)}. Then → ${to(stage.next)}`
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settings → Integrations: which integrations work. One
|
|
2
|
+
* Settings → Integrations: which integrations work. One record each: its name
|
|
3
3
|
* and state, the account it's connected as, when it last worked and last
|
|
4
4
|
* failed, what uses it, and one action. The list comes from saved checks; a
|
|
5
|
-
* render never waits on one (see integrations.ts).
|
|
5
|
+
* render never waits on one (see integrations.ts). Built from the revamp kit;
|
|
6
|
+
* the Settings frame adds the title.
|
|
6
7
|
*/
|
|
7
8
|
import { type Html } from "./html.js";
|
|
8
9
|
import { type Integration, type SecretsKept } from "./integrations.js";
|
|
9
|
-
export
|
|
10
|
-
export declare
|
|
10
|
+
/** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
|
|
11
|
+
export declare const INTEGRATIONS_CSS = "";
|
|
12
|
+
export declare function integrationsHtml(list: readonly Integration[], csrf: string, said: {
|
|
11
13
|
said?: string | null;
|
|
12
14
|
problem?: string | null;
|
|
13
15
|
checking?: boolean;
|
package/dist/integrations-ui.js
CHANGED
|
@@ -1,46 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settings → Integrations: which integrations work. One
|
|
2
|
+
* Settings → Integrations: which integrations work. One record each: its name
|
|
3
3
|
* and state, the account it's connected as, when it last worked and last
|
|
4
4
|
* failed, what uses it, and one action. The list comes from saved checks; a
|
|
5
|
-
* render never waits on one (see integrations.ts).
|
|
5
|
+
* render never waits on one (see integrations.ts). Built from the revamp kit;
|
|
6
|
+
* the Settings frame adds the title.
|
|
6
7
|
*/
|
|
7
8
|
import { html, joinHtml, postForm } from "./html.js";
|
|
8
9
|
import { STATE_WORDS, secretsNote } from "./integrations.js";
|
|
9
10
|
import { whenUtc } from "./when-html.js";
|
|
10
11
|
import { brandMarkHtml } from "./brand-mark.js";
|
|
12
|
+
import { btn, chip, disclose, infoLine, note, notice, settingsRecord, settingsSection } from "./server/kit.js";
|
|
11
13
|
const when = (at) => whenUtc(at);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
`.integrations .integration>:not(.integration-action),.integrations .integration-body>*{grid-column:1}.integrations .integration-body{display:contents}.integrations .integration:first-of-type{border-top:0}.integrations .integration-head{margin:0;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;min-width:0}` +
|
|
15
|
-
`.integrations .integration .integration-body>*{margin-left:calc(var(--brand-mark) + 10px)}.integrations .integration-head strong{overflow-wrap:anywhere}.integrations .integration p{margin:2px 0 0;min-width:0;overflow-wrap:anywhere}` +
|
|
16
|
-
`.integrations .integration-action{grid-column:2;grid-row:1 / span 3;align-self:center;margin:0}.integrations .integration-action button,.integrations .integration-action .button-link{min-height:44px;white-space:nowrap}` +
|
|
17
|
-
`.integrations .integration .integration-action button{width:auto}.integrations .integration .integration-action a.integration-quiet,.integrations .integration .integration-action button.integration-quiet{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
|
|
18
|
-
`.integrations .integration-fix{color:var(--so-danger)}.integrations code{overflow-wrap:anywhere}.integrations details{margin-top:4px;font-size:.8125rem}.integrations summary{cursor:pointer;padding:4px 0}` +
|
|
19
|
-
`.integration-state{display:inline-flex;align-items:center;gap:6px;font-size:.75rem;font-weight:500;padding:0 8px;border-radius:5px;line-height:20px;white-space:nowrap}` +
|
|
20
|
-
`.integration-state i{width:6px;height:6px;border-radius:50%;background:currentColor}` +
|
|
21
|
-
`.integration-state--connected{color:var(--so-success);background:var(--so-success-soft)}.integration-state--broken{color:var(--so-danger);background:var(--so-danger-soft)}` +
|
|
22
|
-
`.integration-state--not-set-up,.integration-state--checking{color:var(--so-muted);background:var(--so-neutral-soft)}` +
|
|
23
|
-
// A phone: the action sits at the right of the name line, and the facts under it run on as one line.
|
|
24
|
-
`@media (max-width:760px){.integrations h2{margin:16px 0 0}.integrations .integration{gap:0 12px;padding:6px 0 8px}.integrations .integration-head{min-height:44px}` +
|
|
25
|
-
`.integrations .integration-action{grid-row:1;align-self:center}.integrations .integration-body{display:block;grid-column:1 / -1;line-height:1.35;padding-left:calc(var(--brand-mark) + 10px)}.integrations .integration .integration-body>*{margin-left:0}` +
|
|
26
|
-
`.integrations .integration-body>p{margin:0}.integrations .integration-body>p.meta{display:inline}.integrations .integration-body>p.meta+p.meta::before{content:" · "}` +
|
|
27
|
-
`.integrations .integration-body>:not(.meta){margin-top:4px}.integrations .integration-body>:not(.meta)+p.meta{display:block;margin-top:2px}.integrations details{margin-top:2px}}`;
|
|
14
|
+
/** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
|
|
15
|
+
export const INTEGRATIONS_CSS = "";
|
|
28
16
|
const GROUPS = [
|
|
29
17
|
["chat", "Chat"], ["code", "Code and issues"], ["mail", "Email"], ["tools", "MCP tools"], ["monitoring", "Monitoring"], ["agents", "Agents"],
|
|
30
18
|
];
|
|
31
19
|
function action(one, csrf) {
|
|
32
20
|
const a = one.action;
|
|
21
|
+
// One harmless read-only check of this integration, now (the handler sends nothing).
|
|
33
22
|
if (a.kind === "test") {
|
|
34
|
-
return csrf === "" ? "" : postForm("/settings/integrations/test",
|
|
23
|
+
return csrf === "" ? "" : postForm("/settings/integrations/test", btn("Check connection", { sm: true, type: "submit" }), { hidden: { key: one.key } });
|
|
35
24
|
}
|
|
36
25
|
// A project's youiyoux requests: one switch for that project.
|
|
37
26
|
if (a.kind === "switch") {
|
|
38
|
-
return csrf === "" ? "" : postForm("/settings/integrations/youiyoux",
|
|
27
|
+
return csrf === "" ? "" : postForm("/settings/integrations/youiyoux", btn(a.label, { sm: true, type: "submit" }), { hidden: { repo: a.repo, on: a.on ? "1" : "0" } });
|
|
39
28
|
}
|
|
40
29
|
// A command is run where Toolroll runs; a page is linked. Fix and Set up both say which.
|
|
41
|
-
// Fix is the
|
|
30
|
+
// Fix is the louder of the two; Set up stays quiet so a list of unused services doesn't shout.
|
|
42
31
|
if (a.href !== null)
|
|
43
|
-
return
|
|
32
|
+
return btn(a.label, { sm: true, href: a.href, variant: a.kind === "setup" ? "ghost" : "outline" });
|
|
44
33
|
return "";
|
|
45
34
|
}
|
|
46
35
|
/** Which mark a row shows: email is Gmail's only when the account is a Gmail address; a custom tool is a letter. */
|
|
@@ -49,33 +38,38 @@ function markFor(one) {
|
|
|
49
38
|
return null;
|
|
50
39
|
return one.key === "email" && /@(gmail|googlemail)\.com$/i.test(one.account ?? "") ? "gmail" : one.key;
|
|
51
40
|
}
|
|
41
|
+
/** Its state as one of the kit's status chips: working, broken, not set up, or still being checked. */
|
|
42
|
+
function stateChip(one) {
|
|
43
|
+
if (!one.checked && one.state === "connected")
|
|
44
|
+
return chip("Checking", "live");
|
|
45
|
+
return chip(STATE_WORDS[one.state], one.state === "connected" ? "done" : one.state === "broken" ? "failed" : "");
|
|
46
|
+
}
|
|
52
47
|
function row(one, csrf) {
|
|
53
|
-
const badge = !one.checked && one.state === "connected"
|
|
54
|
-
? html `<span class="integration-state integration-state--checking"><i aria-hidden="true"></i>Checking</span>`
|
|
55
|
-
: html `<span class="integration-state integration-state--${one.state}"><i aria-hidden="true"></i>${STATE_WORDS[one.state]}</span>`;
|
|
56
48
|
const facts = [one.account, one.detail, one.credentials].filter(Boolean).join(" · ");
|
|
57
49
|
const command = one.action.kind === "setup" || one.action.kind === "fix" ? one.action.command : null;
|
|
58
50
|
const words = one.action.kind === "fix" || one.action.kind === "switch" ? one.action.words : null;
|
|
59
|
-
const fix = words !== null ?
|
|
60
|
-
const run = command === null || (one.action.kind === "fix" && one.action.words.includes(command)) ? "" : html
|
|
51
|
+
const fix = words !== null ? notice(words) : "";
|
|
52
|
+
const run = command === null || (one.action.kind === "fix" && one.action.words.includes(command)) ? "" : note(html `Run <code class="k-mono">${command}</code> on the computer running Toolroll.`);
|
|
61
53
|
const seen = [one.lastSuccessAt === null ? null : html `Last success ${when(one.lastSuccessAt)}`, one.usedBy.length === 0 ? null : html `Used by ${one.usedBy.join(", ")}`].filter(Boolean);
|
|
62
|
-
const history = one.state === "not-set-up" || seen.length === 0 ? "" :
|
|
54
|
+
const history = one.state === "not-set-up" || seen.length === 0 ? "" : note(joinHtml(seen, " · "));
|
|
63
55
|
const lastError = one.lastError === null || one.state === "not-set-up" ? "" : one.state === "broken" && words === one.lastError
|
|
64
|
-
? html
|
|
65
|
-
:
|
|
66
|
-
|
|
56
|
+
? note(html `Failed ${when(one.lastErrorAt ?? "")}`)
|
|
57
|
+
: disclose(`Last error${one.lastErrorAt === null ? "" : ` ${one.lastErrorAt.slice(0, 16).replace("T", " ")} UTC`}`, note(one.lastError));
|
|
58
|
+
const more = joinHtml([fix, run, history, lastError]);
|
|
59
|
+
return settingsRecord({ title: html `${brandMarkHtml(markFor(one), one.name, one.state === "connected")}<span>${one.name}</span> ${stateChip(one)}`,
|
|
60
|
+
sub: facts, action: action(one, csrf), attrs: { "data-integration": one.key, "data-state": one.state }, more });
|
|
67
61
|
}
|
|
68
|
-
export function integrationsHtml(list, csrf,
|
|
69
|
-
const
|
|
62
|
+
export function integrationsHtml(list, csrf, said) {
|
|
63
|
+
const answer = said.problem ? notice(said.problem) : said.said ? infoLine(said.said) : "";
|
|
70
64
|
const broken = list.filter(one => one.state === "broken").length;
|
|
71
|
-
const summary = broken > 0 ? html `<p><
|
|
72
|
-
:
|
|
65
|
+
const summary = broken > 0 ? html `<p class="k-note"><b>${broken} need${broken === 1 ? "s" : ""} fixing.</b></p>`
|
|
66
|
+
: said.checking || list.some(one => !one.checked) ? html `<p class="k-note" role="status">Checking in the background. Refresh in a few seconds.</p>` : "";
|
|
67
|
+
// No keychain here, or Toolroll's folder for secrets can't be used: said once, plainly.
|
|
68
|
+
const kept = secretsNote(list, said.secrets);
|
|
69
|
+
const secrets = kept === null ? "" : kept.problem ? notice(kept.words) : html `<p class="k-note" role="note">${kept.words}</p>`;
|
|
73
70
|
const sections = GROUPS.map(([group, title]) => {
|
|
74
71
|
const rows = list.filter(one => one.group === group);
|
|
75
|
-
return rows.length === 0 ? "" :
|
|
72
|
+
return rows.length === 0 ? "" : settingsSection({ id: `integrations-${group}`, title, body: joinHtml(rows.map(one => row(one, csrf))) });
|
|
76
73
|
});
|
|
77
|
-
|
|
78
|
-
const said = secretsNote(list, notice.secrets);
|
|
79
|
-
const kept = said === null ? "" : said.problem ? html `<p class="problem integration-secrets" role="alert">${said.words}</p>` : html `<p class="meta integration-secrets" role="note">${said.words}</p>`;
|
|
80
|
-
return html `<section class="integrations">${note}${summary}${kept}${sections}</section>`;
|
|
74
|
+
return joinHtml([answer, summary, secrets, ...sections]);
|
|
81
75
|
}
|
package/dist/integrations.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type IntegrationAction = ({
|
|
|
13
13
|
label: "Set up";
|
|
14
14
|
} & IntegrationPlace) | {
|
|
15
15
|
kind: "test";
|
|
16
|
-
label: "
|
|
16
|
+
label: "Check connection";
|
|
17
17
|
} | ({
|
|
18
18
|
kind: "fix";
|
|
19
19
|
label: "Fix";
|
|
@@ -124,7 +124,7 @@ export declare function renderIntegrations(list: readonly Integration[], secrets
|
|
|
124
124
|
export type IntegrationMonitor = {
|
|
125
125
|
/** The list now, from the last answers saved. It only reads: a page calls it on every render (docs/plans/get-side-effects.md). */
|
|
126
126
|
list(): Integration[];
|
|
127
|
-
/** Check now and wait (
|
|
127
|
+
/** Check now and wait (Check connection, Check now, or the first answer in a test). */
|
|
128
128
|
check(keys?: readonly string[]): Promise<Integration[]>;
|
|
129
129
|
/** The background pass: start a check when the last one is older than the cache. Never called from a page. */
|
|
130
130
|
refreshIfDue(): void;
|
package/dist/integrations.js
CHANGED
|
@@ -327,7 +327,7 @@ function targets(io) {
|
|
|
327
327
|
const list = [];
|
|
328
328
|
list.push({
|
|
329
329
|
key: "telegram", group: "chat", name: "Telegram", configured: telegram !== null, account: null,
|
|
330
|
-
setup: place("/settings#telegram-token"), fix: place("/settings#telegram-token"), usedBy: chatUses("telegram"), activity: telegramActivity(store),
|
|
330
|
+
setup: place("/settings/alerts#telegram-token"), fix: place("/settings/alerts#telegram-token"), usedBy: chatUses("telegram"), activity: telegramActivity(store),
|
|
331
331
|
probe: telegram === null ? null : check => probeTelegram(check, telegram.token),
|
|
332
332
|
});
|
|
333
333
|
list.push({
|
|
@@ -371,7 +371,7 @@ function targets(io) {
|
|
|
371
371
|
: { okAt: str(mailWatch["updated_at"]), error: null, errorAt: null, failing: false };
|
|
372
372
|
list.push({
|
|
373
373
|
key: "email", group: "mail", name: "Email", configured: email !== null || google !== null, account: email?.from ?? google?.address ?? null,
|
|
374
|
-
setup: place("/settings#email"), fix: place("/settings#email"), usedBy: [...flowWords(emailUses)], activity: merge(triggerActivity(emailUses), watchActivity),
|
|
374
|
+
setup: place("/settings/alerts#email"), fix: place("/settings/alerts#email"), usedBy: [...flowWords(emailUses)], activity: merge(triggerActivity(emailUses), watchActivity),
|
|
375
375
|
probe: email === null ? null : check => probeEmail(check, { host: email.host, port: email.port, secure: email.secure }, email.imap),
|
|
376
376
|
});
|
|
377
377
|
// Each project's MCP tools: the server starts and lists its tools.
|
|
@@ -432,7 +432,7 @@ function targets(io) {
|
|
|
432
432
|
for (const provider of AGENTS) {
|
|
433
433
|
list.push({
|
|
434
434
|
key: `agent:${provider}`, group: "agents", name: providerName(provider), configured: true, account: null,
|
|
435
|
-
setup: place("/settings#providers"), fix: place("/settings#providers"), usedBy: ["Tasks"], activity: NO_ACTIVITY,
|
|
435
|
+
setup: place("/settings/agents#providers"), fix: place("/settings/agents#providers"), usedBy: ["Tasks"], activity: NO_ACTIVITY,
|
|
436
436
|
probe: check => probeAgent(check, provider),
|
|
437
437
|
});
|
|
438
438
|
}
|
|
@@ -458,7 +458,7 @@ function combine(target, row) {
|
|
|
458
458
|
if (failing !== null) {
|
|
459
459
|
return { ...base, ...common, state: "broken", action: { kind: "fix", label: "Fix", words: scrubIntegrationText(failing ?? "The last check failed."), ...target.fix } };
|
|
460
460
|
}
|
|
461
|
-
return { ...base, ...common, state: "connected", action: { kind: "test", label: "
|
|
461
|
+
return { ...base, ...common, state: "connected", action: { kind: "test", label: "Check connection" } };
|
|
462
462
|
}
|
|
463
463
|
/**
|
|
464
464
|
* youiyoux change requests, one row per project that uses youiyoux (youiyoux-intake.ts): Connected with how many of
|
package/dist/kits-ui.d.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Starter kits
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Starter kits: the "With an agent" rows of New flow (each kit's promise, what it sets up, one button to its page)
|
|
3
|
+
* and a kit's own page, a reading page: what it sets up, then its checklist with the next step on each line
|
|
4
|
+
* (connect email, connect a tool, try a sample card).
|
|
5
5
|
*/
|
|
6
6
|
import type { Store } from "./store.js";
|
|
7
7
|
import { type Kit } from "./kits.js";
|
|
8
8
|
import { type Html } from "./html.js";
|
|
9
9
|
export declare const KITS_CSS: string;
|
|
10
|
-
/**
|
|
11
|
-
export declare function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
/** A kit's own page: what's set up, and the next step on each line. */
|
|
15
|
-
export declare function kitPageHtml(store: Store, kit: Kit, repo: string, dir: string | null, canSetUp: boolean, notice: {
|
|
10
|
+
/** "With an agent": every kit as a row; Set it up (or Open, once it's there) goes to the kit's page for `repo`. */
|
|
11
|
+
export declare function kitRowsHtml(store: Store, repo: string | null): Html;
|
|
12
|
+
/** A kit's own page: what it sets up, Set up (once), then the next step on each line. */
|
|
13
|
+
export declare function kitPageHtml(store: Store, kit: Kit, repo: string, dir: string | null, canSetUp: boolean, notice_: {
|
|
16
14
|
said?: string | null;
|
|
17
15
|
problem?: string | null;
|
|
18
|
-
}): Html;
|
|
16
|
+
}, projectName?: (repo: string) => string): Html;
|