toolroll 0.9.86 → 0.9.88
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 +86 -0
- package/dist/acp-check.d.ts +9 -0
- package/dist/acp-check.js +40 -28
- 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/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 -70
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +329 -183
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- 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/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/design/kit.css +722 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +31 -1
- package/dist/discord-settings.js +8 -4
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- 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.js +7 -3
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +141 -30
- 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/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 +38 -4
- 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-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- 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 +23 -0
- package/dist/release.js +46 -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/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +3 -0
- package/dist/scope.js +6 -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 +16 -16
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +24 -12
- package/dist/server/flows.js +64 -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 +70 -0
- package/dist/server/kit.d.ts +419 -0
- package/dist/server/kit.js +369 -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 +22 -190
- package/dist/server/render-pages.js +123 -575
- 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 +35 -5
- package/dist/server/render-tasks.js +114 -85
- package/dist/server/route-table.js +20 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +85 -2
- package/dist/server/tasks.d.ts +8 -0
- package/dist/server/tasks.js +95 -40
- 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/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 +112 -3
- package/dist/store.js +292 -20
- package/dist/surface.js +5 -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/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.js +11 -7
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +7 -4
- package/package.json +1 -1
package/dist/policy-ui.d.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { type Html } from "./html.js";
|
|
8
8
|
import type { LedgerEntry } from "./action-ledger.js";
|
|
9
9
|
import { type SavedPolicy } from "./policy.js";
|
|
10
|
-
|
|
10
|
+
/** The old look; the page is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
11
|
+
export declare const POLICY_CSS = "";
|
|
11
12
|
export type PolicyView = {
|
|
12
13
|
policy: SavedPolicy;
|
|
13
14
|
history: LedgerEntry[];
|
|
@@ -15,7 +16,7 @@ export type PolicyView = {
|
|
|
15
16
|
/** The project tools here, as a hint for the tools list. */
|
|
16
17
|
toolNames: string[];
|
|
17
18
|
};
|
|
18
|
-
export declare function policyHtml(view: PolicyView,
|
|
19
|
+
export declare function policyHtml(view: PolicyView, outcome: {
|
|
19
20
|
said?: string | null;
|
|
20
21
|
problem?: string | null;
|
|
21
22
|
}): Html;
|
package/dist/policy-ui.js
CHANGED
|
@@ -4,33 +4,39 @@
|
|
|
4
4
|
* action ledger. Anyone signed in reads it; an instance operator changes it
|
|
5
5
|
* with their password.
|
|
6
6
|
*/
|
|
7
|
-
import { html,
|
|
7
|
+
import { html, joinHtml } from "./html.js";
|
|
8
8
|
import { whenUtc } from "./when-html.js";
|
|
9
9
|
import { LEVEL_HINTS, LEVEL_NAMES, PERMISSION_LEVELS, POLICY_PROVIDERS, PROVIDER_NAMES, policyParts } from "./policy.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
`.policy textarea{width:100%;min-height:72px;font-family:var(--font-mono);font-size:.8125rem;box-sizing:border-box}.policy .step-up{margin-top:18px}.policy button[type=submit]{margin-top:14px}` +
|
|
16
|
-
`.policy details{margin-top:28px}.policy summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600;font-size:.875rem}` +
|
|
17
|
-
`.policy ol{list-style:none;padding:0;margin:0}.policy li{padding:10px 0;border-bottom:1px solid var(--border);font-size:.8125rem;overflow-wrap:anywhere}.policy li .what{display:block;font-size:.875rem}`;
|
|
18
|
-
/** A ledger entry as one line of history: when, who, what, before → after. */
|
|
10
|
+
import { disclose, facts, formRow, infoLine, note, notice, radios, settingsRecord, settingsSection, textarea } from "./server/kit.js";
|
|
11
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
12
|
+
/** The old look; the page is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
13
|
+
export const POLICY_CSS = "";
|
|
14
|
+
/** A ledger entry as one record of history: what, before → after; who; when. */
|
|
19
15
|
function historyItem(entry) {
|
|
20
16
|
const what = entry.action.replace(/^organisation policy: /, "");
|
|
21
17
|
const [before, after] = (entry.detail ?? "").split(" → ");
|
|
22
|
-
return html
|
|
18
|
+
return settingsRecord({ title: html `${what.charAt(0).toUpperCase() + what.slice(1)}: ${before ?? ""} → <b>${after ?? ""}</b>`, sub: entry.actor, when: whenUtc(entry.at) });
|
|
23
19
|
}
|
|
24
|
-
export function policyHtml(view,
|
|
25
|
-
const
|
|
20
|
+
export function policyHtml(view, outcome) {
|
|
21
|
+
const answer = outcome.problem ? notice(outcome.problem) : outcome.said ? infoLine(outcome.said) : "";
|
|
26
22
|
const parts = policyParts(view.policy);
|
|
27
|
-
const changed = view.policy.updatedBy === null ? "" :
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (!view.canChange)
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
23
|
+
const changed = view.policy.updatedBy === null ? "" : ` · changed by ${view.policy.updatedBy} on ${(view.policy.updatedAt ?? "").slice(0, 10)}`;
|
|
24
|
+
const history = view.history.length === 0 ? "" : settingsSection({ id: "policy-history", title: "History",
|
|
25
|
+
body: disclose(`${view.history.length} ${view.history.length === 1 ? "change" : "changes"}`, joinHtml(view.history.map(historyItem))) });
|
|
26
|
+
if (!view.canChange) {
|
|
27
|
+
return joinHtml([answer, settingsSection({ id: "policy", title: "Organisation policy", state: changed, lead: "What every task may use, in every project.",
|
|
28
|
+
body: joinHtml([facts([["Providers", parts.providers === "any" ? "Any" : parts.providers], ["Models", parts.models === "any" ? "Any" : parts.models],
|
|
29
|
+
["Tools", parts.tools === "any" ? "Any" : parts.tools], ["Ceiling", parts.ceiling]]), note("An instance operator sets the policy.")]) }), history]);
|
|
30
|
+
}
|
|
31
|
+
const providers = html `<div class="k-checks" role="group" aria-label="Providers">${POLICY_PROVIDERS.map(one => html `<label for="policy-provider-${one}"><input type="checkbox" id="policy-provider-${one}" name="provider" value="${one}"${view.policy.providers === null || view.policy.providers.includes(one) ? html ` checked` : ""}> ${PROVIDER_NAMES[one]}</label>`)}</div>`;
|
|
34
32
|
const toolHint = view.toolNames.length === 0 ? "" : ` Tools here: ${view.toolNames.slice(0, 12).join(", ")}${view.toolNames.length > 12 ? "…" : ""}.`;
|
|
35
|
-
return
|
|
33
|
+
return joinHtml([answer, settingsSection({ id: "policy", title: "Organisation policy", state: changed, lead: "What every task may use, in every project.",
|
|
34
|
+
body: kitSectionForm({ proof: "policy", action: "/settings/policy", id: "policy-form", gate: true, save: { label: "Save policy" }, confirm: "Change the policy",
|
|
35
|
+
body: joinHtml([
|
|
36
|
+
formRow("Providers", "Work runs only on the ticked ones. All of them allows any.", providers, "wide"),
|
|
37
|
+
formRow("Models", "One per line; end with * to allow a family. Blank allows any.", textarea({ id: "policy-models", name: "models", value: (view.policy.models ?? []).join("\n"), rows: 3, attrs: { spellcheck: "false", placeholder: "Any model", "aria-label": "Models" } }), "top"),
|
|
38
|
+
formRow("Project tools", `One per line. Blank allows any.${toolHint}`, textarea({ id: "policy-tools", name: "tools", value: (view.policy.tools ?? []).join("\n"), rows: 3, attrs: { spellcheck: "false", placeholder: "Any tool", "aria-label": "Project tools" } }), "top"),
|
|
39
|
+
formRow("Permission ceiling", "The most any task may be allowed to do.", radios({ name: "ceiling", legend: "Permission ceiling", current: view.policy.ceiling,
|
|
40
|
+
items: [...PERMISSION_LEVELS].reverse().map(level => ({ value: level, label: LEVEL_NAMES[level], text: LEVEL_HINTS[level] })) }), "wide"),
|
|
41
|
+
]) }) }), history]);
|
|
36
42
|
}
|
package/dist/policy.d.ts
CHANGED
|
@@ -87,3 +87,6 @@ export declare function checkPolicy(input: {
|
|
|
87
87
|
ok: false;
|
|
88
88
|
problem: string;
|
|
89
89
|
};
|
|
90
|
+
/** The providers ticked on the policy form: every provider the policy knows ticked (and nothing unknown) means "any"
|
|
91
|
+
* (null), so a provider added later is allowed too; anything else is the list as ticked, for checkPolicy to check. */
|
|
92
|
+
export declare function chosenProviders(chosen: readonly string[]): string[] | null;
|
package/dist/policy.js
CHANGED
|
@@ -224,7 +224,7 @@ export function checkPolicy(input) {
|
|
|
224
224
|
if (input.providers !== null) {
|
|
225
225
|
const unknown = input.providers.filter(one => policyProvider(one.toLowerCase()) === null);
|
|
226
226
|
if (unknown.length > 0)
|
|
227
|
-
return { ok: false, problem: `${unknown.join(", ")} isn't a provider here. Choose from
|
|
227
|
+
return { ok: false, problem: `${unknown.join(", ")} isn't a provider here. Choose from ${providerChoices()}.` };
|
|
228
228
|
providers = POLICY_PROVIDERS.filter(one => input.providers.some(given => policyProvider(given.toLowerCase()) === one));
|
|
229
229
|
if (providers.length === 0)
|
|
230
230
|
return { ok: false, problem: "Allow at least one provider, or allow any." };
|
|
@@ -248,3 +248,16 @@ export function checkPolicy(input) {
|
|
|
248
248
|
return { ok: false, problem: tools.problem };
|
|
249
249
|
return { ok: true, policy: { providers, models, tools, ceiling: input.ceiling } };
|
|
250
250
|
}
|
|
251
|
+
/** Every provider the policy knows, in plain names: "Claude, Codex, … and Goose". */
|
|
252
|
+
function providerChoices() {
|
|
253
|
+
const names = POLICY_PROVIDERS.map(one => PROVIDER_NAMES[one]);
|
|
254
|
+
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
255
|
+
}
|
|
256
|
+
/** The providers ticked on the policy form: every provider the policy knows ticked (and nothing unknown) means "any"
|
|
257
|
+
* (null), so a provider added later is allowed too; anything else is the list as ticked, for checkPolicy to check. */
|
|
258
|
+
export function chosenProviders(chosen) {
|
|
259
|
+
const known = new Set(chosen.map(one => policyProvider(one.toLowerCase())));
|
|
260
|
+
if (known.has(null))
|
|
261
|
+
return [...chosen];
|
|
262
|
+
return POLICY_PROVIDERS.every(one => known.has(one)) ? null : [...chosen];
|
|
263
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { WritesMode } from "./writes-mode.js";
|
|
|
4
4
|
import { type CheckAccess } from "./check-access.js";
|
|
5
5
|
import { type Html } from "./html.js";
|
|
6
6
|
import { type ProjectHoldings } from "./project-delete.js";
|
|
7
|
+
/** Check access: a wider level's warning shows only under the option chosen. Everything else on the page is kit markup. */
|
|
7
8
|
export declare const PROJECT_DELETE_CSS: string;
|
|
8
9
|
export type ProjectSettingsView = {
|
|
9
10
|
remoteUrl?: string | null;
|
|
@@ -68,8 +69,9 @@ export type ProjectSettingsView = {
|
|
|
68
69
|
signInsUnavailable?: string | null;
|
|
69
70
|
};
|
|
70
71
|
};
|
|
71
|
-
/** The project's page: its path, what Toolroll holds for it, and (for an instance operator)
|
|
72
|
-
|
|
72
|
+
/** The project's page: its name and path, what Toolroll holds for it, its build settings, and (for an instance operator)
|
|
73
|
+
* Remove and Delete. */
|
|
74
|
+
export declare function projectSettingsHtml(view: ProjectSettingsView, said: {
|
|
73
75
|
said?: string | null;
|
|
74
76
|
problem?: string | null;
|
|
75
77
|
}): Html;
|
|
@@ -6,55 +6,34 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { sandboxRuntimeWords } from "./sandbox-runtime.js";
|
|
8
8
|
import { CHECK_ACCESS_WARNINGS, signInHostsMissing } from "./check-access.js";
|
|
9
|
-
import { html,
|
|
9
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
10
10
|
import { holdingsWords } from "./project-delete.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`.project-settings .build-access .tool-name{font:var(--so-typography-mono)}.project-settings .build-access details{margin:4px 0 0}.project-settings .build-access summary{min-height:44px;display:inline-flex;align-items:center;cursor:pointer;color:var(--so-muted);font:var(--so-typography-label)}` +
|
|
22
|
-
`.project-settings .build-access ul{margin:0 0 4px;padding-left:20px;font:var(--so-typography-body);color:var(--so-ink)}.project-settings .build-access li{margin:2px 0}.project-settings .build-access li .meta{color:var(--so-muted)}` +
|
|
23
|
-
`.project-settings .build-access a.manage-tools{display:inline-flex;align-items:center;justify-content:center;min-height:44px;margin-top:4px;padding:0 14px;white-space:nowrap;background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line);border-radius:var(--so-radius-md);font:var(--so-typography-label);text-decoration:none}` +
|
|
24
|
-
// A radio or checkbox choice sits beside its words, at its natural size, not as a full-width grid row.
|
|
25
|
-
`.project-settings .build-access label.check{display:flex;align-items:center;gap:8px;min-height:44px;font:var(--so-typography-body);font-weight:500}` +
|
|
26
|
-
`.project-settings .build-access label.check input{width:auto;min-height:0;margin:0;flex:none}.project-settings .build-access form .meta{margin:-8px 0 0 26px}` +
|
|
27
|
-
// Check access: a warning shows only under the option chosen; the folds draw the console's own chevron (as a diff's).
|
|
28
|
-
`.project-settings .check-access-setting .option .warning{display:none;color:var(--so-ink)}.project-settings .check-access-setting .option:has(input:checked) .warning{display:block}` +
|
|
29
|
-
`.project-settings .check-access-setting form{margin-top:0;gap:10px}.project-settings .check-access-setting .option{display:grid;gap:2px}` +
|
|
30
|
-
`.project-settings .check-access-setting .option .meta{margin:0 0 0 26px}.project-settings .check-access-setting label.check{min-height:36px}` +
|
|
31
|
-
`.project-settings .check-access-setting details > summary{list-style:none;display:flex;align-items:center;gap:10px;min-height:40px;padding:0}` +
|
|
32
|
-
`.project-settings .check-access-setting details > summary::-webkit-details-marker{display:none}.project-settings .check-access-setting details > summary::marker{content:""}` +
|
|
33
|
-
`.project-settings .check-access-setting details > summary::before{content:"";flex:none;width:.35rem;height:.35rem;border-right:1.5px solid var(--so-muted);border-bottom:1.5px solid var(--so-muted);transform:rotate(-45deg);transition:transform .15s ease}` +
|
|
34
|
-
`.project-settings .check-access-setting details[open] > summary::before{transform:rotate(45deg) translateY(-.1rem)}` +
|
|
35
|
-
// Show hosts: a quiet fold in the line, not a card.
|
|
36
|
-
`.project-settings .check-access-setting details.hosts{border:0;background:none;padding:0;margin:0}.project-settings .check-access-setting details.hosts > summary{min-height:32px;font-size:.8125rem}` +
|
|
37
|
-
`.project-settings .check-access-setting details.hosts .tool-name{margin:0 0 4px;font-size:.75rem}` +
|
|
38
|
-
`.project-settings .check-access-setting button.primary{background:var(--so-signal);color:var(--so-on-signal);border-color:var(--so-signal)}` +
|
|
39
|
-
`.project-settings .build-access a.manage-tools:hover{background:var(--so-raised)}.project-settings .build-access a.manage-tools:focus-visible,.project-settings .build-access summary:focus-visible{outline:2px solid var(--so-ink);outline-offset:2px}`;
|
|
40
|
-
/** Where builds may write, said plainly; an approver changes it with their password. */
|
|
11
|
+
import { acts, btn, codeLine, disclose, formRow, infoLine, input, note, notice, radios, settingsSection, switchInput, textarea } from "./server/kit.js";
|
|
12
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
13
|
+
/** Check access: a wider level's warning shows only under the option chosen. Everything else on the page is kit markup. */
|
|
14
|
+
export const PROJECT_DELETE_CSS = ".k-opt [data-check-access-warning]{display:none;color:var(--so-ink)}.k-opt:has(input:checked) [data-check-access-warning]{display:block}" +
|
|
15
|
+
".k-opt:has(input:disabled){cursor:default;opacity:.6}";
|
|
16
|
+
const mono = (text) => html `<span class="k-mono">${text}</span>`;
|
|
17
|
+
const list = (items) => html `<ul class="k-note">${items.map(one => html `<li>${one}</li>`)}</ul>`;
|
|
18
|
+
const SCOPED = "Tasks scoped from now on use this; tasks already approved keep what they were approved with.";
|
|
19
|
+
const APPROVER_CHANGES = note("An approver for this project can change this.");
|
|
20
|
+
/** Where builds may write, said plainly; an approver changes it with their password (always typed: it widens). */
|
|
41
21
|
function writesHtml(view) {
|
|
42
22
|
const writes = view.writes;
|
|
43
23
|
if (writes === undefined)
|
|
44
24
|
return "";
|
|
45
25
|
const said = writes.setting === "workspace"
|
|
46
|
-
?
|
|
47
|
-
:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return html `<section class="build-access writes-setting" id="writes" aria-labelledby="writes-title"><h2 id="writes-title">Where builds write</h2>${said}<details><summary>Change</summary>${form}</details></section>`;
|
|
26
|
+
? "Builds write only their own checkout, its caches and its temp folder. Your shell profile, login items, agent settings and the repository's Git config and hooks stay as they are."
|
|
27
|
+
: "Builds may write anywhere outside Toolroll's own folders, your credentials and other projects.";
|
|
28
|
+
return settingsSection({ id: "writes", title: "Where builds write", state: writes.setting === "workspace" ? " · Only their workspace" : " · Anywhere", lead: said,
|
|
29
|
+
body: !writes.canChange ? APPROVER_CHANGES : disclose("Change", kitSectionForm({ proof: "project-writes", action: "/settings/project/writes", id: "writes-form", gate: true, hidden: { repo: view.repo },
|
|
30
|
+
body: joinHtml([
|
|
31
|
+
formRow("Builds may write", null, radios({ name: "writes", legend: "Where builds write", current: writes.setting, items: [
|
|
32
|
+
{ value: "workspace", label: "Only their workspace (recommended)", text: "The checkout and its Git data, the project's own package caches, the build's temp folder, and what its agent needs to keep its session." },
|
|
33
|
+
{ value: "anywhere", label: "Anywhere outside Toolroll's fenced folders", text: "For a project whose tools must write elsewhere in your home folder or the shared temp folder. Credentials, Toolroll's own folders and other projects stay fenced." }
|
|
34
|
+
] }), "wide"),
|
|
35
|
+
note(SCOPED),
|
|
36
|
+
]) })) });
|
|
58
37
|
}
|
|
59
38
|
/** Home folder (project-sandbox.ts): what builds and checks can read there, said once; the paths they can't, and the
|
|
60
39
|
* advanced list (macOS services), changed under Change with the approver's password. */
|
|
@@ -64,24 +43,20 @@ function sandboxHtml(view) {
|
|
|
64
43
|
return "";
|
|
65
44
|
const { readDeny, machLookup } = sandbox.setting;
|
|
66
45
|
const said = readDeny.length === 0
|
|
67
|
-
?
|
|
68
|
-
:
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<p class="meta">Tasks scoped from now on use this; tasks already approved keep what they were approved with.</p>
|
|
82
|
-
${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}
|
|
83
|
-
<button type="submit">Save</button>`, { hidden: { repo: view.repo } });
|
|
84
|
-
return html `<section class="build-access sandbox-setting" id="sandbox" aria-labelledby="sandbox-title">${head}<details><summary>Change</summary>${form}</details></section>`;
|
|
46
|
+
? "Builds and checks can read your home folder, as in Anthropic's sandbox runtime. Toolroll's own folders stay out of reach; other tasks' work is read, never written."
|
|
47
|
+
: `Builds and checks can read your home folder except ${readDeny.length === 1 ? readDeny[0] : `${readDeny.length} paths`}.`;
|
|
48
|
+
const head = joinHtml([
|
|
49
|
+
machLookup.length === 0 ? "" : note(`${machLookup.length} macOS ${machLookup.length === 1 ? "service" : "services"} allowed`),
|
|
50
|
+
// The engine, named where the sandbox is set (sandbox-runtime.ts).
|
|
51
|
+
html `<p class="k-note" data-sandbox-engine>Builds, checks and flows run in ${sandboxRuntimeWords()}.</p>`,
|
|
52
|
+
]);
|
|
53
|
+
return settingsSection({ id: "sandbox", title: "Home folder", lead: said,
|
|
54
|
+
body: joinHtml([head, !sandbox.canChange ? APPROVER_CHANGES : disclose("Change", kitSectionForm({ proof: "project-sandbox", action: "/settings/project/sandbox", id: "sandbox-form", gate: true, hidden: { repo: view.repo },
|
|
55
|
+
body: joinHtml([
|
|
56
|
+
formRow("Paths builds can't read", "One per line: ~/… or a full path.", textarea({ id: "sandbox-read-deny", name: "read-deny", value: readDeny.join("\n"), rows: 4, attrs: { placeholder: "~/.ssh\n~/.aws", "aria-label": "Paths builds can't read" } }), "top"),
|
|
57
|
+
disclose("Advanced", formRow("macOS services builds may also use", null, textarea({ id: "sandbox-mach", name: "mach", value: machLookup.join("\n"), rows: 2, attrs: { "aria-label": "macOS services builds may also use" } }), "top")),
|
|
58
|
+
note(SCOPED),
|
|
59
|
+
]) }))]) });
|
|
85
60
|
}
|
|
86
61
|
/** Check access (check-access.ts): how far the project's checks reach, said once. Collapsed: the level, what it means
|
|
87
62
|
* when it isn't the default, and who changed it. Inside Change: a line per level, and each wider level's warning under it
|
|
@@ -99,32 +74,41 @@ function checkAccessHtml(view) {
|
|
|
99
74
|
const meaning = { sandboxed: null, "sign-ins": "Checks can read your Claude, Codex and GitHub sign-ins.", unsandboxed: "Checks run with your full access to this machine and to Toolroll." };
|
|
100
75
|
// The sign-ins never widen the network: how many hosts they'd still need, and where to add them (a change of its own).
|
|
101
76
|
const missing = setting === "sign-ins" && network !== null ? signInHostsMissing(network) : [];
|
|
102
|
-
const hosts = missing.length === 0 ? "" :
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
77
|
+
const hosts = missing.length === 0 ? "" : joinHtml([
|
|
78
|
+
html `<p class="k-note" data-sign-in-hosts>${missing.length} sign-in ${missing.length === 1 ? "host isn't" : "hosts aren't"} allowed yet · <a class="k-link" href="#network">Add them</a></p>`,
|
|
79
|
+
disclose("Show hosts", html `<p class="k-note k-mono">${missing.join(", ")}</p>`),
|
|
80
|
+
]);
|
|
81
|
+
const changed = access.changed === null ? "" : note(`Changed by ${access.changed.by}, ${new Date(access.changed.at).toLocaleDateString("en-GB", { day: "numeric", month: "short", timeZone: "UTC" })}`);
|
|
107
82
|
// No OS sandbox here, and unconfined running allowed: checks run without a sandbox whatever is saved, and that is
|
|
108
83
|
// what the page says first (never "sandboxed"); the saved level is named under it.
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
84
|
+
const unconfinedHere = access.unconfinedHere === true;
|
|
85
|
+
const shown = unconfinedHere ? "unsandboxed" : setting;
|
|
86
|
+
const head = unconfinedHere
|
|
87
|
+
? joinHtml([saved === "unsandboxed" ? "" : html `<p class="k-note" data-saved-level>Saved as ${names[saved]}.</p>`,
|
|
88
|
+
html `<p class="k-note" data-unconfined-here>This machine can't sandbox checks, so they run without a sandbox whatever is chosen here.</p>`, changed])
|
|
89
|
+
: joinHtml([setting !== saved ? html `<p class="k-note" data-sign-ins-unavailable>Saved with model and GitHub sign-ins, but checks run sandboxed: sign-ins can't be shared with checks safely yet.</p>` : "", hosts, changed]);
|
|
90
|
+
const section = (body) => settingsSection({ id: "check-access", title: "Check access", state: ` · ${names[shown]}`,
|
|
91
|
+
lead: unconfinedHere ? undefined : meaning[setting] ?? undefined, body });
|
|
112
92
|
if (!access.canNarrow && !access.canWiden)
|
|
113
|
-
return
|
|
93
|
+
return section(head);
|
|
114
94
|
const option = (value, detail, warning) => {
|
|
115
95
|
const allowed = (value === setting || access.canWiden || value === "sandboxed") && !(value === "sign-ins" && unavailable !== null);
|
|
116
|
-
return html `<
|
|
96
|
+
return html `<label class="k-opt"><input type="radio" name="level" value="${value}"${setting === value ? html ` checked` : ""}${allowed ? "" : html ` disabled`}><span><b>${names[value]}</b><span>${detail}</span>\
|
|
97
|
+
${warning === null ? "" : html `<span data-check-access-warning="${value}">${warning}</span>`}</span></label>`;
|
|
117
98
|
};
|
|
118
99
|
const open = network?.mode === "open";
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
100
|
+
const choices = html `<fieldset class="k-radios"><legend class="k-sr">Check access</legend>\
|
|
101
|
+
${option("sandboxed", "Reads your home folder, as in Anthropic's sandbox runtime, except the folders this project hides (Home folder, above). Never Toolroll's folders or other tasks' work.", null)}\
|
|
102
|
+
${unavailable !== null ? option("sign-ins", unavailable, null) : option("sign-ins", `Also reads the files your Claude, Codex and GitHub sign-ins are kept in, on this project's network (${open ? "any host" : network === null || network.hosts.length === 0 ? "registries only" : `registries and ${network.hosts.length} ${network.hosts.length === 1 ? "host" : "hosts"}`}).`, CHECK_ACCESS_WARNINGS["sign-ins"])}\
|
|
103
|
+
${option("unsandboxed", "For projects whose checks create sandboxes themselves, such as Toolroll's own.", CHECK_ACCESS_WARNINGS.unsandboxed)}</fieldset>`;
|
|
104
|
+
const confirmOpen = open && unavailable === null
|
|
105
|
+
? html `<label class="k-opt"><input type="checkbox" name="confirm-open" value="yes"><span><b>The network is open</b><span>With sign-ins, checks could send them to any host.</span></span></label>`
|
|
106
|
+
: "";
|
|
107
|
+
// A form that offers a wider level asks for an instance operator's password, typed now (the server decides).
|
|
108
|
+
const form = kitSectionForm({ proof: access.canWiden ? "project-check-access-widen" : "project-check-access", action: "/settings/project/check-access", id: "check-access-form", gate: true, hidden: { repo: view.repo },
|
|
109
|
+
body: joinHtml([formRow("How far checks reach", null, choices, "wide"), confirmOpen,
|
|
110
|
+
note(`${access.canWiden ? "" : "Only an instance operator can widen this. "}Flow script steps always run sandboxed. Tasks already approved keep their level.`)]) });
|
|
111
|
+
return section(joinHtml([head, disclose("Change", form)]));
|
|
128
112
|
}
|
|
129
113
|
/** Network: what the project's builds may reach, said plainly; an approver changes it with their password. */
|
|
130
114
|
function networkHtml(view) {
|
|
@@ -133,22 +117,20 @@ function networkHtml(view) {
|
|
|
133
117
|
return "";
|
|
134
118
|
const { setting } = network;
|
|
135
119
|
const reach = setting.mode === "open"
|
|
136
|
-
? html
|
|
137
|
-
: html
|
|
120
|
+
? html `Builds may reach any host.`
|
|
121
|
+
: html `Builds reach their agent's provider and the package registries this project uses${setting.hosts.length === 0 ? "" : html `, and ${mono(setting.hosts.join(", "))}`}. Everything else is refused.`;
|
|
138
122
|
const local = setting.loopback === "any"
|
|
139
|
-
?
|
|
140
|
-
:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<button type="submit">Save</button>`, { hidden: { repo: view.repo } });
|
|
151
|
-
return html `<section class="build-access network-setting" id="network" aria-labelledby="network-title"><h2 id="network-title">Network</h2>${reach}${local}<details><summary>Change</summary>${form}</details></section>`;
|
|
123
|
+
? note("On a Mac, builds may reach this Mac's local services: test servers, and anything else listening here.")
|
|
124
|
+
: note("On a Mac, builds reach no local services. A project whose tests start a local server needs them on.");
|
|
125
|
+
return settingsSection({ id: "network", title: "Network", state: setting.mode === "open" ? " · Open" : "", lead: reach,
|
|
126
|
+
body: joinHtml([local, !network.canChange ? APPROVER_CHANGES : disclose("Change", kitSectionForm({ proof: "project-network", action: "/settings/project/network", id: "network-form", gate: true, hidden: { repo: view.repo },
|
|
127
|
+
body: joinHtml([
|
|
128
|
+
formRow("This Mac's local services", "Local test servers work, and so does anything else listening on this Mac (a local database, a dev proxy). Linux builds always have local services of their own.", switchInput({ id: "network-local", name: "local", label: "Allowed", on: setting.loopback === "any", attrs: { value: "any", "aria-label": "Builds may reach this Mac's local services" } }), "inline"),
|
|
129
|
+
formRow("Other hosts builds may reach", "One per line.", textarea({ id: "network-hosts", name: "hosts", value: setting.hosts.join("\n"), rows: 3,
|
|
130
|
+
attrs: { autocomplete: "off", autocapitalize: "off", spellcheck: "false", placeholder: "api.stripe.com\n*.example.com", "aria-label": "Other hosts builds may reach" } }), "top"),
|
|
131
|
+
formRow("Open", "Builds may reach any host.", switchInput({ id: "network-open", name: "mode", label: "Open", on: setting.mode === "open", attrs: { value: "open", "aria-label": "Open: builds may reach any host" } }), "inline"),
|
|
132
|
+
note("Tasks scoped from now on use this; tasks already approved keep the network they were approved with."),
|
|
133
|
+
]) }))]) });
|
|
152
134
|
}
|
|
153
135
|
/** Live view: hidden by default; showing commands takes the password, hiding them is one step. */
|
|
154
136
|
function liveViewHtml(view) {
|
|
@@ -156,63 +138,79 @@ function liveViewHtml(view) {
|
|
|
156
138
|
if (live === undefined)
|
|
157
139
|
return "";
|
|
158
140
|
const said = live.commands
|
|
159
|
-
?
|
|
160
|
-
:
|
|
161
|
-
const change = !live.canChange ?
|
|
141
|
+
? "Commands and their output show in the live view of this project's agents. Secrets are hidden where they're recognised, which isn't guaranteed."
|
|
142
|
+
: "Commands are hidden in the live view. You see which files an agent reads and edits, but not the commands it runs or their output.";
|
|
143
|
+
const change = !live.canChange ? APPROVER_CHANGES
|
|
162
144
|
: live.commands
|
|
163
|
-
? postForm("/settings/project/live-view",
|
|
164
|
-
:
|
|
165
|
-
|
|
145
|
+
? postForm("/settings/project/live-view", joinHtml([note("From the next run."), acts(btn("Hide commands", { sm: true, type: "submit" }))]), { hidden: { repo: view.repo, act: "off" } })
|
|
146
|
+
: kitSectionForm({ proof: "project-live-view", action: "/settings/project/live-view", id: "live-view-form", hidden: { repo: view.repo, act: "on", nonce: live.nonce ?? "" },
|
|
147
|
+
save: { label: "Show commands", variant: "outline" }, confirm: "Show this project's commands in the live view",
|
|
148
|
+
body: note("Output can include secrets the filter misses. Turn this on only where you're happy for anyone who can see this project to read it. From the next run.") });
|
|
149
|
+
return settingsSection({ id: "live-view", title: "Live view", state: html `<span data-live-view-setting="${live.commands ? "commands" : "base"}"> · ${live.commands ? "Commands shown" : "Commands hidden"}</span>`,
|
|
150
|
+
lead: said, body: change });
|
|
166
151
|
}
|
|
167
152
|
/** Build access: one sentence for what a build gets, the detail on demand, and the way to change it. */
|
|
168
153
|
function accessHtml(view) {
|
|
169
154
|
const access = view.access;
|
|
170
155
|
if (access === undefined)
|
|
171
156
|
return "";
|
|
172
|
-
const manage =
|
|
157
|
+
const manage = acts(btn("Manage tools", { sm: true, href: `/settings/tools?repo=${encodeURIComponent(view.repo)}` }));
|
|
173
158
|
const always = [
|
|
174
159
|
"Your other projects, keys and sign-in files are out of reach.",
|
|
175
160
|
"Your own Claude and Codex hooks, plugins and MCP servers don't load.",
|
|
176
161
|
"Builds sign in with your existing Claude or Codex login.",
|
|
177
162
|
];
|
|
178
163
|
if ("problem" in access) {
|
|
179
|
-
return
|
|
164
|
+
return settingsSection({ id: "build-access", title: "Build access", body: joinHtml([notice(access.problem), disclose("Details", list(always)), manage]) });
|
|
180
165
|
}
|
|
181
166
|
const loading = access.tools.filter(one => one.loads);
|
|
182
|
-
const names = loading.map((one, at) => html `${at === 0 ? "" : ", "}
|
|
167
|
+
const names = loading.map((one, at) => html `${at === 0 ? "" : ", "}${mono(one.name)}`);
|
|
183
168
|
const outcome = loading.length === 0
|
|
184
|
-
? html
|
|
185
|
-
: html
|
|
186
|
-
const tools = access.tools.map(one => html
|
|
187
|
-
return
|
|
169
|
+
? html `Builds work in their task's folder, with no extra tools.`
|
|
170
|
+
: html `Builds work in their task's folder, with ${loading.length === 1 ? "one tool" : `${loading.length} tools`}: ${names}.`;
|
|
171
|
+
const tools = access.tools.map(one => html `${mono(one.name)} · ${one.words}`);
|
|
172
|
+
return settingsSection({ id: "build-access", title: "Build access", lead: outcome, body: joinHtml([disclose("Details", list([...always, ...tools])), manage]) });
|
|
188
173
|
}
|
|
189
174
|
/** Builds at once: one number, one Save. The worker's cap is said only when it lowers the number. */
|
|
190
175
|
function buildsHtml(view) {
|
|
191
176
|
const b = view.builds;
|
|
192
177
|
if (b === undefined)
|
|
193
178
|
return "";
|
|
194
|
-
const capped = b.capacity !== null && b.capacity < b.setting ? ` Its
|
|
179
|
+
const capped = b.capacity !== null && b.capacity < b.setting ? ` Its builder runs ${b.capacity} at once, so ${b.capacity} for now.` : "";
|
|
195
180
|
const now = `${b.building} building now.${capped}`;
|
|
196
181
|
if (!b.canChange)
|
|
197
|
-
return
|
|
198
|
-
return
|
|
182
|
+
return settingsSection({ id: "builds-at-once", title: "Builds at once", state: ` · Up to ${b.setting}`, body: note(now) });
|
|
183
|
+
return settingsSection({ id: "builds-at-once", title: "Builds at once",
|
|
184
|
+
body: postForm("/settings/project/concurrency", formRow("Builds at once", now, html `${input({ id: "builds-at-once-n", name: "concurrency", type: "number", value: String(b.setting),
|
|
185
|
+
attrs: { min: 1, max: 64, step: 1, inputmode: "numeric", "aria-label": "Builds at once" } })}${btn("Save", { sm: true, type: "submit" })}`, "inline"), { hidden: { repo: view.repo } }) });
|
|
199
186
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
export function projectSettingsHtml(view,
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
187
|
+
/** The project's page: its name and path, what Toolroll holds for it, its build settings, and (for an instance operator)
|
|
188
|
+
* Remove and Delete. */
|
|
189
|
+
export function projectSettingsHtml(view, said) {
|
|
190
|
+
const answer = said.problem ? notice(said.problem) : said.said ? infoLine(said.said) : "";
|
|
191
|
+
const project = settingsSection({ id: "project", title: view.name, lead: mono(view.repo),
|
|
192
|
+
body: joinHtml([
|
|
193
|
+
note(`Toolroll holds ${holdingsWords(view.holdings)} for this project.`),
|
|
194
|
+
formRow("Recorded remote", view.remoteUrl ? null : "None. Landed work waits for your review.", view.remoteUrl ? mono(view.remoteUrl) : html ``, view.remoteUrl ? "wide" : "inline"),
|
|
195
|
+
disclose("Change remote", joinHtml([note("An owner or instance operator can run:"), codeLine("toolroll repos add <project> --remote <url>")])),
|
|
196
|
+
]) });
|
|
197
|
+
const settings = joinHtml([buildsHtml(view), accessHtml(view), networkHtml(view), writesHtml(view), sandboxHtml(view), checkAccessHtml(view), liveViewHtml(view)]);
|
|
208
198
|
if (!view.canDelete)
|
|
209
|
-
return
|
|
210
|
-
const
|
|
211
|
-
?
|
|
212
|
-
:
|
|
199
|
+
return joinHtml([answer, project, settings, note("An instance operator can remove or delete a project.")]);
|
|
200
|
+
const deleteBody = view.running.length > 0
|
|
201
|
+
? notice(`Its work is running: ${view.running.join(", ")}. Stop it before deleting the project.`)
|
|
202
|
+
: joinHtml([note(`Removes everything Toolroll holds for ${view.name}. The repository and its own branches stay. There's no undo.`),
|
|
203
|
+
postForm("/settings/project/delete", joinHtml([
|
|
204
|
+
formRow(`Type ${view.name} to continue`, null, input({ id: "delete-name", name: "name", attrs: { autocomplete: "off", autocapitalize: "off", spellcheck: "false", required: true, "aria-label": `Type ${view.name} to continue` } })),
|
|
205
|
+
acts(btn("Continue", { sm: true, type: "submit", variant: "danger" })),
|
|
206
|
+
]), { hidden: { repo: view.repo } })]);
|
|
213
207
|
// Removing is the reversible step: off the lists and the builder, everything kept.
|
|
214
|
-
const
|
|
215
|
-
|
|
208
|
+
const leave = settingsSection({ id: "remove", title: "Remove or delete",
|
|
209
|
+
body: joinHtml([
|
|
210
|
+
formRow("Remove from Toolroll", "Its tasks and results stay saved. Add it again to bring it back.", postForm("/projects/remove", btn("Remove from Toolroll", { sm: true, type: "submit" }), { hidden: { repo: view.repo } }), "inline"),
|
|
211
|
+
disclose("Delete project", deleteBody, Boolean(said.problem)),
|
|
212
|
+
]) });
|
|
213
|
+
return joinHtml([answer, project, settings, leave]);
|
|
216
214
|
}
|
|
217
215
|
/** The second step: exactly what goes, and the password. */
|
|
218
216
|
export function projectDeleteConfirmHtml(view, problem) {
|
|
@@ -226,5 +224,16 @@ export function projectDeleteConfirmHtml(view, problem) {
|
|
|
226
224
|
...(h.subagents > 0 ? [count(h.subagents, "subagent")] : []),
|
|
227
225
|
"Its budgets, settings and knowledge",
|
|
228
226
|
];
|
|
229
|
-
return
|
|
227
|
+
return joinHtml([
|
|
228
|
+
problem === null ? "" : notice(problem),
|
|
229
|
+
settingsSection({ id: "delete", title: "This removes",
|
|
230
|
+
body: joinHtml([
|
|
231
|
+
list(items),
|
|
232
|
+
note(html `The repository at ${mono(view.repo)} and its own branches stay. The ledger keeps its history. There's no undo.`),
|
|
233
|
+
// The answer is a page of its own (this page again with a refusal, or the project page), so it posts as a navigation.
|
|
234
|
+
kitSectionForm({ proof: "project-delete", action: "/settings/project/delete", id: "delete-form", native: true, hidden: { repo: view.repo, name: view.name, step: "delete" },
|
|
235
|
+
save: { label: "Delete project", variant: "danger" }, confirm: `Delete ${view.name}`,
|
|
236
|
+
body: acts(btn("Cancel", { sm: true, variant: "ghost", href: `/settings/project?repo=${encodeURIComponent(view.repo)}` })) }),
|
|
237
|
+
]) }),
|
|
238
|
+
]);
|
|
230
239
|
}
|