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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,91 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.88 — 2026-11-03
|
|
4
|
+
|
|
5
|
+
- **Every text box looks the same, and calmer.** Wherever you type or pick
|
|
6
|
+
something, the field is one soft filled shape with its label above and any
|
|
7
|
+
hint or error below, with no outlines at rest. Text boxes grow as you
|
|
8
|
+
write, so there are no resize grips. On a phone, fields are larger and easy
|
|
9
|
+
to tap.
|
|
10
|
+
- **A message box like the chat apps you know.** The lead chat has one
|
|
11
|
+
rounded box with a **+** menu, a chip that shows the project, and a solid
|
|
12
|
+
round send button that turns into Stop while the lead works. The task
|
|
13
|
+
conversation's box has the same look.
|
|
14
|
+
- **Conversation settings moved into the + menu.** The automatic crew
|
|
15
|
+
updates, conversation details, End the conversation and the link to Lead
|
|
16
|
+
settings now open from **+ → Conversation settings** instead of sitting
|
|
17
|
+
under the chat box. Escape closes it.
|
|
18
|
+
- **New task matches.** Pick the project from the chip in the box, and turn on
|
|
19
|
+
"Read code before planning" with one tap.
|
|
20
|
+
- **A filled phone dock**, matching the new look.
|
|
21
|
+
|
|
22
|
+
### For developers
|
|
23
|
+
|
|
24
|
+
- **Faster checks.** Pull request CI finishes in about 8 minutes instead of
|
|
25
|
+
about 19: test shards are balanced by measured time, and macOS runs only
|
|
26
|
+
the tests that depend on macOS (the nightly run still covers everything).
|
|
27
|
+
The scripted end-to-end journeys now run on every pull request, as an
|
|
28
|
+
advisory check. The Mac release check uses more of a busy machine.
|
|
29
|
+
|
|
30
|
+
## 0.9.87 — 2026-11-03
|
|
31
|
+
|
|
32
|
+
- **The whole app is redesigned.** Every page, from sign-in to Settings,
|
|
33
|
+
now has one calm look: the same buttons, forms, status words and phone
|
|
34
|
+
layout everywhere, and amber only for what needs you.
|
|
35
|
+
- **A new Home.** One summary of where things stand and one Needs-you count,
|
|
36
|
+
the same count you see in the side rail, the phone dock and the tab title.
|
|
37
|
+
- **A new task page.** One column with a five-step progress line (Plan ·
|
|
38
|
+
Approval · Build · Checks · Ready), then Plan or Result, Activity and
|
|
39
|
+
Attempts. Approving asks for a password only when one is needed: with
|
|
40
|
+
Passwordless or a remembered password it is one click, and an automatic
|
|
41
|
+
rule shows nothing to press. The box keeps the exact terms it showed you;
|
|
42
|
+
if they change, it offers Reload, never a yes.
|
|
43
|
+
- **Tasks, as a list or a board.** Filter by project and by Needs you,
|
|
44
|
+
Running, Queued or Finished, each with its count; each waiting task says
|
|
45
|
+
what it asks for and offers one action.
|
|
46
|
+
- **Ship shows how long each check took**, every pull request check on one
|
|
47
|
+
timeline.
|
|
48
|
+
- **Release progress.** A release task shows every step in order, where it
|
|
49
|
+
stands ("Deploy to this Mac · Step 6 of 13") and why it stopped.
|
|
50
|
+
- **A new lead chat**, plus **⌘K search** from anywhere and a `/search` page.
|
|
51
|
+
- **Flows on the phone.** The flow canvas pans and pinch-zooms on a phone,
|
|
52
|
+
and the Flows list, templates, kits and starter flows are on the new look.
|
|
53
|
+
- **Settings in eight groups**, with what needs you at the top. Each section
|
|
54
|
+
saves on its own. Spend and Models live in Settings too.
|
|
55
|
+
- **A first-run guide at `/welcome`**: set a password, add a project, pick an
|
|
56
|
+
agent and models, turn on phone alerts, file a first task. The next step is
|
|
57
|
+
always the one highlighted.
|
|
58
|
+
- **New task is one box**: pick the project, type, send. Everything else is
|
|
59
|
+
under More options.
|
|
60
|
+
- **Some Settings saves work again.** Turning sign-in with a provider on or
|
|
61
|
+
off, turning Passwordless on, request limits and adding a tool no longer
|
|
62
|
+
fail with "Toolroll refused this (404)", and a save that stays on the same
|
|
63
|
+
page now shows what was saved.
|
|
64
|
+
- **From the terminal:** `toolroll task ship <task>` shows a task's pull
|
|
65
|
+
request checks, `toolroll release status [<task>]` shows release progress,
|
|
66
|
+
`toolroll task list --view queued` lists queued work, and `toolroll release
|
|
67
|
+
--branch <name>` releases a branch named like one of the command's words.
|
|
68
|
+
|
|
69
|
+
### Safety
|
|
70
|
+
|
|
71
|
+
- **Approving a flow's script is bound to the exact version you read.** If
|
|
72
|
+
the script changes after you looked, the approval doesn't count.
|
|
73
|
+
- **Changing a flow ends its "run on its own" approvals**; it waits for you
|
|
74
|
+
again.
|
|
75
|
+
- **Password pages open no live connection** to the rest of the app.
|
|
76
|
+
- **Project credential forms always show the password field**, even with
|
|
77
|
+
Passwordless on (Where builds write, Home folder, Check access, Network).
|
|
78
|
+
Before, they hid it and every save was refused.
|
|
79
|
+
- **Choosing providers no longer widens by accident.** "Any provider" now
|
|
80
|
+
means every provider ticked; ticking some saves exactly those.
|
|
81
|
+
- **Releases ignore the advisory youiyoux check**, so a design warning never
|
|
82
|
+
holds up a release.
|
|
83
|
+
|
|
84
|
+
### For developers
|
|
85
|
+
|
|
86
|
+
- **youiyoux enforces the kit in CI.** A pull request that adds design drift
|
|
87
|
+
against the shared kit (`src/design/KIT.md`) fails the youiyoux job.
|
|
88
|
+
|
|
3
89
|
## 0.9.86 — 2026-11-02
|
|
4
90
|
|
|
5
91
|
- **Old tasks tidy themselves.** A routine's newer run puts away its older
|
package/dist/acp-check.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export declare const CHECK_PROMPTS: {
|
|
|
5
5
|
readonly ok: "Create a file named done.txt in the current folder containing exactly the word ok. Then stop. Do nothing else.";
|
|
6
6
|
readonly stop: "Run the shell command `sleep 120` and wait for it to finish.";
|
|
7
7
|
};
|
|
8
|
+
/** How long the stop step waits for the agent to start its command, and for the cancel to be answered. Every product
|
|
9
|
+
* path waits exactly this: runAcpChecks takes no other value (src/acp-check-timing.test.ts holds it). */
|
|
10
|
+
export declare const STOP_WAIT_MS = 60000;
|
|
8
11
|
export type AcpCheckJob = {
|
|
9
12
|
store: Store;
|
|
10
13
|
runner: string;
|
|
@@ -26,3 +29,9 @@ export type AcpCheckJob = {
|
|
|
26
29
|
/** Close what no live watch holds any more, let go of what finished checks still hold, then run this runner's next
|
|
27
30
|
* check, if one is waiting. */
|
|
28
31
|
export declare function runAcpChecks(job: AcpCheckJob): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Tests only: runAcpChecks with a shorter stop-step window, so a test whose agent never starts its command doesn't
|
|
34
|
+
* wait out the product's minute. No product code may import it (src/acp-check-timing.test.ts fails if any does); the
|
|
35
|
+
* job carries no wait of its own, so no CLI, environment or config value can change the product's.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runAcpChecksForTests(job: AcpCheckJob, stopWaitMs: number): Promise<void>;
|
package/dist/acp-check.js
CHANGED
|
@@ -28,33 +28,15 @@ export const CHECK_PROMPTS = {
|
|
|
28
28
|
ok: "Create a file named done.txt in the current folder containing exactly the word ok. Then stop. Do nothing else.",
|
|
29
29
|
stop: "Run the shell command `sleep 120` and wait for it to finish.",
|
|
30
30
|
};
|
|
31
|
-
/** How long the stop step waits for the agent to start its command, and for the cancel to be answered.
|
|
32
|
-
|
|
31
|
+
/** How long the stop step waits for the agent to start its command, and for the cancel to be answered. Every product
|
|
32
|
+
* path waits exactly this: runAcpChecks takes no other value (src/acp-check-timing.test.ts holds it). */
|
|
33
|
+
export const STOP_WAIT_MS = 60_000;
|
|
33
34
|
/** How long the agent has to answer the cancel. */
|
|
34
35
|
const CANCEL_ANSWER_MS = 15_000;
|
|
35
36
|
/** Close what no live watch holds any more, let go of what finished checks still hold, then run this runner's next
|
|
36
37
|
* check, if one is waiting. */
|
|
37
38
|
export async function runAcpChecks(job) {
|
|
38
|
-
|
|
39
|
-
job.store.expireAcpChecks(clock());
|
|
40
|
-
for (const abandoned of job.store.abandonedAcpChecks(job.runner, clock()))
|
|
41
|
-
job.store.failAcpCheck(abandoned.id, "interrupted", clock());
|
|
42
|
-
await settleChecks(job, clock);
|
|
43
|
-
if (process.platform === "win32")
|
|
44
|
-
return;
|
|
45
|
-
const request = job.store.claimAcpCheck(job.runner, job.watch, clock());
|
|
46
|
-
if (request === null)
|
|
47
|
-
return;
|
|
48
|
-
const name = isAcpProvider(request.provider) ? ACP_AGENTS[request.provider].name : request.provider;
|
|
49
|
-
job.progress?.(`agents: checking ${name} (${request.model}) on ${job.runner}`);
|
|
50
|
-
try {
|
|
51
|
-
const verdict = await check(job, request, clock);
|
|
52
|
-
job.progress?.(`agents: ${name} ${verdict === null ? "passed its check" : `failed its check — ${verdict}`}`);
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
job.store.failAcpCheck(request.id, `the check stopped: ${error instanceof Error ? error.message : String(error)}`, clock());
|
|
56
|
-
}
|
|
57
|
-
await settleChecks(job, clock);
|
|
39
|
+
return runChecks(job, STOP_WAIT_MS);
|
|
58
40
|
}
|
|
59
41
|
/**
|
|
60
42
|
* Let go of what finished checks hold — their checkout, their task, their claim — once nothing of theirs can still be
|
|
@@ -139,8 +121,38 @@ async function settleChecks(job, clock) {
|
|
|
139
121
|
}
|
|
140
122
|
}
|
|
141
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Tests only: runAcpChecks with a shorter stop-step window, so a test whose agent never starts its command doesn't
|
|
126
|
+
* wait out the product's minute. No product code may import it (src/acp-check-timing.test.ts fails if any does); the
|
|
127
|
+
* job carries no wait of its own, so no CLI, environment or config value can change the product's.
|
|
128
|
+
*/
|
|
129
|
+
export async function runAcpChecksForTests(job, stopWaitMs) {
|
|
130
|
+
return runChecks(job, stopWaitMs);
|
|
131
|
+
}
|
|
132
|
+
async function runChecks(job, stopWaitMs) {
|
|
133
|
+
const clock = job.clock ?? (() => new Date());
|
|
134
|
+
job.store.expireAcpChecks(clock());
|
|
135
|
+
for (const abandoned of job.store.abandonedAcpChecks(job.runner, clock()))
|
|
136
|
+
job.store.failAcpCheck(abandoned.id, "interrupted", clock());
|
|
137
|
+
await settleChecks(job, clock);
|
|
138
|
+
if (process.platform === "win32")
|
|
139
|
+
return;
|
|
140
|
+
const request = job.store.claimAcpCheck(job.runner, job.watch, clock());
|
|
141
|
+
if (request === null)
|
|
142
|
+
return;
|
|
143
|
+
const name = isAcpProvider(request.provider) ? ACP_AGENTS[request.provider].name : request.provider;
|
|
144
|
+
job.progress?.(`agents: checking ${name} (${request.model}) on ${job.runner}`);
|
|
145
|
+
try {
|
|
146
|
+
const verdict = await check(job, request, clock, stopWaitMs);
|
|
147
|
+
job.progress?.(`agents: ${name} ${verdict === null ? "passed its check" : `failed its check — ${verdict}`}`);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
job.store.failAcpCheck(request.id, `the check stopped: ${error instanceof Error ? error.message : String(error)}`, clock());
|
|
151
|
+
}
|
|
152
|
+
await settleChecks(job, clock);
|
|
153
|
+
}
|
|
142
154
|
/** Run one claimed check; null when it passed (and was published), else why not. */
|
|
143
|
-
async function check(job, request, clock) {
|
|
155
|
+
async function check(job, request, clock, stopWaitMs) {
|
|
144
156
|
const { store } = job;
|
|
145
157
|
const fail = (words) => { store.failAcpCheck(request.id, words, clock()); return words; };
|
|
146
158
|
if (!isAcpProvider(request.provider))
|
|
@@ -185,7 +197,7 @@ async function check(job, request, clock) {
|
|
|
185
197
|
const canaryFile = join(stateDir, `acp-check-secret-${request.id}`);
|
|
186
198
|
let verdict = null;
|
|
187
199
|
try {
|
|
188
|
-
verdict = await steps(job, request, { ref, leaseId, worktree, canaryFile }, clock);
|
|
200
|
+
verdict = await steps(job, request, { ref, leaseId, worktree, canaryFile }, clock, stopWaitMs);
|
|
189
201
|
}
|
|
190
202
|
finally {
|
|
191
203
|
rmSync(canaryFile, { force: true });
|
|
@@ -200,17 +212,17 @@ function finishCheckRun(store, run, clock) {
|
|
|
200
212
|
store.finishRun(run, { outcome: "refused", reason: "agent-check", now: clock() });
|
|
201
213
|
}
|
|
202
214
|
/** The two steps; whatever throws, the step's run in flight is finished (cleanup still waits for its processes). */
|
|
203
|
-
async function steps(job, request, at, clock) {
|
|
215
|
+
async function steps(job, request, at, clock, stopWaitMs) {
|
|
204
216
|
let current = null;
|
|
205
217
|
try {
|
|
206
|
-
return await runSteps(job, request, at, clock, run => { current = run; });
|
|
218
|
+
return await runSteps(job, request, at, clock, run => { current = run; }, stopWaitMs);
|
|
207
219
|
}
|
|
208
220
|
finally {
|
|
209
221
|
if (current !== null)
|
|
210
222
|
finishCheckRun(job.store, current, clock);
|
|
211
223
|
}
|
|
212
224
|
}
|
|
213
|
-
async function runSteps(job, request, at, clock, track) {
|
|
225
|
+
async function runSteps(job, request, at, clock, track, stopWaitMs) {
|
|
214
226
|
const { store } = job;
|
|
215
227
|
const spec = { provider: request.provider, model: request.model };
|
|
216
228
|
const ask = (brief) => ({ phase: "build", brief, maxTurns: 1, permissionMode: "bypassPermissions", skipPermissions: true, resumeSession: null });
|
|
@@ -290,7 +302,7 @@ async function runSteps(job, request, at, clock, track) {
|
|
|
290
302
|
},
|
|
291
303
|
acp: { prompt: "", model: "", expect: null, onControl: (control) => {
|
|
292
304
|
// The agent's word only times the cancel: what the plane itself sees alive is what is graded.
|
|
293
|
-
const until = Date.now() +
|
|
305
|
+
const until = Date.now() + stopWaitMs;
|
|
294
306
|
const poll = setInterval(() => {
|
|
295
307
|
if (Date.now() > until) {
|
|
296
308
|
clearInterval(poll);
|
package/dist/agents-board.d.ts
CHANGED
|
@@ -130,4 +130,16 @@ export declare function laneNewestBuildSql(): string;
|
|
|
130
130
|
export declare function agentLanesOf(store: Store, now: Date, access: {
|
|
131
131
|
repos: readonly string[] | null;
|
|
132
132
|
}, account: string, window: AgentLaneWindow, path?: string): BrowserAgentLanes | null;
|
|
133
|
+
/**
|
|
134
|
+
* The Agents list's every live agent, not only builds (UI revamp wave B): the build board's groups, with each running
|
|
135
|
+
* planner, scout and reviewer in the admitted projects added under Working (or Needs you when it asks). Reads the
|
|
136
|
+
* open leases only (store.liveRuns), then each run through the same admitted read the opened view uses.
|
|
137
|
+
*/
|
|
138
|
+
export declare function agentsListOf(store: Store, now: Date, access: {
|
|
139
|
+
repos: readonly string[] | null;
|
|
140
|
+
}, account: string, root?: string): {
|
|
141
|
+
key: AgentGroupKey;
|
|
142
|
+
label: string;
|
|
143
|
+
rows: BrowserAgentRow[];
|
|
144
|
+
}[];
|
|
133
145
|
export {};
|
package/dist/agents-board.js
CHANGED
|
@@ -332,3 +332,29 @@ export function agentLanesOf(store, now, access, account, window, path = "/agent
|
|
|
332
332
|
running: tasks.length === 0 ? 0 : Number(tasks[0]["running"]),
|
|
333
333
|
};
|
|
334
334
|
}
|
|
335
|
+
/** The word a running planner, scout or reviewer wears on the Agents list (a build keeps its own state word). */
|
|
336
|
+
const ROLE_WORDS = { planner: "Planning", scout: "Investigating", reviewer: "Reviewing" };
|
|
337
|
+
/**
|
|
338
|
+
* The Agents list's every live agent, not only builds (UI revamp wave B): the build board's groups, with each running
|
|
339
|
+
* planner, scout and reviewer in the admitted projects added under Working (or Needs you when it asks). Reads the
|
|
340
|
+
* open leases only (store.liveRuns), then each run through the same admitted read the opened view uses.
|
|
341
|
+
*/
|
|
342
|
+
export function agentsListOf(store, now, access, account, root) {
|
|
343
|
+
const groups = agentsBoardOf(store, now, access, account, root);
|
|
344
|
+
const shown = new Set(groups.flatMap(group => group.rows.map(row => row.runId)));
|
|
345
|
+
const extra = [];
|
|
346
|
+
for (const run of store.liveRuns(now)) {
|
|
347
|
+
const role = run.role;
|
|
348
|
+
if (!(role in ROLE_WORDS) || shown.has(run.id))
|
|
349
|
+
continue;
|
|
350
|
+
const facts = agentFactsFor(store, run.id, access, account);
|
|
351
|
+
if (facts === null)
|
|
352
|
+
continue;
|
|
353
|
+
const row = agentRowOf(store, facts, now, root);
|
|
354
|
+
extra.push(row.group === "working" || row.group === "stuck" ? { ...row, group: "working", label: ROLE_WORDS[role] } : row);
|
|
355
|
+
}
|
|
356
|
+
if (extra.length === 0)
|
|
357
|
+
return groups;
|
|
358
|
+
const all = [...groups.flatMap(group => group.rows), ...extra];
|
|
359
|
+
return AGENT_GROUPS.map(group => ({ ...group, rows: all.filter(row => row.group === group.key) })).filter(group => group.rows.length > 0);
|
|
360
|
+
}
|
|
@@ -30,7 +30,10 @@ export type ReleaseApprovalView = {
|
|
|
30
30
|
at: string | null;
|
|
31
31
|
canChange: boolean;
|
|
32
32
|
};
|
|
33
|
-
export declare function releaseApprovalHtml(view: ReleaseApprovalView
|
|
33
|
+
export declare function releaseApprovalHtml(view: ReleaseApprovalView, notice?: {
|
|
34
|
+
said?: string | null;
|
|
35
|
+
problem?: string | null;
|
|
36
|
+
}): Html;
|
|
34
37
|
export declare function approvalRulesHtml(view: ApprovalRulesView, notice: {
|
|
35
38
|
said?: string | null;
|
|
36
39
|
problem?: string | null;
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* the identity provider); everyone else reads them. Every change is in the
|
|
5
5
|
* ledger, before → after.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { joinHtml } from "./html.js";
|
|
8
|
+
import { formRow, note, radios, settingsSection, switchInput, textarea } from "./server/kit.js";
|
|
9
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
8
10
|
const RULES_CSS = `.approval-rules{max-width:720px;min-width:0}.approval-rules fieldset{border:0;padding:0;margin:18px 0 0;min-width:0}` +
|
|
9
11
|
`.approval-rules legend{font-weight:600;font-size:.875rem;margin-bottom:8px}.approval-rules .choice{display:grid;grid-template-columns:auto minmax(0,1fr);gap:10px;align-items:start;margin:0 0 10px;font-weight:400}` +
|
|
10
12
|
`.approval-rules .choice input{margin:.2rem 0 0}.approval-rules .choice strong{display:block;font-size:.875rem;font-weight:500}.approval-rules .choice small{display:block;color:var(--muted-foreground);font-size:.8125rem;margin-top:2px}` +
|
|
@@ -24,23 +26,37 @@ export function rulesSummary(rules) {
|
|
|
24
26
|
parts.push(`Work touching ${rules.protectedPaths.join(", ")} is protected: two people approve it, never a subagent or an operating mode.`);
|
|
25
27
|
return parts.length === 0 ? "No approval rules: anyone who can approve on this project can approve its work, including their own." : parts.join(" ");
|
|
26
28
|
}
|
|
27
|
-
export function releaseApprovalHtml(view) {
|
|
28
|
-
const since = view.setBy === null ? "" :
|
|
29
|
-
const current =
|
|
30
|
-
const heading = html `<h2>Release approval</h2>`;
|
|
29
|
+
export function releaseApprovalHtml(view, notice = {}) {
|
|
30
|
+
const since = view.setBy === null ? "" : ` · changed by ${view.setBy} on ${(view.at ?? "").slice(0, 10)}`;
|
|
31
|
+
const current = view.auto ? "Releases approve themselves once the pull request’s CI and the release check pass on the exact commit. Anything red still asks you." : "Every release waits for your approval.";
|
|
31
32
|
if (!view.canChange)
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
return settingsSection({ id: "release-approval", title: "Releases", state: since, body: joinHtml([note(current), note("An approver of this project sets this.")]) });
|
|
34
|
+
return settingsSection({ id: "release-approval", title: "Releases", state: since, lead: current,
|
|
35
|
+
body: kitSectionForm({ proof: "release-approval", action: "/settings/release-approval", id: "release-form", gate: true, hidden: { repo: view.repo }, proofWhen: ["action", "auto"],
|
|
36
|
+
confirm: "Approve releases automatically", said: notice.said ?? null, problem: notice.problem ?? null,
|
|
37
|
+
body: formRow("Releases", "Turning this on asks for your password, every time, even with Passwordless on.", radios({ name: "action", legend: "Releases", current: view.auto ? "auto" : "ask", items: [
|
|
38
|
+
{ value: "ask", label: "Ask me", text: "A release deploys only after you approve it." },
|
|
39
|
+
{ value: "auto", label: "Approve automatically when checks pass", text: "You’re told when a release approves itself. A failed check, red CI or a CI failure after the deploy still stops and asks you." }
|
|
40
|
+
] }), "wide") }) });
|
|
35
41
|
}
|
|
36
42
|
export function approvalRulesHtml(view, notice) {
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const current = html `<p class="current">${rulesSummary(view.rules)}${changed}</p>`;
|
|
43
|
+
const changed = view.rules.updatedBy === null ? "" : ` · changed by ${view.rules.updatedBy} on ${(view.rules.updatedAt ?? "").slice(0, 10)}`;
|
|
44
|
+
const current = rulesSummary(view.rules);
|
|
40
45
|
if (!view.canChange)
|
|
41
|
-
return
|
|
46
|
+
return settingsSection({ id: "rules", title: `Approval rules · ${view.name}`, state: changed, body: joinHtml([note(current), note("An instance operator sets these.")]) });
|
|
42
47
|
const protect = view.rules.protectProject ? "project" : view.rules.protectedPaths.length > 0 ? "paths" : "none";
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
const few = view.approvers < 2 ? note(`Only ${view.approvers === 1 ? "one person" : "no one"} can approve on this project, so protected work would wait for a second approver. Add one in People first.`) : "";
|
|
49
|
+
return settingsSection({ id: "rules", title: `Approval rules · ${view.name}`, state: changed, lead: current,
|
|
50
|
+
body: kitSectionForm({ proof: "approval", action: "/settings/approval", id: "rules-form", gate: true, hidden: { repo: view.repo }, confirm: "Change approval rules",
|
|
51
|
+
said: notice.said ?? null, problem: notice.problem ?? null,
|
|
52
|
+
body: joinHtml([
|
|
53
|
+
formRow("Someone other than the requester", "The person who filed a task, or asked for a revision, can’t approve it. Operating modes stop approving their signer’s own filings here.", switchInput({ id: "rules-not-requester", name: "not_requester", label: "Required", on: view.rules.notRequester, attrs: { value: "1", "aria-label": "Someone other than the requester" } }), "inline"),
|
|
54
|
+
formRow("Protected work", "Changes here always wait for two people, never a subagent or an operating mode.", radios({ name: "protect", legend: "Protected work", current: protect, items: [
|
|
55
|
+
{ value: "none", label: "Nothing protected", text: "One approval is enough." },
|
|
56
|
+
{ value: "project", label: "The whole project", text: "Every task needs two different people to approve the same scope." },
|
|
57
|
+
{ value: "paths", label: "Only these paths", text: "Tasks that touch them, or don’t say what they touch, need two people." }
|
|
58
|
+
] }), "wide"),
|
|
59
|
+
formRow("Protected paths", "One per line. Used when “Only these paths” is chosen.", textarea({ id: "rules-paths", name: "paths", value: view.rules.protectedPaths.join("\n"), rows: 3, attrs: { spellcheck: "false", placeholder: "infra/**\nmigrations/**", "aria-label": "Protected paths" } }), "top"),
|
|
60
|
+
few,
|
|
61
|
+
]) }) });
|
|
46
62
|
}
|
package/dist/assignment-ui.js
CHANGED
|
@@ -29,7 +29,7 @@ export function assignmentActionHref(assignment) {
|
|
|
29
29
|
if (action.code === 'inspect-decisions')
|
|
30
30
|
return '/';
|
|
31
31
|
if (action.code === 'sign-in')
|
|
32
|
-
return '/settings#providers';
|
|
32
|
+
return '/settings/agents#providers';
|
|
33
33
|
// The confirmation sits on the task page, behind the password.
|
|
34
34
|
if (action.code === 'confirm-stopped')
|
|
35
35
|
return `/t/${encodeURIComponent(assignment.rootId)}#confirm-stopped`;
|
package/dist/backup-ui.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings → Backups (sprint 8): how the last backup went and one button to
|
|
3
|
-
* back up now, then the schedule
|
|
4
|
-
*
|
|
3
|
+
* back up now, then the schedule, past backups and how to restore. Built from
|
|
4
|
+
* the revamp kit (src/design/KIT.md); the Settings frame adds the title.
|
|
5
5
|
*/
|
|
6
6
|
import { type Html } from "./html.js";
|
|
7
7
|
import type { BackupRun, BackupSettings, LiveBackupState } from "./store.js";
|
|
@@ -10,7 +10,8 @@ export declare const BACKUP_EVERY_HOURS: readonly [1, 6, 12, 24];
|
|
|
10
10
|
/** The continuous copy's choices, in minutes (live-backup.ts LIVE_EVERY_MINUTES; here so the page never loads it). */
|
|
11
11
|
export declare const LIVE_EVERY: readonly [1, 5, 15, 30];
|
|
12
12
|
export declare const MAX_KEEP = 100;
|
|
13
|
-
export
|
|
13
|
+
/** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
|
|
14
|
+
export declare const BACKUP_CSS = "";
|
|
14
15
|
export type BackupView = {
|
|
15
16
|
settings: BackupSettings;
|
|
16
17
|
folder: string;
|
|
@@ -25,7 +26,7 @@ export type BackupView = {
|
|
|
25
26
|
defaultFolder: string;
|
|
26
27
|
};
|
|
27
28
|
};
|
|
28
|
-
export declare function backupHtml(view: BackupView,
|
|
29
|
+
export declare function backupHtml(view: BackupView, said: {
|
|
29
30
|
said?: string | null;
|
|
30
31
|
problem?: string | null;
|
|
31
32
|
}): Html;
|
package/dist/backup-ui.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings → Backups (sprint 8): how the last backup went and one button to
|
|
3
|
-
* back up now, then the schedule
|
|
4
|
-
*
|
|
3
|
+
* back up now, then the schedule, past backups and how to restore. Built from
|
|
4
|
+
* the revamp kit (src/design/KIT.md); the Settings frame adds the title.
|
|
5
5
|
*/
|
|
6
|
-
import { html,
|
|
6
|
+
import { html, joinHtml, postForm } from "./html.js";
|
|
7
7
|
import { bytesWords } from "./storage.js";
|
|
8
8
|
import { whenUtc } from "./when-html.js";
|
|
9
|
+
import { btn, chip, codeLine, formRow, infoLine, input, note, notice, select, settingsRecord, settingsSection } from "./server/kit.js";
|
|
10
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
9
11
|
/** The schedules on offer and the most copies kept. Here rather than in backup.ts so the page never loads `node:sqlite`. */
|
|
10
12
|
export const BACKUP_EVERY_HOURS = [1, 6, 12, 24];
|
|
11
13
|
/** The continuous copy's choices, in minutes (live-backup.ts LIVE_EVERY_MINUTES; here so the page never loads it). */
|
|
@@ -15,53 +17,80 @@ const when = (at) => whenUtc(at);
|
|
|
15
17
|
const whenWords = (at) => `${at.slice(0, 16).replace("T", " ")} UTC`;
|
|
16
18
|
const everyWords = (hours) => hours === 1 ? "Every hour" : hours === 24 ? "Every day" : `Every ${hours} hours`;
|
|
17
19
|
const fileName = (path) => path.slice(path.lastIndexOf("/") + 1);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
`.backups .backup-state form{margin:0}.backups .backup-state button{white-space:nowrap}` +
|
|
21
|
-
`.backups fieldset{border:0;padding:0;margin:0;min-width:0}.backups label{display:grid;gap:4px;margin:12px 0 0;font-size:.875rem}` +
|
|
22
|
-
`.backups input[type=text],.backups input[type=number],.backups input[type=password],.backups select{width:100%;box-sizing:border-box;max-width:100%}` +
|
|
23
|
-
`.backups .pair{display:grid;grid-template-columns:1fr 1fr;gap:12px}.backups button[type=submit]{margin-top:14px}` +
|
|
24
|
-
`.backups details{margin-top:16px}.backups summary{cursor:pointer}.backups ol{list-style:none;padding:0;margin:8px 0 0}` +
|
|
25
|
-
`.backups li{display:flex;gap:8px 12px;flex-wrap:wrap;padding:8px 0;border-top:1px solid var(--line, rgba(0,0,0,.08));font-size:.875rem}` +
|
|
26
|
-
`.backups li .failed{color:var(--danger)}.backups code{white-space:nowrap}.backups pre{overflow-x:auto;margin:8px 0}` +
|
|
27
|
-
`@media (max-width:900px){.backups .backup-state form,.backups .backup-state button{width:100%}.backups .pair{grid-template-columns:1fr}}`;
|
|
20
|
+
/** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
|
|
21
|
+
export const BACKUP_CSS = "";
|
|
28
22
|
/** The continuous copy in one line: its latest verified copy, or why there is none. */
|
|
29
23
|
function liveLine(view) {
|
|
30
24
|
const live = view.live;
|
|
31
25
|
if (live === undefined)
|
|
32
26
|
return "";
|
|
33
27
|
if (!live.state.settings.enabled)
|
|
34
|
-
return
|
|
28
|
+
return note("Continuous copy is off.");
|
|
35
29
|
if (live.state.problem !== null)
|
|
36
|
-
return
|
|
30
|
+
return notice(`The continuous copy failed: ${live.state.problem}`);
|
|
37
31
|
if (live.state.latest === null)
|
|
38
|
-
return
|
|
39
|
-
return html `<p class="
|
|
32
|
+
return note("The first continuous copy is made within a few minutes of Toolroll starting.");
|
|
33
|
+
return html `<p class="k-note" data-live-copy>Continuous copy checked ${when(live.state.latest.madeAt)}, every ${live.state.settings.everyMinutes === 1 ? "minute" : `${live.state.settings.everyMinutes} minutes`}.</p>`;
|
|
40
34
|
}
|
|
41
|
-
/** The
|
|
35
|
+
/** The last backup as one record: what happened, the next one, and Back up now. */
|
|
42
36
|
function state(view) {
|
|
43
37
|
const last = view.runs[0];
|
|
44
38
|
const good = view.runs.find(one => one.ok === true);
|
|
45
39
|
const next = !view.settings.enabled ? "Scheduled backups are off."
|
|
46
40
|
: good === undefined ? "The first scheduled backup runs within a minute or two of the console starting."
|
|
47
41
|
: `Next one about ${whenWords(new Date(Date.parse(good.startedAt) + view.settings.everyHours * 3_600_000).toISOString())}.`;
|
|
42
|
+
const now = postForm("/settings/backups/now", btn("Back up now", { sm: true, type: "submit" }));
|
|
48
43
|
if (last === undefined)
|
|
49
|
-
return
|
|
44
|
+
return settingsRecord({ title: "No backups yet", sub: next, action: now });
|
|
50
45
|
if (last.ok === null)
|
|
51
|
-
return html
|
|
46
|
+
return settingsRecord({ title: html `Backing up ${chip("Running", "live")}`, sub: html `Started ${when(last.startedAt)}.`, action: now });
|
|
52
47
|
if (last.ok === false) {
|
|
53
|
-
return html
|
|
48
|
+
return settingsRecord({ title: html `Last backup failed ${chip("Failed", "failed")}`, attrs: { "data-backup-state": "failed" }, action: now,
|
|
49
|
+
sub: html `${good === undefined ? "No backup has succeeded yet." : html `Last good backup ${when(good.startedAt)}.`} ${next}`,
|
|
50
|
+
more: notice(html `${when(last.startedAt)}: ${last.error ?? "no reason was recorded"}`) });
|
|
54
51
|
}
|
|
55
|
-
return html
|
|
52
|
+
return settingsRecord({ title: html `Backed up ${when(last.startedAt)}`, attrs: { "data-backup-state": "ok" }, action: now,
|
|
53
|
+
sub: `${last.bytes === null ? "" : `${bytesWords(last.bytes)} · `}${next}` });
|
|
56
54
|
}
|
|
57
|
-
|
|
55
|
+
/** One past backup: when, how it went, its file, and whether it was by hand. */
|
|
56
|
+
function runRow(run) {
|
|
57
|
+
const how = run.trigger === "manual" ? "By hand" : "Scheduled";
|
|
58
|
+
const attrs = { "data-backup-run": run.id };
|
|
59
|
+
if (run.ok === true) {
|
|
60
|
+
return settingsRecord({ title: when(run.startedAt), attrs, when: chip("Finished", "done"),
|
|
61
|
+
sub: html `${how}${run.bytes === null ? "" : ` · ${bytesWords(run.bytes)}`}${run.file === null ? "" : html ` · <span class="k-mono">${fileName(run.file)}</span>`}` });
|
|
62
|
+
}
|
|
63
|
+
if (run.ok === false)
|
|
64
|
+
return settingsRecord({ title: when(run.startedAt), attrs, when: chip("Failed", "failed"), sub: `${how} · ${run.error ?? "no reason recorded"}` });
|
|
65
|
+
return settingsRecord({ title: when(run.startedAt), attrs, when: chip("Running", "live"), sub: how });
|
|
66
|
+
}
|
|
67
|
+
export function backupHtml(view, said) {
|
|
58
68
|
const s = view.settings;
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
const answer = said.problem ? notice(said.problem) : said.said ? infoLine(said.said) : "";
|
|
70
|
+
const now = settingsSection({ id: "backup-now", title: "Last backup", body: joinHtml([answer, state(view), liveLine(view)]) });
|
|
71
|
+
const every = select({ id: "backup-every", name: "every", label: "Back up", current: s.enabled ? String(s.everyHours) : "off",
|
|
72
|
+
items: [["off", "Off"], ...BACKUP_EVERY_HOURS.map(hours => [String(hours), everyWords(hours)])] });
|
|
73
|
+
const schedule = settingsSection({ id: "backup-schedule", title: "Schedule", lead: "Only the database is copied: provider keys and sign-in files stay out.",
|
|
74
|
+
body: kitSectionForm({ proof: "backups", action: "/settings/backups", id: "backup-form", gate: true,
|
|
75
|
+
body: joinHtml([
|
|
76
|
+
formRow("Back up", null, every, "inline"),
|
|
77
|
+
formRow("Keep the newest", "How many backups to keep. Older ones are removed.", input({ id: "backup-keep", name: "keep", type: "number", value: String(s.keep),
|
|
78
|
+
attrs: { min: 1, max: MAX_KEEP, step: 1, required: true, "aria-label": "Keep the newest" } }), "inline"),
|
|
79
|
+
formRow("Folder", "Blank: the default folder.", input({ id: "backup-folder", name: "folder", value: s.folder ?? "",
|
|
80
|
+
attrs: { placeholder: view.defaultFolder, spellcheck: "false", "aria-label": "Folder" } })),
|
|
81
|
+
liveFields(view),
|
|
82
|
+
]) }) });
|
|
83
|
+
const runs = view.runs.map(runRow);
|
|
84
|
+
const history = settingsSection({ id: "backup-history", title: "Past backups", state: view.runs.length === 0 ? "" : ` · ${view.runs.length}`,
|
|
85
|
+
lead: html `In <span class="k-mono">${view.folder}</span>.`,
|
|
86
|
+
body: runs.length === 0 ? note("None yet.") : joinHtml(runs) });
|
|
87
|
+
const restore = settingsSection({ id: "backup-restore", title: "Restore a backup", lead: "Stop Toolroll, then run:",
|
|
88
|
+
body: joinHtml([
|
|
89
|
+
codeLine("toolroll restore <file> --dry-run"),
|
|
90
|
+
note(html `The dry run checks the backup (its version and ledger chain) and changes nothing. Run it again without <code class="k-mono">--dry-run</code> to restore; the current database is kept as a copy first.`),
|
|
91
|
+
view.live === undefined ? "" : note(html `Continuous copies are in <span class="k-mono">${view.live.folder}</span>; restore one the same way.`),
|
|
92
|
+
]) });
|
|
93
|
+
return joinHtml([now, schedule, history, restore]);
|
|
65
94
|
}
|
|
66
95
|
/** The continuous copy's two choices, beside the schedule's: how often, and where (blank: beside the database). */
|
|
67
96
|
function liveFields(view) {
|
|
@@ -69,7 +98,11 @@ function liveFields(view) {
|
|
|
69
98
|
if (live === undefined)
|
|
70
99
|
return "";
|
|
71
100
|
const on = live.state.settings;
|
|
72
|
-
const
|
|
73
|
-
...LIVE_EVERY.map(minutes =>
|
|
74
|
-
return
|
|
101
|
+
const every = select({ id: "backup-live-every", name: "live_every", label: "Copy continuously", current: on.enabled ? String(on.everyMinutes) : "off",
|
|
102
|
+
items: [["off", "Off"], ...LIVE_EVERY.map(minutes => [String(minutes), minutes === 1 ? "Every minute" : `Every ${minutes} minutes`])] });
|
|
103
|
+
return joinHtml([
|
|
104
|
+
formRow("Copy continuously", "A checked copy of the database every few minutes, between backups.", every, "inline"),
|
|
105
|
+
formRow("Copy folder", "Blank: beside the database.", input({ id: "backup-live-folder", name: "live_folder", value: on.folder ?? "",
|
|
106
|
+
attrs: { placeholder: live.defaultFolder, spellcheck: "false", "aria-label": "Copy folder" } })),
|
|
107
|
+
]);
|
|
75
108
|
}
|