toolroll 0.9.54 → 0.9.55
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 +9 -0
- package/README.md +5 -4
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +15 -20
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +2 -3
- package/dist/browser-workspace.js +0 -3
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -3
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +6 -5
- package/dist/contracts/health.d.ts +1 -3
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +17 -42
- package/dist/envelope.js +10 -0
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-triggers.d.ts +1 -0
- package/dist/flow-triggers.js +16 -14
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +19 -30
- package/dist/flows.js +2 -1
- package/dist/guides.js +108 -3
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +18 -20
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/lead-context.d.ts +11 -1
- package/dist/lead-context.js +6 -8
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +2 -1
- package/dist/limits-ui.js +7 -7
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +11 -7
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +15 -27
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +69 -58
- package/dist/server/chat.d.ts +3 -6
- package/dist/server/chat.js +969 -1006
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +840 -766
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +5 -2
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +580 -572
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +37 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +147 -0
- package/dist/server/render-settings.js +577 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +163 -75
- package/dist/server/runtime.d.ts +15 -7
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2181 -2117
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1486 -1418
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +17 -42
- package/dist/spend-ui.d.ts +2 -1
- package/dist/spend-ui.js +9 -32
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/surface.js +14 -3
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +20 -31
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -1
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
package/dist/policy-ui.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings → Policy (sprint 8): the organisation policy — allowed providers,
|
|
3
|
+
* models and tools, and the permission ceiling — and its history from the
|
|
4
|
+
* action ledger. Anyone signed in reads it; an instance operator changes it
|
|
5
|
+
* with their password.
|
|
6
|
+
*/
|
|
7
|
+
import { html, postForm } from "./html.js";
|
|
1
8
|
import { whenUtc } from "./when-html.js";
|
|
2
9
|
import { LEVEL_HINTS, LEVEL_NAMES, PERMISSION_LEVELS, POLICY_PROVIDERS, PROVIDER_NAMES, policyParts } from "./policy.js";
|
|
3
|
-
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
4
10
|
export const POLICY_CSS = `.policy{max-width:720px;min-width:0}.policy dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:6px 16px;margin:12px 0 0;font-size:.875rem}` +
|
|
5
11
|
`.policy dt{color:var(--muted-foreground)}.policy dd{margin:0;overflow-wrap:anywhere}.policy fieldset{border:0;padding:0;margin:20px 0 0;min-width:0}` +
|
|
6
12
|
`.policy legend{font-weight:600;font-size:.875rem;margin-bottom:8px}.policy legend{padding:0}.policy .providers{display:grid;grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:0 16px}.policy .providers label{display:flex;gap:8px;align-items:center;min-height:44px;margin:0;padding:0;font-weight:400}` +
|
|
@@ -13,30 +19,18 @@ export const POLICY_CSS = `.policy{max-width:720px;min-width:0}.policy dl{displa
|
|
|
13
19
|
function historyItem(entry) {
|
|
14
20
|
const what = entry.action.replace(/^organisation policy: /, "");
|
|
15
21
|
const [before, after] = (entry.detail ?? "").split(" → ");
|
|
16
|
-
return `<li><span class="what">${
|
|
17
|
-
`<span class="meta">${e(entry.actor)} · ${whenUtc(entry.at)}</span></li>`;
|
|
22
|
+
return html `<li><span class="what">${what.charAt(0).toUpperCase() + what.slice(1)}: ${before ?? ""} → <strong>${after ?? ""}</strong></span><span class="meta">${entry.actor} · ${whenUtc(entry.at)}</span></li>`;
|
|
18
23
|
}
|
|
19
|
-
export function policyHtml(view,
|
|
20
|
-
const note = notice.problem ? `<p class="problem" role="alert">${
|
|
24
|
+
export function policyHtml(view, notice) {
|
|
25
|
+
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
21
26
|
const parts = policyParts(view.policy);
|
|
22
|
-
const changed = view.policy.updatedBy === null ? "" : `<p class="meta">Changed by ${
|
|
23
|
-
const current = `<dl><dt>Providers</dt><dd>${
|
|
24
|
-
|
|
25
|
-
const history = view.history.length === 0 ? "" : `<details><summary>History (${view.history.length})</summary><ol>${view.history.map(historyItem).join("")}</ol></details>`;
|
|
27
|
+
const changed = view.policy.updatedBy === null ? "" : html `<p class="meta">Changed by ${view.policy.updatedBy} on ${(view.policy.updatedAt ?? "").slice(0, 10)}.</p>`;
|
|
28
|
+
const current = html `<dl><dt>Providers</dt><dd>${parts.providers === "any" ? "Any" : parts.providers}</dd><dt>Models</dt><dd>${parts.models === "any" ? "Any" : parts.models}</dd><dt>Tools</dt><dd>${parts.tools === "any" ? "Any" : parts.tools}</dd><dt>Ceiling</dt><dd>${parts.ceiling}</dd></dl>${changed}`;
|
|
29
|
+
const history = view.history.length === 0 ? "" : html `<details><summary>History (${view.history.length})</summary><ol>${view.history.map(historyItem)}</ol></details>`;
|
|
26
30
|
if (!view.canChange)
|
|
27
|
-
return `<section class="policy">${note}${current}<p class="meta">An instance operator sets the policy.</p>${history}</section>`;
|
|
28
|
-
const providers = POLICY_PROVIDERS.map(one => `<label><input type="checkbox" name="provider" value="${one}"${view.policy.providers === null || view.policy.providers.includes(one) ?
|
|
29
|
-
const levels = [...PERMISSION_LEVELS].reverse().map(level => `<label class="choice"><input type="radio" name="ceiling" value="${level}"${view.policy.ceiling === level ?
|
|
30
|
-
`<span><strong>${LEVEL_NAMES[level]}</strong><small>${e(LEVEL_HINTS[level])}</small></span></label>`).join("");
|
|
31
|
+
return html `<section class="policy">${note}${current}<p class="meta">An instance operator sets the policy.</p>${history}</section>`;
|
|
32
|
+
const providers = POLICY_PROVIDERS.map(one => html `<label><input type="checkbox" name="provider" value="${one}"${view.policy.providers === null || view.policy.providers.includes(one) ? html ` checked` : ""}> ${PROVIDER_NAMES[one]}</label>`);
|
|
33
|
+
const levels = [...PERMISSION_LEVELS].reverse().map(level => html `<label class="choice"><input type="radio" name="ceiling" value="${level}"${view.policy.ceiling === level ? html ` checked` : ""}><span><strong>${LEVEL_NAMES[level]}</strong><small>${LEVEL_HINTS[level]}</small></span></label>`);
|
|
31
34
|
const toolHint = view.toolNames.length === 0 ? "" : ` Tools here: ${view.toolNames.slice(0, 12).join(", ")}${view.toolNames.length > 12 ? "…" : ""}.`;
|
|
32
|
-
return `<section class="policy">${note}${changed}
|
|
33
|
-
`<form method="post" action="/settings/policy"><input type="hidden" name="csrf" value="${e(csrf)}">` +
|
|
34
|
-
`<fieldset><legend>Providers</legend><div class="providers">${providers}</div></fieldset>` +
|
|
35
|
-
`<fieldset><legend><label for="policy-models">Models</label></legend><textarea id="policy-models" name="models" spellcheck="false" placeholder="Any model" aria-describedby="policy-models-hint">${e((view.policy.models ?? []).join("\n"))}</textarea>` +
|
|
36
|
-
`<span class="hint" id="policy-models-hint">One per line; end with * to allow a family. Blank allows any.</span></fieldset>` +
|
|
37
|
-
`<fieldset><legend><label for="policy-tools">Project tools</label></legend><textarea id="policy-tools" name="tools" spellcheck="false" placeholder="Any tool" aria-describedby="policy-tools-hint">${e((view.policy.tools ?? []).join("\n"))}</textarea>` +
|
|
38
|
-
`<span class="hint" id="policy-tools-hint">One per line. Blank allows any.${e(toolHint)}</span></fieldset>` +
|
|
39
|
-
`<fieldset><legend>Permission ceiling</legend>${levels}</fieldset>` +
|
|
40
|
-
`<div class="step-up"><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label></div>` +
|
|
41
|
-
`<button type="submit">Save policy</button></form>${history}</section>`;
|
|
35
|
+
return html `<section class="policy">${note}${changed}${postForm("/settings/policy", html `<fieldset><legend>Providers</legend><div class="providers">${providers}</div></fieldset><fieldset><legend><label for="policy-models">Models</label></legend><textarea id="policy-models" name="models" spellcheck="false" placeholder="Any model" aria-describedby="policy-models-hint">${(view.policy.models ?? []).join("\n")}</textarea><span class="hint" id="policy-models-hint">One per line; end with * to allow a family. Blank allows any.</span></fieldset><fieldset><legend><label for="policy-tools">Project tools</label></legend><textarea id="policy-tools" name="tools" spellcheck="false" placeholder="Any tool" aria-describedby="policy-tools-hint">${(view.policy.tools ?? []).join("\n")}</textarea><span class="hint" id="policy-tools-hint">One per line. Blank allows any.${toolHint}</span></fieldset><fieldset><legend>Permission ceiling</legend>${levels}</fieldset><div class="step-up"><label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label></div><button type="submit">Save policy</button>`)}${history}</section>`;
|
|
42
36
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* then read exactly what goes and confirm with their password (or a fresh
|
|
5
5
|
* sign-in with the identity provider). The repository itself stays.
|
|
6
6
|
*/
|
|
7
|
+
import { type Html } from "./html.js";
|
|
7
8
|
import { type ProjectHoldings } from "./project-delete.js";
|
|
8
9
|
export declare const PROJECT_DELETE_CSS: string;
|
|
9
10
|
export type ProjectSettingsView = {
|
|
@@ -21,9 +22,9 @@ export type ProjectSettingsView = {
|
|
|
21
22
|
};
|
|
22
23
|
};
|
|
23
24
|
/** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
|
|
24
|
-
export declare function projectSettingsHtml(view: ProjectSettingsView,
|
|
25
|
+
export declare function projectSettingsHtml(view: ProjectSettingsView, notice: {
|
|
25
26
|
said?: string | null;
|
|
26
27
|
problem?: string | null;
|
|
27
|
-
}):
|
|
28
|
+
}): Html;
|
|
28
29
|
/** The second step: exactly what goes, and the password. */
|
|
29
|
-
export declare function projectDeleteConfirmHtml(view: ProjectSettingsView,
|
|
30
|
+
export declare function projectDeleteConfirmHtml(view: ProjectSettingsView, problem: string | null): Html;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* then read exactly what goes and confirm with their password (or a fresh
|
|
5
5
|
* sign-in with the identity provider). The repository itself stays.
|
|
6
6
|
*/
|
|
7
|
+
import { html, postForm } from "./html.js";
|
|
7
8
|
import { holdingsWords } from "./project-delete.js";
|
|
8
|
-
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9
9
|
export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:720px;min-width:0}.project-settings .path,.project-delete .path{font-family:var(--font-mono);font-size:.8125rem;overflow-wrap:anywhere;word-break:break-all}` +
|
|
10
10
|
`.project-settings details.danger-zone{margin-top:24px;border:1px solid var(--border);border-radius:10px;padding:0 14px}.project-settings details.danger-zone summary{min-height:44px;display:flex;align-items:center;cursor:pointer;font-weight:500;color:var(--danger)}` +
|
|
11
11
|
`.project-settings details.danger-zone[open]{padding-bottom:14px}.project-settings form,.project-delete form{display:grid;gap:12px;margin-top:12px}.project-settings label,.project-delete label{display:grid;gap:4px;font-size:.8125rem;font-weight:500}` +
|
|
@@ -16,39 +16,32 @@ export const PROJECT_DELETE_CSS = `.project-settings,.project-delete{max-width:7
|
|
|
16
16
|
`.project-settings .builds-at-once .builds-row{display:flex;gap:8px;align-items:stretch}.project-settings .builds-at-once input{width:5.5rem;min-height:44px;margin:0;box-sizing:border-box}.project-settings .builds-at-once button{min-height:44px;margin:0}` +
|
|
17
17
|
`.project-settings .builds-at-once .meta{margin:0}.project-settings form.remove-project{margin-top:20px;gap:4px}.project-settings form.remove-project button{min-height:44px}.project-settings form.remove-project .meta{margin:0}`;
|
|
18
18
|
/** Builds at once: one number, one Save. The worker's cap is said only when it lowers the number. */
|
|
19
|
-
function buildsHtml(view
|
|
19
|
+
function buildsHtml(view) {
|
|
20
20
|
const b = view.builds;
|
|
21
21
|
if (b === undefined)
|
|
22
22
|
return "";
|
|
23
23
|
const capped = b.capacity !== null && b.capacity < b.setting ? ` Its worker runs ${b.capacity} at once, so ${b.capacity} for now.` : "";
|
|
24
24
|
const now = `${b.building} building now.${capped}`;
|
|
25
25
|
if (!b.canChange)
|
|
26
|
-
return `<section class="builds-at-once"><h2>Builds at once</h2><p>Up to ${b.setting}. ${
|
|
27
|
-
return `<section class="builds-at-once"><h2>Builds at once</h2><
|
|
28
|
-
`<input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="repo" value="${e(view.repo)}">` +
|
|
29
|
-
`<div class="builds-row"><input type="number" name="concurrency" min="1" max="64" step="1" inputmode="numeric" value="${b.setting}" aria-label="Builds at once" aria-describedby="builds-now"><button type="submit">Save</button></div>` +
|
|
30
|
-
`<p class="meta" id="builds-now">${e(now)}</p></form></section>`;
|
|
26
|
+
return html `<section class="builds-at-once"><h2>Builds at once</h2><p>Up to ${b.setting}. ${now}</p></section>`;
|
|
27
|
+
return html `<section class="builds-at-once"><h2>Builds at once</h2>${postForm("/settings/project/concurrency", html `<div class="builds-row"><input type="number" name="concurrency" min="1" max="64" step="1" inputmode="numeric" value="${b.setting}" aria-label="Builds at once" aria-describedby="builds-now"><button type="submit">Save</button></div><p class="meta" id="builds-now">${now}</p>`, { hidden: { repo: view.repo } })}</section>`;
|
|
31
28
|
}
|
|
32
|
-
const note = (notice) => notice.problem ? `<p class="problem" role="alert">${
|
|
29
|
+
const note = (notice) => notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
33
30
|
/** The project's page: its path, what Toolroll holds for it, and (for an instance operator) Delete project. */
|
|
34
|
-
export function projectSettingsHtml(view,
|
|
35
|
-
const held = `<p>Toolroll holds ${
|
|
36
|
-
const head =
|
|
31
|
+
export function projectSettingsHtml(view, notice) {
|
|
32
|
+
const held = html `<p>Toolroll holds ${holdingsWords(view.holdings)} for <strong>${view.name}</strong>.</p>`;
|
|
33
|
+
const head = html `${note(notice)}<p class="path">${view.repo}</p>${held}${buildsHtml(view)}`;
|
|
37
34
|
if (!view.canDelete)
|
|
38
|
-
return
|
|
35
|
+
return html `<section class="project-settings">${head}<p class="meta">An instance operator can delete a project.</p></section>`;
|
|
39
36
|
const body = view.running.length > 0
|
|
40
|
-
? `<p class="problem">Its work is running: ${
|
|
41
|
-
: `<p>Removes everything Toolroll holds for ${
|
|
42
|
-
`<form method="post" action="/settings/project/delete"><input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="repo" value="${e(view.repo)}">` +
|
|
43
|
-
`<label><span>Type <strong>${e(view.name)}</strong> to continue</span><input name="name" autocomplete="off" autocapitalize="off" spellcheck="false" required></label>` +
|
|
44
|
-
`<button type="submit">Continue</button></form>`;
|
|
37
|
+
? html `<p class="problem">Its work is running: ${view.running.join(", ")}. Stop it before deleting the project.</p>`
|
|
38
|
+
: html `<p>Removes everything Toolroll holds for ${view.name}. The repository and its own branches stay. There's no undo.</p>${postForm("/settings/project/delete", html `<label><span>Type <strong>${view.name}</strong> to continue</span><input name="name" autocomplete="off" autocapitalize="off" spellcheck="false" required></label><button type="submit">Continue</button>`, { hidden: { repo: view.repo } })}`;
|
|
45
39
|
// Removing is the reversible step: off the lists and the builder, everything kept.
|
|
46
|
-
const remove =
|
|
47
|
-
|
|
48
|
-
return `${head}${remove}<details class="danger-zone"${notice.problem ? " open" : ""}><summary>Delete project</summary>${body}</details></section>`;
|
|
40
|
+
const remove = postForm("/projects/remove", html `<button type="submit">Remove from Toolroll</button><p class="meta">Its tasks and results stay saved. Add it again to bring it back.</p>`, { attrs: { class: "remove-project" }, hidden: { repo: view.repo } });
|
|
41
|
+
return html `<section class="project-settings">${head}${remove}<details class="danger-zone"${notice.problem ? html ` open` : ""}><summary>Delete project</summary>${body}</details></section>`;
|
|
49
42
|
}
|
|
50
43
|
/** The second step: exactly what goes, and the password. */
|
|
51
|
-
export function projectDeleteConfirmHtml(view,
|
|
44
|
+
export function projectDeleteConfirmHtml(view, problem) {
|
|
52
45
|
const h = view.holdings;
|
|
53
46
|
const count = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
54
47
|
const items = [
|
|
@@ -59,10 +52,5 @@ export function projectDeleteConfirmHtml(view, csrf, problem) {
|
|
|
59
52
|
...(h.teammates > 0 ? [count(h.teammates, "teammate")] : []),
|
|
60
53
|
"Its budgets, settings and knowledge",
|
|
61
54
|
];
|
|
62
|
-
return `<section class="project-delete">${problem === null ? "" : `<p class="problem" role="alert">${
|
|
63
|
-
`<p>This removes:</p><ul class="removes">${items.map(one => `<li>${e(one)}</li>`).join("")}</ul>` +
|
|
64
|
-
`<p>The repository at <span class="path">${e(view.repo)}</span> and its own branches stay. The ledger keeps its history. There's no undo.</p>` +
|
|
65
|
-
`<form method="post" action="/settings/project/delete"><input type="hidden" name="csrf" value="${e(csrf)}"><input type="hidden" name="repo" value="${e(view.repo)}"><input type="hidden" name="name" value="${e(view.name)}"><input type="hidden" name="step" value="delete">` +
|
|
66
|
-
`<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>` +
|
|
67
|
-
`<div class="actions"><button type="submit" class="danger">Delete project</button><a href="/settings/project?repo=${e(encodeURIComponent(view.repo))}">Cancel</a></div></form></section>`;
|
|
55
|
+
return html `<section class="project-delete">${problem === null ? "" : html `<p class="problem" role="alert">${problem}</p>`}<p>This removes:</p><ul class="removes">${items.map(one => html `<li>${one}</li>`)}</ul><p>The repository at <span class="path">${view.repo}</span> and its own branches stay. The ledger keeps its history. There's no undo.</p>${postForm("/settings/project/delete", html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label><div class="actions"><button type="submit" class="danger">Delete project</button><a href="/settings/project?repo=${encodeURIComponent(view.repo)}">Cancel</a></div>`, { hidden: { repo: view.repo, name: view.name, step: "delete" } })}</section>`;
|
|
68
56
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Settings → Projects → Pull requests: one project's pull-request setup, in plain words. Off shows what setup
|
|
2
2
|
* found (or what to fix) and one Turn on; on shows where pull requests go and how they merge. Every change takes
|
|
3
3
|
* the password; turning on restates the exact repository and branch that were checked. */
|
|
4
|
+
import { type Html } from "./html.js";
|
|
4
5
|
import type { Publishing, PublishingPlan } from "./pull-request-flow.js";
|
|
5
6
|
export type PullRequestSettingsView = {
|
|
6
7
|
repo: string;
|
|
@@ -19,5 +20,5 @@ export type PullRequestSettingsView = {
|
|
|
19
20
|
said: string | null;
|
|
20
21
|
problem: string | null;
|
|
21
22
|
};
|
|
22
|
-
export declare function pullRequestSettingsHtml(view: PullRequestSettingsView):
|
|
23
|
+
export declare function pullRequestSettingsHtml(view: PullRequestSettingsView): Html;
|
|
23
24
|
export declare const PULL_REQUEST_SETTINGS_CSS = "\n .pr-settings { display: grid; gap: .6rem; max-width: 40rem; }\n .pr-settings p, .pr-settings h2 { margin: 0; }\n .pr-settings label { display: grid; gap: .25rem; }\n .pr-settings label input[type=password], .pr-settings select { min-height: 44px; box-sizing: border-box; width: 100%; }\n .pr-settings .pr-check { display: flex; align-items: center; gap: .5rem; min-height: 44px; }\n .pr-settings button { min-height: 44px; justify-self: start; }\n .pr-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n @media (max-width: 480px) { .pr-settings button { justify-self: stretch; } }\n";
|
package/dist/pull-request-ui.js
CHANGED
|
@@ -1,47 +1,32 @@
|
|
|
1
|
-
|
|
1
|
+
/** Settings → Projects → Pull requests: one project's pull-request setup, in plain words. Off shows what setup
|
|
2
|
+
* found (or what to fix) and one Turn on; on shows where pull requests go and how they merge. Every change takes
|
|
3
|
+
* the password; turning on restates the exact repository and branch that were checked. */
|
|
4
|
+
import { html, postForm } from "./html.js";
|
|
2
5
|
const METHOD_WORDS = { squash: "Squash and merge", merge: "Merge commit", rebase: "Rebase and merge" };
|
|
3
6
|
export function pullRequestSettingsHtml(view) {
|
|
4
|
-
const
|
|
5
|
-
const password = `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`;
|
|
7
|
+
const post = (act, body, hidden = {}) => postForm("/settings/pull-requests", body, { attrs: { class: "card pr-settings" }, hidden: { repo: view.repo, act, ...hidden } });
|
|
8
|
+
const password = html `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`;
|
|
6
9
|
const notes = [
|
|
7
|
-
view.said === null ? "" : `<p class="notice" role="status">${
|
|
8
|
-
view.problem === null ? "" : `<p class="problem" role="alert">${
|
|
9
|
-
]
|
|
10
|
-
const head = `<p class="meta">${
|
|
10
|
+
view.said === null ? "" : html `<p class="notice" role="status">${view.said}</p>`,
|
|
11
|
+
view.problem === null ? "" : html `<p class="problem" role="alert">${view.problem}</p>`,
|
|
12
|
+
];
|
|
13
|
+
const head = html `<p class="meta">${view.name} · <span class="mono">${view.repo}</span></p>${notes}`;
|
|
11
14
|
const publishing = view.publishing;
|
|
12
15
|
if (publishing.on) {
|
|
13
|
-
const status = `<section class="card pr-settings"><p><strong>On</strong> · <span class="mono">${
|
|
14
|
-
`<p class="meta">${publishing.legacy ? "Every finished build opens a pull request (set up with publish grant)." : "Complete offers “Complete and open a pull request”."} Set up by ${escape(publishing.grantedBy)}.</p></section>`;
|
|
16
|
+
const status = html `<section class="card pr-settings"><p><strong>On</strong> · <span class="mono">${publishing.githubRepo}</span> into <span class="mono">${publishing.base}</span></p><p class="meta">${publishing.legacy ? "Every finished build opens a pull request (set up with publish grant)." : "Complete offers “Complete and open a pull request”."} Set up by ${publishing.grantedBy}.</p></section>`;
|
|
15
17
|
if (!view.canChange)
|
|
16
|
-
return head
|
|
17
|
-
const methods = ["squash", "merge", "rebase"].map(method => `<option value="${method}"${publishing.mergeMethod === method ?
|
|
18
|
-
return head
|
|
19
|
-
`<form method="post" action="/settings/pull-requests" class="card pr-settings">${hidden("settings")}` +
|
|
20
|
-
`<h2>Merging</h2>` +
|
|
21
|
-
`<label>Merge method<select name="method">${methods}</select></label>` +
|
|
22
|
-
`<label class="row pr-check"><input type="checkbox" name="when-green" value="1"${publishing.mergeWhenGreen ? " checked" : ""}> Merge when checks pass</label>` +
|
|
23
|
-
`<p class="meta">Off by default. When on, a pull request merges by itself once its checks pass on the completed commit. Branches are deleted after merging.</p>` +
|
|
24
|
-
`${password}<button type="submit">Save</button></form>` +
|
|
25
|
-
`<details class="settings-more"><summary>Turn off pull requests</summary>` +
|
|
26
|
-
`<form method="post" action="/settings/pull-requests" class="card pr-settings">${hidden("off")}` +
|
|
27
|
-
`<p class="meta">Nothing more is pushed. Open pull requests stay on GitHub.</p>${password}<button type="submit" class="danger">Turn off</button></form></details>`;
|
|
18
|
+
return html `${head}${status}`;
|
|
19
|
+
const methods = ["squash", "merge", "rebase"].map(method => html `<option value="${method}"${publishing.mergeMethod === method ? html ` selected` : ""}>${METHOD_WORDS[method]}${method === "squash" ? " (default)" : ""}</option>`);
|
|
20
|
+
return html `${head}${status}${post("settings", html `<h2>Merging</h2><label>Merge method<select name="method">${methods}</select></label><label class="row pr-check"><input type="checkbox" name="when-green" value="1"${publishing.mergeWhenGreen ? html ` checked` : ""}> Merge when checks pass</label><p class="meta">Off by default. When on, a pull request merges by itself once its checks pass on the completed commit. Branches are deleted after merging.</p>${password}<button type="submit">Save</button>`)}<details class="settings-more"><summary>Turn off pull requests</summary>${post("off", html `<p class="meta">Nothing more is pushed. Open pull requests stay on GitHub.</p>${password}<button type="submit" class="danger">Turn off</button>`)}</details>`;
|
|
28
21
|
}
|
|
29
22
|
const check = view.check;
|
|
30
23
|
if (check === null)
|
|
31
|
-
return head
|
|
24
|
+
return html `${head}<section class="card pr-settings"><p><strong>Off</strong></p><p class="meta">An approver can turn on pull requests.</p></section>`;
|
|
32
25
|
if (!check.ok) {
|
|
33
|
-
return head
|
|
34
|
-
`<p><a class="button-link" href="/settings/pull-requests?repo=${encodeURIComponent(view.repo)}">Check again</a></p></section>`;
|
|
26
|
+
return html `${head}<section class="card pr-settings"><p><strong>Can’t turn on yet</strong></p><p>${check.message}</p><p><a class="button-link" href="/settings/pull-requests?repo=${encodeURIComponent(view.repo)}">Check again</a></p></section>`;
|
|
35
27
|
}
|
|
36
28
|
const plan = check.plan;
|
|
37
|
-
return head
|
|
38
|
-
`<form method="post" action="/settings/pull-requests" class="card pr-settings">${hidden("on")}` +
|
|
39
|
-
`<input type="hidden" name="github" value="${escape(plan.githubRepo)}"><input type="hidden" name="base" value="${escape(plan.base)}">` +
|
|
40
|
-
`<p><strong>Ready to turn on</strong></p>` +
|
|
41
|
-
`<ul class="pr-terms"><li>Pull requests open on <span class="mono">${escape(plan.githubRepo)}</span> into <span class="mono">${escape(plan.base)}</span>${plan.account === null ? "" : `, as ${escape(plan.account)}`}.</li>` +
|
|
42
|
-
`<li>Complete offers “Complete and open a pull request”, from the exact completed commit. A flow's Pull request zone can open one too.</li>` +
|
|
43
|
-
`<li>Merging squashes and deletes the branch. A person merges with their password; a flow merges only when its zone says so, after a person approved the card.</li></ul>` +
|
|
44
|
-
`${password}<button type="submit">Turn on pull requests</button></form>`;
|
|
29
|
+
return html `${head}${post("on", html `<p><strong>Ready to turn on</strong></p><ul class="pr-terms"><li>Pull requests open on <span class="mono">${plan.githubRepo}</span> into <span class="mono">${plan.base}</span>${plan.account === null ? "" : `, as ${plan.account}`}.</li><li>Complete offers “Complete and open a pull request”, from the exact completed commit. A flow's Pull request zone can open one too.</li><li>Merging squashes and deletes the branch. A person merges with their password; a flow merges only when its zone says so, after a person approved the card.</li></ul>${password}<button type="submit">Turn on pull requests</button>`, { github: plan.githubRepo, base: plan.base })}`;
|
|
45
30
|
}
|
|
46
31
|
export const PULL_REQUEST_SETTINGS_CSS = `
|
|
47
32
|
.pr-settings { display: grid; gap: .6rem; max-width: 40rem; }
|
package/dist/recipe-ui.d.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { type Recipe, type RecipeDocument, type WorkflowPreview } from "./recipes.js";
|
|
2
|
+
import { type Html } from "./html.js";
|
|
2
3
|
export declare function recipeFromForm(body: URLSearchParams): RecipeDocument;
|
|
3
|
-
export declare function recipeLibraryHtml(starters: Recipe[], saved: Recipe[], project: string | null,
|
|
4
|
+
export declare function recipeLibraryHtml(starters: Recipe[], saved: Recipe[], project: string | null, revision: number, recent: {
|
|
4
5
|
name: string;
|
|
5
6
|
href: string;
|
|
6
7
|
state: string;
|
|
7
|
-
}[]):
|
|
8
|
-
export declare function recipeEditorHtml(recipe: Recipe, repo: string,
|
|
9
|
-
export declare function workflowPreviewHtml(preview: WorkflowPreview,
|
|
8
|
+
}[]): Html;
|
|
9
|
+
export declare function recipeEditorHtml(recipe: Recipe, repo: string, revision: number, problem?: string | null, values?: URLSearchParams, creator?: boolean): Html;
|
|
10
|
+
export declare function workflowPreviewHtml(preview: WorkflowPreview, revision: number, now: Date, readiness: {
|
|
10
11
|
title: string;
|
|
11
12
|
detail: string;
|
|
12
13
|
href?: string;
|
|
13
|
-
}[], auto: boolean):
|
|
14
|
-
export declare function recipeDefinitionPreviewHtml(preview: WorkflowPreview,
|
|
15
|
-
export declare function recipeRunHtml(recipe: Recipe, repo: string,
|
|
14
|
+
}[], auto: boolean): Html;
|
|
15
|
+
export declare function recipeDefinitionPreviewHtml(preview: WorkflowPreview, revision: number): Html;
|
|
16
|
+
export declare function recipeRunHtml(recipe: Recipe, repo: string, revision: number, problem?: string | null, values?: URLSearchParams, saved?: boolean): Html;
|
|
16
17
|
export declare function recipeAnswersFromForm(body: URLSearchParams): Map<string, string>;
|
|
17
18
|
export declare function recipeScript(): string;
|
|
18
19
|
export declare const RECIPE_CSS = "\n.recipe-heading { margin: .5rem 0 1.75rem; max-width: 46rem; }\n.recipe-heading h1 { font-size: clamp(1.65rem,3vw,2.5rem); letter-spacing:-.035em; margin:.35rem 0 .6rem; }\n.recipe-kicker { font-size:.7rem; text-transform:uppercase; letter-spacing:.12em; color:var(--muted-foreground); }\n.recipe-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.8rem; margin:1rem 0 2rem; }\n.recipe-card { border:1px solid var(--border); border-radius:var(--radius); padding:1.1rem; background:var(--card); display:flex; flex-direction:column; gap:.5rem; }\n.recipe-card h3 { font-size:1.05rem; margin:0; letter-spacing:-.015em; }\n.recipe-card p { color:var(--muted-foreground); margin:.2rem 0; font-size:.85rem; line-height:1.5; }\n.recipe-card-link { margin-top:auto; padding-top:.75rem; display:flex; justify-content:space-between; }\n.recipe-editor { max-width:52rem; }.recipe-editor h2 { margin-top:0; }\n.recipe-criterion { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); padding:.8rem; margin:.8rem 0; }\n.recipe-evidence { display:flex; flex-wrap:wrap; gap:.5rem 1rem; }.recipe-evidence label { display:flex; align-items:center; gap:.4rem; font-size:.8rem; }.recipe-evidence input { width:auto; margin:0; }\n.recipe-options { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }\n.recipe-preview-grid { display:grid; grid-template-columns:minmax(0,1.7fr) minmax(16rem,1fr); gap:1rem; }\n.recipe-steps { padding-left:1.4rem; }.recipe-steps li { padding:.4rem 0 .6rem .3rem; }.recipe-steps p { margin:.3rem 0 0; font-size:.85rem; color:var(--muted-foreground); }\n.recipe-launch { display:grid; grid-template-columns:1fr 1fr; gap:1rem; }.recipe-launch form { margin:0 0 .6rem; }\n.recipe-history a { display:flex; justify-content:space-between; gap:1rem; padding:.6rem 0; border-bottom:1px solid var(--border); }.recipe-history span { color:var(--muted-foreground); }\n.recipe-error { border-color:var(--destructive); }\n.recipe-card[hidden] { display:none; }.recipe-search { max-width:28rem; }.recipe-question { border:1px solid var(--border); border-radius:calc(var(--radius) - 3px); margin:1rem 0; padding:.8rem; min-width:0; }.recipe-run { max-width:42rem; }.recipe-run button { margin-top:1rem; }\n@media(max-width:1000px){.recipe-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }}\n@media(max-width:760px){.recipe-grid,.recipe-options,.recipe-preview-grid,.recipe-launch { grid-template-columns:1fr; }.recipe-heading { margin-bottom:1rem; }.recipe-history a { flex-direction:column; gap:.2rem; }}\n";
|
package/dist/recipe-ui.js
CHANGED
|
@@ -2,8 +2,8 @@ import { TASK_TEXT_LIMITS } from "./task-text.js";
|
|
|
2
2
|
import { composerSchedule, scheduleEditorHtml, scheduleEditorScript } from "./task-composer.js";
|
|
3
3
|
import { parseRecipe, workflowSteps, recipeScheduleWords, RecipeError, RECIPE_INPUT_LIMIT } from "./recipes.js";
|
|
4
4
|
import { ACCEPTANCE_LIMITS } from "./scope.js";
|
|
5
|
-
|
|
6
|
-
const hidden = (name, value) => `<input type="hidden" name="${
|
|
5
|
+
import { html, joinHtml, postForm, replaceMarkup } from "./html.js";
|
|
6
|
+
const hidden = (name, value) => html `<input type="hidden" name="${name}" value="${value}">`;
|
|
7
7
|
const evidence = [["check", "Passing tests or commands"], ["changed-path", "Changed files"], ["screenshot", "Screenshot"], ["manual-review", "Reviewer assessment"]];
|
|
8
8
|
export function recipeFromForm(body) {
|
|
9
9
|
for (const name of ["name", "description", "goal", "not", "touches", "planning", "deliverable", "cadence", "repeat", "time", "timezone", "weekday", "interval", "interval-unit", "ceiling", "source", "sourceRevision", "projectRevision", "repo", "preview", "purpose", "sourceFormat"]) {
|
|
@@ -53,63 +53,70 @@ export function recipeFromForm(body) {
|
|
|
53
53
|
return parseRecipe({ format: "standing-orders-recipe", version: inputs.length ? 2 : 1, ...(inputs.length ? { inputs } : {}), name: body.get("name") ?? "", description: body.get("description") ?? "", goal: body.get("goal") ?? "", outOfScope: body.get("not") || null,
|
|
54
54
|
touches: (body.get("touches") ?? "").split(/[\n,]/).map(one => one.trim()).filter(Boolean), acceptance, planning: body.get("planning") ?? "auto", deliverable: body.get("deliverable") ?? "branch", schedule: schedule.schedule, costCeilingUsd: ceiling === "" ? null : Number(ceiling) });
|
|
55
55
|
}
|
|
56
|
-
export function recipeLibraryHtml(starters, saved, project,
|
|
57
|
-
const cards = (recipes, reusable = false) => `<div class="recipe-grid"${reusable
|
|
58
|
-
const savedSection = `<h2>Your project’s recipes</h2>${saved.length >= 100
|
|
59
|
-
return `<div class="recipe-heading"><p class="recipe-kicker">Workflows</p><h1>Good work, on repeat.</h1><p class="hint">Save the work you do often. Answer what changes this time. Kick it off.</p><p><a href="/recipes/new" class="new-task">Create a recipe</a> <a href="/flows">Manage scheduled work →</a></p></div>`
|
|
60
|
-
(project === null ? `<div class="card"><strong>Choose a project to get started.</strong><p>Browse recipes now; <a href="/projects">open a project</a> to customize, save, or run one.</p></div>` : "") +
|
|
61
|
-
(saved.length ? savedSection : "") + `<h2>Start here</h2>${cards(starters)}` + (saved.length ? "" : savedSection) +
|
|
62
|
-
(recent.length ? `<h2>Recent workflows</h2><div class="card recipe-history">${recent.map(one => `<a href="${e(one.href)}"><strong>${e(one.name)}</strong><span>${e(one.state)} →</span></a>`).join("")}</div>` : "") +
|
|
63
|
-
(project === null ? "" : `<details class="card"><summary>Import a shared recipe</summary><p class="hint">Paste exported recipe JSON. You’ll review it in this project before creating any work.</p><form method="post" action="/recipes/import">${hidden("csrf", csrf)}${hidden("repo", project)}${hidden("projectRevision", String(revision))}<label>Recipe JSON<textarea name="document" rows="7" required maxlength="32768" spellcheck="false"></textarea></label><button type="submit">Review imported recipe</button></form></details>`);
|
|
56
|
+
export function recipeLibraryHtml(starters, saved, project, revision, recent) {
|
|
57
|
+
const cards = (recipes, reusable = false) => html `<div class="recipe-grid"${reusable && html ` data-saved-recipes`}>${recipes.map(recipe => html `<article class="recipe-card" data-recipe-card data-recipe-search="${recipe.document.name + " " + recipe.document.description}"><p class="recipe-kicker">${recipe.document.deliverable === "report" ? "Report" : recipe.document.schedule === null ? "One-time work" : "Recurring care"}${recipe.document.version === 2 ? ` · ${recipe.document.inputs.length} question${recipe.document.inputs.length === 1 ? "" : "s"}` : ""}</p><h3>${recipe.document.name}</h3><p>${recipe.document.description}</p><a class="recipe-card-link" href="/recipes/${reusable ? "run" : "start"}?recipe=${recipe.id}&revision=${recipe.revision}">${reusable ? "Use recipe" : "Customize recipe"} <span aria-hidden="true">→</span></a>${reusable && html `<a class="meta" href="/recipes/start?recipe=${recipe.id}&revision=${recipe.revision}">Edit a copy</a>`}</article>`)}</div>`;
|
|
58
|
+
const savedSection = html `<h2>Your project’s recipes</h2>${saved.length >= 100 && html `<p class="meta">Showing 100 recipes, with recently used recipes first.</p>`}${saved.length ? html `<p class="hint">Your usual work, ready to go. Recently used recipes come first.</p><label class="recipe-search">Find a recipe<input type="search" data-recipe-search-input placeholder="Search your recipes…"></label>${cards(saved, true)}<p data-recipe-empty hidden>No recipes match. Try another search.</p>` : html `<p class="hint">Save work you do often. Teammates with project access can use the same recipe.</p>`}`;
|
|
59
|
+
return html `<div class="recipe-heading"><p class="recipe-kicker">Workflows</p><h1>Good work, on repeat.</h1><p class="hint">Save the work you do often. Answer what changes this time. Kick it off.</p><p><a href="/recipes/new" class="new-task">Create a recipe</a> <a href="/flows">Manage scheduled work →</a></p></div>${project === null && html `<div class="card"><strong>Choose a project to get started.</strong><p>Browse recipes now; <a href="/projects">open a project</a> to customize, save, or run one.</p></div>`}${saved.length > 0 && savedSection}<h2>Start here</h2>${cards(starters)}${saved.length === 0 && savedSection}${recent.length > 0 && html `<h2>Recent workflows</h2><div class="card recipe-history">${recent.map(one => html `<a href="${one.href}"><strong>${one.name}</strong><span>${one.state} →</span></a>`)}</div>`}${project !== null && html `<details class="card"><summary>Import a shared recipe</summary><p class="hint">Paste exported recipe JSON. You’ll review it in this project before creating any work.</p>${postForm("/recipes/import", html `<label>Recipe JSON<textarea name="document" rows="7" required maxlength="32768" spellcheck="false"></textarea></label><button type="submit">Review imported recipe</button>`, { projectRevision: String(revision), hidden: { repo: project } })}</details>`}`;
|
|
64
60
|
}
|
|
65
61
|
function criterionHtml(index, criterion, values) {
|
|
66
62
|
const prefix = `criterion-${index}-`;
|
|
67
63
|
const value = (name, fallback) => values?.get(prefix + name) ?? fallback;
|
|
68
64
|
const selected = values ? values.getAll(prefix + "evidence") : criterion?.evidence ?? ["check"];
|
|
69
|
-
return `<fieldset class="recipe-criterion"><legend>${criterion || index === 0 ? `Success check ${index + 1}` : "Additional success check (optional)"}</legend>${hidden(prefix + "id", value("id", criterion?.id ?? `c${index + 1}`))}<label>What should be true?<textarea name="${prefix}statement" rows="2" maxlength="300">${
|
|
65
|
+
return html `<fieldset class="recipe-criterion"><legend>${criterion || index === 0 ? `Success check ${index + 1}` : "Additional success check (optional)"}</legend>${hidden(prefix + "id", value("id", criterion?.id ?? `c${index + 1}`))}<label>What should be true?<textarea name="${prefix}statement" rows="2" maxlength="300">${value("statement", criterion?.statement ?? "")}</textarea></label><div class="recipe-evidence">${evidence.map(([id, label]) => html `<label><input type="checkbox" name="${prefix}evidence" value="${id}"${selected.includes(id) && html ` checked`}>${label}</label>`)}</div><label>How to check it <span class="meta">optional; command or review instructions</span><input type="text" name="${prefix}how" value="${value("how", criterion?.how ?? "")}" maxlength="500"></label></fieldset>`;
|
|
70
66
|
}
|
|
71
|
-
|
|
67
|
+
/** The blank success check the page's script copies, its numbers left for the script to fill in. */
|
|
68
|
+
const criterionTemplate = () => replaceMarkup(criterionHtml(11), /criterion-11-|value="c12"/g, match => match === "criterion-11-" ? html `criterion-INDEX-` : html `value="cNEXT"`);
|
|
69
|
+
export function recipeEditorHtml(recipe, repo, revision, problem = null, values, creator = false) {
|
|
72
70
|
const d = recipe.document;
|
|
73
71
|
const value = (name, fallback) => values?.get(name) ?? fallback;
|
|
74
72
|
const cadence = value("cadence", d.schedule === null ? "once" : "repeat");
|
|
75
73
|
const planning = value("planning", d.planning), deliverable = value("deliverable", d.deliverable);
|
|
76
74
|
const count = values ? Math.min(ACCEPTANCE_LIMITS.criteria, Math.max(d.acceptance.length + 1, ...[...values.keys()].map(key => Number(/^criterion-(\d+)-statement$/.exec(key)?.[1] ?? -1) + 1))) : Math.min(ACCEPTANCE_LIMITS.criteria, d.acceptance.length + 1);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`<section class="card"><h2>What does success look like?</h2><p class="hint">Give each outcome the evidence you want to see. The workflow checks these before calling the work done.</p><div data-recipe-criteria>${Array.from({ length: count }, (_, i) => criterionHtml(i, d.acceptance[i], values)).join("")}</div><button type="button" data-add-criterion hidden>Add another success check</button><template data-criterion-template>${criterionHtml(11).replaceAll("criterion-11-", "criterion-INDEX-").replaceAll('value="c12"', 'value="cNEXT"')}</template></section>` +
|
|
83
|
-
`<section class="card"><h2>How it should run</h2><div class="recipe-options"><label>Result<select name="deliverable"><option value="branch"${deliverable === "branch" ? " selected" : ""}>Code change with evidence</option><option value="report"${deliverable === "report" ? " selected" : ""}>Report without code changes</option></select></label><label>Planning<select name="planning">${[["auto", "Plan when helpful"], ["required", "Always plan first"], ["skip", "Use this scope directly"]].map(([id, label]) => `<option value="${id}"${planning === id ? " selected" : ""}>${label}</option>`).join("")}</select></label></div><p class="meta">Report work investigates directly. Repeating work files the same scope each time; plan-first workflows run once.</p><label>Frequency<select name="cadence" data-recipe-cadence><option value="once"${cadence === "once" ? " selected" : ""}>Run once</option><option value="repeat"${cadence === "repeat" ? " selected" : ""}>Repeat on a schedule</option></select></label><div data-recipe-recurring>${scheduleEditorHtml(d.schedule, values)}<label>Weekly spending limit <span class="meta">optional, USD</span><input type="number" name="ceiling" min="0.01" step="0.01" value="${e(value("ceiling", d.costCeilingUsd === null ? "" : String(d.costCeilingUsd)))}"></label><p class="meta">One at a time. A spending limit needs an agent that reports dollar costs.</p></div></section>` +
|
|
84
|
-
`<div class="sticky-actions"><button type="submit" name="purpose" value="${creator ? "recipe" : "workflow"}">${creator ? "Preview recipe →" : "Preview workflow →"}</button>${creator ? "" : `<button type="submit" name="purpose" value="recipe" class="quiet">Preview recipe to save</button>`}<p class="meta">${creator ? "Save the instructions once. You can use the recipe whenever this work comes up." : "Previewing runs no agents. You’ll see the approval and scheduling steps next."}</p></div></form>`;
|
|
75
|
+
const outcome = html `<section class="card"><h2>The outcome</h2><label>${creator ? "Recipe name" : "Workflow name"}<input type="text" name="name" value="${value("name", d.name)}" maxlength="200" required></label><label>Short description<input type="text" name="description" value="${value("description", d.description)}" maxlength="400"></label><label>What should the agent do?<textarea name="goal" rows="5" maxlength="${TASK_TEXT_LIMITS.text}" required>${value("goal", d.goal)}</textarea></label><label>What should it leave alone?<textarea name="not" rows="3" maxlength="${TASK_TEXT_LIMITS.text}">${value("not", d.outOfScope ?? "")}</textarea></label><label>Allowed files or folders <span class="meta">optional; one per line</span><textarea name="touches" rows="2" placeholder="src/ tests/">${value("touches", d.touches.join("\n"))}</textarea></label><p class="meta">Blank allows work across this project. Add paths to keep the workflow focused.</p></section>`;
|
|
76
|
+
const success = html `<section class="card"><h2>What does success look like?</h2><p class="hint">Give each outcome the evidence you want to see. The workflow checks these before calling the work done.</p><div data-recipe-criteria>${Array.from({ length: count }, (_, i) => criterionHtml(i, d.acceptance[i], values))}</div><button type="button" data-add-criterion hidden>Add another success check</button><template data-criterion-template>${criterionTemplate()}</template></section>`;
|
|
77
|
+
const running = html `<section class="card"><h2>How it should run</h2><div class="recipe-options"><label>Result<select name="deliverable"><option value="branch"${deliverable === "branch" && html ` selected`}>Code change with evidence</option><option value="report"${deliverable === "report" && html ` selected`}>Report without code changes</option></select></label><label>Planning<select name="planning">${[["auto", "Plan when helpful"], ["required", "Always plan first"], ["skip", "Use this scope directly"]].map(([id, label]) => html `<option value="${id}"${planning === id && html ` selected`}>${label}</option>`)}</select></label></div><p class="meta">Report work investigates directly. Repeating work files the same scope each time; plan-first workflows run once.</p><label>Frequency<select name="cadence" data-recipe-cadence><option value="once"${cadence === "once" && html ` selected`}>Run once</option><option value="repeat"${cadence === "repeat" && html ` selected`}>Repeat on a schedule</option></select></label><div data-recipe-recurring>${scheduleEditorHtml(d.schedule, values)}<label>Weekly spending limit <span class="meta">optional, USD</span><input type="number" name="ceiling" min="0.01" step="0.01" value="${value("ceiling", d.costCeilingUsd === null ? "" : String(d.costCeilingUsd))}"></label><p class="meta">One instance at a time. A spending limit requires an agent that reports dollar costs; approval checks this.</p></div></section>`;
|
|
78
|
+
const actions = html `<div class="sticky-actions"><button type="submit" name="purpose" value="${creator ? "recipe" : "workflow"}">${creator ? "Preview recipe →" : "Preview workflow →"}</button>${!creator && html `<button type="submit" name="purpose" value="recipe" class="quiet">Preview recipe to save</button>`}<p class="meta">${creator ? "Save the instructions once. You can use the recipe whenever this work comes up." : "Previewing runs no agents. You’ll see the approval and scheduling steps next."}</p></div>`;
|
|
79
|
+
return html `<a href="/recipes" class="meta">← All recipes</a><div class="recipe-heading"><p class="recipe-kicker">${creator ? "1 · Describe → 2 · Preview → 3 · Save" : "1 · Choose → 2 · Customize → 3 · Preview"}</p><h1>${creator ? "Create a reusable recipe" : "Make this workflow yours"}</h1><p class="hint">${repo.split(/[\\/]/).pop() || repo} · Choose the outcome, boundaries, and proof you want back.</p></div>${problem !== null && html `<p role="alert" class="card recipe-error">${problem}</p>`}${postForm("/recipes/preview", html `${outcome}${recipeQuestionsHtml(d, values)}${success}${running}${actions}`, { attrs: { class: "recipe-editor" }, projectRevision: String(revision), hidden: { repo, source: recipe.id, sourceRevision: String(recipe.revision), sourceFormat: value("sourceFormat", String(d.version)) } })}`;
|
|
85
80
|
}
|
|
86
|
-
|
|
81
|
+
/** What a success check's evidence is called, for a reader. */
|
|
82
|
+
const evidenceWords = (kinds) => joinHtml(kinds.map(kind => evidence.find(([id]) => id === kind)?.[1] ?? kind), " · ");
|
|
83
|
+
export function workflowPreviewHtml(preview, revision, now, readiness, auto) {
|
|
87
84
|
const d = preview.document;
|
|
88
|
-
const common =
|
|
85
|
+
const common = { projectRevision: String(revision), hidden: { repo: preview.repo, preview: preview.token } };
|
|
89
86
|
const target = preview.taskId === null ? preview.flowId === null ? null : `/flows/${preview.flowId}` : `/t/${encodeURIComponent(preview.taskId)}`;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
87
|
+
const agreed = html `<section class="card"><h2>The agreed work</h2><p class="recap">${d.goal}</p><h3>Leave alone</h3><p class="recap">${d.outOfScope ?? "No additional exclusions."}</p><h3>Allowed paths</h3><p>${d.touches.length ? joinHtml(d.touches, ", ") : "This project"}</p><h3>Success checks</h3><ul>${d.acceptance.map(c => html `<li><strong>${c.statement}</strong><p class="meta">${evidenceWords(c.evidence)}${c.how ? ` · ${c.how}` : ""}</p></li>`)}</ul></section>`;
|
|
88
|
+
const before = html `<aside><section class="card"><h2>Before it starts</h2>${readiness.map(one => html `<p><strong>${one.title}</strong><br><span class="meta">${one.detail}${one.href && html ` <a href="${one.href}">Open settings →</a>`}</span></p>`)}<p class="meta">These checks describe current setup. Agent availability and approval are checked again when work starts.</p></section><section class="card"><h2>Schedule & control</h2><p>${recipeScheduleWords(d, now)}</p><p>${d.costCeilingUsd === null ? "Uses existing project and agent budget controls." : `Up to $${d.costCeilingUsd.toFixed(2)} per rolling seven days.`}</p><p>${d.schedule !== null ? "Approve the routine once on the next screen. Pause it any time from scheduled work." : auto ? "Your signed project policy will be evaluated when you create this workflow." : "Review and approve on the task page. You can stop or resume there."}</p></section></aside>`;
|
|
89
|
+
const launch = html `<div class="card recipe-launch"><div>${target ? html `<a class="new-task" href="${target}">Open created workflow →</a>` : postForm("/recipes/launch", html `<button type="submit">${d.schedule !== null ? "Create scheduled workflow" : auto ? "Create workflow" : "Create task for approval"}</button>`, common)}<p class="meta">${d.schedule !== null ? "Nothing repeats until you turn the schedule on." : "A matching signed policy may start work automatically."} Creating this workflow twice returns the same work.</p></div><div>${preview.savedId ? html `<a href="/recipes/start?recipe=${preview.savedId}&revision=${preview.savedRevision}">Recipe saved · reuse it →</a>` : postForm("/recipes/save", html `<button type="submit" class="quiet">Save as a project recipe</button>`, common)}<a href="/recipes/export?preview=${preview.token}">Export recipe JSON</a><p class="meta">Save or export without running anything. Sharing includes the work definition, never project access or approvals.</p></div></div>`;
|
|
90
|
+
return html `<a href="/recipes/edit?preview=${preview.token}" class="meta">← Edit this workflow</a><div class="recipe-heading"><p class="recipe-kicker">3 · Preview</p><h1>${d.name}</h1><p class="hint">${d.description}</p></div><div class="recipe-preview-grid"><div><section class="card"><h2>What will happen</h2><ol class="recipe-steps">${workflowSteps(d).map(step => html `<li><strong>${step.title}</strong><p>${step.detail}</p></li>`)}</ol></section>${agreed}</div>${before}</div>${launch}`;
|
|
94
91
|
}
|
|
95
92
|
function inputHtml(index, input, values) {
|
|
96
93
|
const value = (field, fallback) => values?.get(`input-${index}-${field}`) ?? fallback;
|
|
97
|
-
return `<fieldset class="recipe-question"><legend>Question ${index + 1}</legend><div class="recipe-options"><label>Short key<input type="text" name="input-${index}-key" value="${
|
|
94
|
+
return html `<fieldset class="recipe-question"><legend>Question ${index + 1}</legend><div class="recipe-options"><label>Short key<input type="text" name="input-${index}-key" value="${value("key", input?.key ?? "")}" maxlength="32" placeholder="module" pattern="[a-z][a-z0-9_]{0,31}"></label><label>What should we ask?<input type="text" name="input-${index}-label" value="${value("label", input?.label ?? "")}" maxlength="80" placeholder="Which module should we work on?"></label></div><label>Default answer <span class="meta">optional</span><input type="text" name="input-${index}-default" value="${value("default", input?.defaultValue ?? "")}" maxlength="500"></label><p class="meta">Use <code data-question-token>{{${value("key", input?.key ?? "module") || "module"}}}</code> in your instructions or a success check. Clear this row and its placeholders to remove the question.</p><button type="button" data-insert-question hidden>Insert into instructions</button></fieldset>`;
|
|
98
95
|
}
|
|
96
|
+
/** The blank question the page's script copies, its numbers left for the script to fill in (the first "Question 8" only, as before). */
|
|
97
|
+
const questionTemplate = () => {
|
|
98
|
+
let numbered = false;
|
|
99
|
+
return replaceMarkup(inputHtml(7), /input-7-|Question 8/g, match => match === "input-7-" ? html `input-INDEX-` : numbered ? null : (numbered = true, html `Question NEXT`));
|
|
100
|
+
};
|
|
99
101
|
function recipeQuestionsHtml(document, values) {
|
|
100
102
|
const inputs = document.version === 2 ? document.inputs : [];
|
|
101
103
|
const submitted = values ? [...values.keys()].filter(key => /^input-[0-7]-/.test(key)).map(key => Number(key.split("-")[1]) + 1) : [];
|
|
102
104
|
const count = Math.min(RECIPE_INPUT_LIMIT, Math.max(1, inputs.length, ...submitted));
|
|
103
|
-
return `<details class="card"${inputs.length || submitted.some(i => values?.get(`input-${i - 1}-key`))
|
|
105
|
+
return html `<details class="card"${(inputs.length > 0 || submitted.some(i => values?.get(`input-${i - 1}-key`))) && html ` open`}><summary>Ask something each time <span class="meta">optional</span></summary><p class="hint">For example: “Test {{module}} and cover {{scenario}}.” Add a question for each placeholder. People answer these when they use the recipe; the saved instructions stay the same.</p><p class="meta">Use answers in the name, instructions, exclusions, or success checks. Keep allowed paths and check commands fixed. Don’t ask for passwords or keys.</p><div data-recipe-questions>${Array.from({ length: count }, (_, i) => inputHtml(i, inputs[i], values))}</div><button type="button" data-add-question hidden>Add a question</button><template data-question-template>${questionTemplate()}</template></details>`;
|
|
104
106
|
}
|
|
105
|
-
export function recipeDefinitionPreviewHtml(preview,
|
|
107
|
+
export function recipeDefinitionPreviewHtml(preview, revision) {
|
|
106
108
|
const d = preview.document;
|
|
107
109
|
const inputs = d.version === 2 ? d.inputs : [];
|
|
108
|
-
|
|
110
|
+
const uses = html `<section class="card"><h2>When someone uses this recipe</h2>${inputs.length ? html `<ol>${inputs.map(input => html `<li><strong>${input.label}</strong><p class="meta">{{${input.key}}}${input.defaultValue ? ` · Default: ${input.defaultValue}` : " · Answer required each time"}</p></li>`)}</ol>` : html `<p>No questions to answer. The saved scope is ready to preview and use.</p>`}<p>Each use previews the exact work before creating it. The project’s approval and recovery settings still apply.</p></section>`;
|
|
111
|
+
const instructions = html `<section class="card"><h2>Saved instructions</h2><p class="recap">${d.goal}</p><h3>Leave alone</h3><p class="recap">${d.outOfScope ?? "No additional exclusions."}</p><h3>Allowed paths</h3><p>${d.touches.length ? joinHtml(d.touches, ", ") : "This project"}</p><h3>Success checks</h3><ul>${d.acceptance.map(one => html `<li>${one.statement}<p class="meta">${evidenceWords(one.evidence)}${one.how ? ` · ${one.how}` : ""}</p></li>`)}</ul><p class="meta">${d.deliverable === "report" ? "Report" : "Code change"} · ${d.schedule === null ? "Run once" : d.schedule} · ${d.planning === "required" ? "Plan first" : d.planning === "auto" ? "Plan when helpful" : "Use the approved scope directly"}${d.costCeilingUsd === null ? "" : ` · $${d.costCeilingUsd} weekly limit`}</p></section>`;
|
|
112
|
+
const save = preview.savedId ? html `<a class="new-task" href="/recipes/run?recipe=${preview.savedId}&revision=${preview.savedRevision}">Use recipe →</a>`
|
|
113
|
+
: postForm("/recipes/save", html `<button type="submit">Save recipe</button>`, { projectRevision: String(revision), hidden: { repo: preview.repo, preview: preview.token, next: "use" } });
|
|
114
|
+
return html `<a class="meta" href="/recipes/edit?preview=${preview.token}&purpose=recipe">← Edit recipe</a><div class="recipe-heading"><p class="recipe-kicker">2 · Preview your recipe</p><h1>${d.name}</h1><p class="hint">${d.description || "A reusable starting point for work you do often."}</p></div>${uses}${instructions}<section class="card recipe-launch"><div>${save}<p class="meta">Saving creates no work. Everyone with project access can reuse this copy.</p></div><a href="/recipes/export?preview=${preview.token}">Export recipe JSON</a></section>`;
|
|
109
115
|
}
|
|
110
|
-
export function recipeRunHtml(recipe, repo,
|
|
116
|
+
export function recipeRunHtml(recipe, repo, revision, problem = null, values, saved = false) {
|
|
111
117
|
const d = recipe.document, inputs = d.version === 2 ? d.inputs : [];
|
|
112
|
-
|
|
118
|
+
const run = postForm("/recipes/prepare", html `<h2>${inputs.length ? "What changes this time?" : "Ready to go"}</h2>${inputs.map(input => html `<label>${input.label}<textarea name="answer-${input.key}" rows="2" maxlength="500" required>${values?.get(`answer-${input.key}`) ?? input.defaultValue ?? ""}</textarea></label>`)}${inputs.length === 0 && html `<p>No setup to repeat. Preview the saved scope and create the work.</p>`}<p class="meta">${d.schedule === null ? "Creates one task each time you use this recipe." : "Creates a new schedule with these answers fixed for every firing. Manage an existing schedule from Routines."}</p><button type="submit">Preview this run →</button><p class="meta">Previewing starts no agents. Existing project approvals apply when you create work.</p>`, { attrs: { class: "recipe-run card" }, projectRevision: String(revision), hidden: { repo, recipe: recipe.id, recipeRevision: String(recipe.revision) } });
|
|
119
|
+
return html `<a class="meta" href="/recipes">← Your recipes</a><div class="recipe-heading"><p class="recipe-kicker">${saved ? "Recipe saved · ready when you are" : "Use a recipe"}</p><h1>${d.name}</h1><p class="hint">${d.description || "Your saved instructions, ready to use again."}</p></div>${problem && html `<p role="alert" class="card recipe-error">${problem}</p>`}${run}<details class="card"><summary>Saved instructions and boundaries</summary><p class="recap">${d.goal}</p><p class="recap">Leave alone: ${d.outOfScope ?? "No additional exclusions."}</p><p>Allowed paths: ${d.touches.length ? joinHtml(d.touches, ", ") : "This project"}</p><ul>${d.acceptance.map(one => html `<li>${one.statement}</li>`)}</ul><a href="/recipes/start?recipe=${recipe.id}&revision=${recipe.revision}">Edit a copy of this recipe →</a></details>`;
|
|
113
120
|
}
|
|
114
121
|
export function recipeAnswersFromForm(body) {
|
|
115
122
|
const answers = new Map();
|