toolroll 0.9.85 → 0.9.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +4 -3
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -6
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +35 -190
- package/dist/server/render-pages.js +154 -563
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -4
- package/package.json +1 -1
package/dist/release.js
CHANGED
|
@@ -81,6 +81,13 @@ export const REQUIRED_PR_CHECKS_WORKFLOW = {
|
|
|
81
81
|
export const PROTECTION_REPORT_ONLY_CHECKS = [];
|
|
82
82
|
/** The checks main's protection must require at minimum: every required check that isn't report-only. */
|
|
83
83
|
export const PROTECTION_MINIMUM_CHECKS = REQUIRED_PR_CHECKS.filter(name => !PROTECTION_REPORT_ONLY_CHECKS.includes(name));
|
|
84
|
+
/**
|
|
85
|
+
* Pull-request checks a release never waits for or fails on, by exact name and the workflow file that defines them:
|
|
86
|
+
* youiyoux's UI drift check (src/design/KIT.md), which fails a pull request that adds design drift but says nothing
|
|
87
|
+
* about whether a build is safe to release. Only a GitHub Actions run of that name from that workflow is left out; the
|
|
88
|
+
* same name from anywhere else is judged like any other check. Never a required check.
|
|
89
|
+
*/
|
|
90
|
+
export const ADVISORY_PR_CHECKS = { "youiyoux · UI drift": ".github/workflows/youiyoux.yml" };
|
|
84
91
|
/** The checks a release's publication waits for on its tag (.github/workflows/publish.yml, docs/plans/test-matrix.md):
|
|
85
92
|
* the release proof (the tagged tree passed every REQUIRED_PR_CHECKS check on its pull request, read from GitHub) and
|
|
86
93
|
* the publish job that needs it. The tag runs no tests again. Nothing else reported on the merge commit (main-push.yml)
|
|
@@ -130,7 +137,7 @@ export function landedWords(landed) {
|
|
|
130
137
|
}
|
|
131
138
|
const branchFile = (branch) => branch.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
132
139
|
export const journalFile = (stateDir, branch) => join(stateDir, "releases", `${branchFile(branch)}.json`);
|
|
133
|
-
export const resumeCommand = (branch, checkout) => `toolroll release ${branch} --repo ${checkout}`;
|
|
140
|
+
export const resumeCommand = (branch, checkout) => `toolroll release ${releaseBranchArg(branch)} --repo ${checkout}`;
|
|
134
141
|
/** Whole or not at all: a private temp file, synced, renamed into place, its folder synced. */
|
|
135
142
|
export function writeJournal(file, journal) {
|
|
136
143
|
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
@@ -232,7 +239,18 @@ export async function runRelease(options, adapters) {
|
|
|
232
239
|
let freshApproval = false;
|
|
233
240
|
let supersededTask = null;
|
|
234
241
|
const save = () => { journal.updatedAt = adapters.now().toISOString(); writeJournal(file, journal); };
|
|
235
|
-
|
|
242
|
+
/** Tell the progress record about a step (v146). Never throws and never waits: the release goes on whatever it says. */
|
|
243
|
+
const mark = (at, state, detail = null) => {
|
|
244
|
+
const j = journal;
|
|
245
|
+
if (j === null || adapters.progress === undefined || at === "done")
|
|
246
|
+
return;
|
|
247
|
+
try {
|
|
248
|
+
adapters.progress({ taskId: j.task.id, tag: j.tag, version: j.packageVersion, pr: j.pr, step: at, state, at: adapters.now(), detail });
|
|
249
|
+
}
|
|
250
|
+
catch { /* best effort */ }
|
|
251
|
+
};
|
|
252
|
+
const advance = (next) => { const was = journal.step; journal.step = next; journal.stopped = null; save(); if (was !== next)
|
|
253
|
+
mark(was, "done"); };
|
|
236
254
|
const intend = (at) => { journal.intents[at] = adapters.now().toISOString(); save(); };
|
|
237
255
|
const recoverDeploy = async () => {
|
|
238
256
|
const j = journal;
|
|
@@ -266,13 +284,18 @@ export async function runRelease(options, adapters) {
|
|
|
266
284
|
unreachable = networkWords(error);
|
|
267
285
|
answer = null;
|
|
268
286
|
}
|
|
269
|
-
if (answer !== null)
|
|
287
|
+
if (answer !== null) {
|
|
288
|
+
if (said && journal !== null)
|
|
289
|
+
mark(journal.step, "running");
|
|
270
290
|
return answer;
|
|
291
|
+
}
|
|
271
292
|
if (adapters.now().getTime() >= deadline)
|
|
272
293
|
throw new Stop("timeout", `Still waiting for ${what} after ${limits[at]} minutes${unreachable === null ? "" : ` (GitHub last didn't answer: ${unreachable})`}.`);
|
|
273
294
|
if (!said) {
|
|
274
295
|
say(` waiting for ${what} (up to ${limits[at]} min)`);
|
|
275
296
|
said = true;
|
|
297
|
+
if (journal !== null)
|
|
298
|
+
mark(journal.step, "waiting", `waiting for ${what}`);
|
|
276
299
|
}
|
|
277
300
|
await adapters.sleep(pollMs);
|
|
278
301
|
}
|
|
@@ -584,6 +607,8 @@ export async function runRelease(options, adapters) {
|
|
|
584
607
|
const watch = async (action) => { if (rel !== undefined)
|
|
585
608
|
await identity(action, true); };
|
|
586
609
|
const restart = async (head) => {
|
|
610
|
+
// The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
|
|
611
|
+
mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
|
|
587
612
|
const tree = await adapters.repo.tree(head);
|
|
588
613
|
if (rel !== undefined && j.authority && epoch !== null) {
|
|
589
614
|
say(` ${j.branch} moved to ${head.slice(0, 7)}: checking whether the approval carries`);
|
|
@@ -663,7 +688,7 @@ export async function runRelease(options, adapters) {
|
|
|
663
688
|
}
|
|
664
689
|
const verdict = prChecksVerdict(runs, j.candidate.sha, REQUIRED_PR_CHECKS, { workflow: REQUIRED_PR_CHECKS_WORKFLOW });
|
|
665
690
|
if (verdict.state === "failed") {
|
|
666
|
-
throw new Stop("ci-failed", `GitHub's checks failed on ${j.candidate.sha.slice(0, 7)}: ${verdict.failed.join(", ")}. ${nothing}. ${j.approval === null ? "" : `The approval covers ${j.candidate.sha.slice(0, 7)} only. `}Rerun the failed job on GitHub and run \`${resume}\` again, or fix the branch and release the new commit with \`toolroll release ${j.branch} --repo ${j.checkout} --new\`.`);
|
|
691
|
+
throw new Stop("ci-failed", `GitHub's checks failed on ${j.candidate.sha.slice(0, 7)}: ${verdict.failed.join(", ")}. ${nothing}. ${j.approval === null ? "" : `The approval covers ${j.candidate.sha.slice(0, 7)} only. `}Rerun the failed job on GitHub and run \`${resume}\` again, or fix the branch and release the new commit with \`toolroll release ${releaseBranchArg(j.branch)} --repo ${j.checkout} --new\`.`);
|
|
667
692
|
}
|
|
668
693
|
if (verdict.state !== "passed")
|
|
669
694
|
return false;
|
|
@@ -739,6 +764,7 @@ export async function runRelease(options, adapters) {
|
|
|
739
764
|
while (j.step !== "done" && homebrewLeft === null) {
|
|
740
765
|
step = j.step;
|
|
741
766
|
say(`• ${j.step}`);
|
|
767
|
+
mark(j.step, "running");
|
|
742
768
|
try {
|
|
743
769
|
switch (j.step) {
|
|
744
770
|
case "gate": {
|
|
@@ -1217,6 +1243,7 @@ export async function runRelease(options, adapters) {
|
|
|
1217
1243
|
save();
|
|
1218
1244
|
if (progress.state === "pending" || progress.state === "failed") {
|
|
1219
1245
|
homebrewLeft = { state: progress.state, why: progress.why };
|
|
1246
|
+
mark("homebrew", progress.state === "failed" ? "failed" : "waiting", progress.why);
|
|
1220
1247
|
break;
|
|
1221
1248
|
}
|
|
1222
1249
|
advance("done");
|
|
@@ -1254,6 +1281,7 @@ export async function runRelease(options, adapters) {
|
|
|
1254
1281
|
save();
|
|
1255
1282
|
}
|
|
1256
1283
|
catch { /* the stop is still reported */ }
|
|
1284
|
+
mark(journal.step, "failed", stop.message);
|
|
1257
1285
|
}
|
|
1258
1286
|
return { ok: false, reason: stop.reason, message: stop.message, step: journal?.step ?? step, journal, file, resume };
|
|
1259
1287
|
}
|
|
@@ -1441,6 +1469,10 @@ export function prChecksVerdict(runs, head, required, options = {}) {
|
|
|
1441
1469
|
const failed = [], waiting = [], failures = [];
|
|
1442
1470
|
const valid = [];
|
|
1443
1471
|
for (const run of runs) {
|
|
1472
|
+
// youiyoux's drift check from its own workflow neither holds nor fails a release (ADVISORY_PR_CHECKS).
|
|
1473
|
+
const advisory = Object.hasOwn(ADVISORY_PR_CHECKS, run.name) ? ADVISORY_PR_CHECKS[run.name] : undefined;
|
|
1474
|
+
if (advisory !== undefined && !required.includes(run.name) && run.app === "github-actions" && workflowIs(run.workflowPath, advisory))
|
|
1475
|
+
continue;
|
|
1444
1476
|
if (run.head !== head) {
|
|
1445
1477
|
failed.push(`${run.name} (reported for ${run.head == null ? "no commit" : run.head.slice(0, 7)}, not ${head.slice(0, 7)})`);
|
|
1446
1478
|
continue;
|
|
@@ -1577,3 +1609,7 @@ export function workflowIs(path, file) {
|
|
|
1577
1609
|
const ref = path.startsWith(`${file}@`) ? path.slice(file.length + 1) : "";
|
|
1578
1610
|
return /^[A-Za-z0-9._+/-]+$/.test(ref) && !/\.ya?ml$/i.test(ref);
|
|
1579
1611
|
}
|
|
1612
|
+
/** The words `toolroll release` reads as its own (release-cli.ts), never as a branch: such a branch is named with --branch. */
|
|
1613
|
+
const RELEASE_WORDS = new Set(["status", "nightly", "carry", "revoke", "ask-every-commit", "unsandboxed-carry", "auto-approve", "publish-ok"]);
|
|
1614
|
+
/** How a person names this branch to `toolroll release` again. */
|
|
1615
|
+
export function releaseBranchArg(branch) { return RELEASE_WORDS.has(branch) ? `--branch ${branch}` : branch; }
|
package/dist/releases.js
CHANGED
|
@@ -129,7 +129,7 @@ export function notifySecurityRelease(store, release, current, updateCommand, no
|
|
|
129
129
|
dedupeKey: `release:security:${newer.version}:${recipient}`,
|
|
130
130
|
kind: "security-release",
|
|
131
131
|
pushClass: "attention",
|
|
132
|
-
link: "/settings#updates",
|
|
132
|
+
link: "/settings/system#updates",
|
|
133
133
|
subject: `Toolroll ${newer.version} is a security release`,
|
|
134
134
|
body: `This computer runs ${current}. Update with \`${updateCommand}\`. Notes: ${newer.url}`,
|
|
135
135
|
recipient,
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ApiTokenRow, Database } from "./store.js";
|
|
7
7
|
import { type Html } from "./html.js";
|
|
8
|
-
|
|
8
|
+
/** The old card look; the section is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
9
|
+
export declare const REQUEST_LIMITS_CSS = "";
|
|
9
10
|
/** "120 requests a minute · 10,000 a day" for one token's line. */
|
|
10
11
|
export declare function tokenLimitWords(db: Database, token: ApiTokenRow): string;
|
|
11
|
-
/** The operator's
|
|
12
|
+
/** The operator's section: everyone's tokens, or one token. Blank fields use the default shown. Saving takes the
|
|
13
|
+
* operator's password (or Passwordless, or a fresh identity-provider sign-in). */
|
|
12
14
|
export declare function requestLimitsHtml(db: Database, tokens: readonly ApiTokenRow[], now: number): Html;
|
|
@@ -1,31 +1,42 @@
|
|
|
1
|
-
import { html,
|
|
1
|
+
import { html, joinHtml } from "./html.js";
|
|
2
|
+
import { btn, disclose, formRow, input, note, select, settingsSection } from "./server/kit.js";
|
|
3
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
2
4
|
import { effectiveLimits, limitOverrides, PER_DAY_MAX, PER_MINUTE_MAX, REQUEST_BUDGET_DEFAULTS } from "./request-budget.js";
|
|
3
5
|
const n = (value) => value.toLocaleString("en-US");
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
`.credentials .limits h3{font-size:.95rem;margin:4px 0 0}@media(max-width:600px){.credentials .limits .pair{grid-template-columns:1fr}.credentials .limits input,.credentials .limits select{font-size:16px}}`;
|
|
6
|
+
/** The old card look; the section is built from the kit now. Kept for chrome.ts, which still concatenates it. */
|
|
7
|
+
export const REQUEST_LIMITS_CSS = "";
|
|
7
8
|
/** "120 requests a minute · 10,000 a day" for one token's line. */
|
|
8
9
|
export function tokenLimitWords(db, token) {
|
|
9
10
|
const limits = effectiveLimits(db, token.id, token.access);
|
|
10
11
|
return `${n(limits.perMinute)} requests a minute · ${n(limits.perDay)} a day`;
|
|
11
12
|
}
|
|
12
|
-
/** The operator's
|
|
13
|
+
/** The operator's section: everyone's tokens, or one token. Blank fields use the default shown. Saving takes the
|
|
14
|
+
* operator's password (or Passwordless, or a fresh identity-provider sign-in). */
|
|
13
15
|
export function requestLimitsHtml(db, tokens, now) {
|
|
14
16
|
const overrides = limitOverrides(db);
|
|
15
17
|
const everyone = overrides.get("*") ?? { readPerMinute: null, actPerMinute: null, perDay: null };
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
18
|
+
const number = (id, name, label, value, placeholder, max) => input({ id, name, type: "number", value: value === null ? "" : String(value), attrs: { min: 1, max, step: 1, inputmode: "numeric", placeholder, "aria-label": label } });
|
|
19
|
+
const installation = kitSectionForm({ proof: "request-limits", action: "/settings/request-limits", id: "limits-everyone", gate: true, hidden: { target: "*", action: "save" },
|
|
20
|
+
save: { label: "Save limits" }, confirm: "Change request limits for everyone's tokens",
|
|
21
|
+
body: joinHtml([
|
|
22
|
+
formRow("Read tokens, a minute", `Default ${n(REQUEST_BUDGET_DEFAULTS.read)}.`, number("limits-read", "read-per-minute", "Read tokens, a minute", everyone.readPerMinute, String(REQUEST_BUDGET_DEFAULTS.read), PER_MINUTE_MAX)),
|
|
23
|
+
formRow("Act tokens, a minute", `Default ${n(REQUEST_BUDGET_DEFAULTS.act)}.`, number("limits-act", "act-per-minute", "Act tokens, a minute", everyone.actPerMinute, String(REQUEST_BUDGET_DEFAULTS.act), PER_MINUTE_MAX)),
|
|
24
|
+
formRow("Each token, a day", `Default ${n(REQUEST_BUDGET_DEFAULTS.day)}.`, number("limits-day", "per-day", "Each token, a day", everyone.perDay, String(REQUEST_BUDGET_DEFAULTS.day), PER_DAY_MAX)),
|
|
25
|
+
]) });
|
|
26
|
+
const reset = kitSectionForm({ proof: "request-limits", action: "/settings/request-limits", id: "limits-everyone-clear", hidden: { target: "*", action: "clear" },
|
|
27
|
+
save: { label: "Use defaults for everyone", variant: "outline" }, confirm: "Put everyone's tokens back on the default limits", body: html `` });
|
|
19
28
|
const live = tokens.filter(one => one.revokedAt === null && Date.parse(one.expiresAt) > now);
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const one = live.length === 0 ? "" : disclose("Limits for one token", kitSectionForm({ proof: "request-limits", action: "/settings/request-limits", id: "limits-one",
|
|
30
|
+
save: { label: "Save limits", name: "action", value: "save" }, confirm: "Change one token's request limits",
|
|
31
|
+
body: joinHtml([
|
|
32
|
+
formRow("Token", null, select({ id: "limits-target", name: "target", label: "Token", wide: true, items: live.map(row => {
|
|
33
|
+
const set = overrides.get(row.id);
|
|
34
|
+
return [row.id, `${row.name} (${row.account}, ${row.access === "act" ? "act" : "read"})${set === undefined ? "" : " · own limits"}`];
|
|
35
|
+
}) })),
|
|
36
|
+
formRow("A minute", "Blank uses everyone's.", number("limits-one-minute", "per-minute", "A minute", null, "Everyone's", PER_MINUTE_MAX)),
|
|
37
|
+
formRow("A day", "Blank uses everyone's.", number("limits-one-day", "per-day", "A day", null, "Everyone's", PER_DAY_MAX)),
|
|
38
|
+
html `<div class="k-acts">${btn("Use everyone's limits", { sm: true, type: "submit", attrs: { name: "action", value: "clear" } })}</div>`,
|
|
39
|
+
]) }));
|
|
40
|
+
return settingsSection({ id: "request-limits", title: "Request limits", lead: "How many requests each API token may make. A token over its limit is refused until the minute or day resets, and nothing it asked for runs.",
|
|
41
|
+
body: joinHtml([installation, reset, one, note("Only an instance operator sees and changes these.")]) });
|
|
31
42
|
}
|
|
@@ -2,6 +2,7 @@ import { checkoutRunner, ranAtLine, widestRan } from "./invoke.js";
|
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { refArchived } from "./task-archive.js";
|
|
5
6
|
import { belongsToRelease } from "./release-authority.js";
|
|
6
7
|
import { run as execRun } from "./exec.js";
|
|
7
8
|
import { hardenedGit } from "./teardown-git.js";
|
|
@@ -126,6 +127,9 @@ function checkTarget(store, runId, level) {
|
|
|
126
127
|
return refuse("no-commit", "This result has no finished commit to check.");
|
|
127
128
|
if (ref.repo === null)
|
|
128
129
|
return refuse("unplaced", "This result isn't in a project, so there is no check to run.");
|
|
130
|
+
// A put-away task (v145) runs no checks: asked for or already waiting, it is refused until restored.
|
|
131
|
+
if (refArchived(store, ref.id))
|
|
132
|
+
return refuse("archived", "This task is put away. Restore it first.");
|
|
129
133
|
const chosen = checkCommandFor(store, ref.repo, level);
|
|
130
134
|
if (chosen.command === null)
|
|
131
135
|
return refuse("no-check", `${ref.repo} has no approved ${level === "quick" ? "quick or full" : "full"} check. Set one in Settings → Projects → Checks.`);
|
package/dist/retention-ui.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings → Retention (v105): how long each kind of data is kept, and what
|
|
3
3
|
* the next daily sweep would remove. An instance operator's page; changes
|
|
4
|
-
* take a step-up.
|
|
4
|
+
* take a step-up. Built from the revamp kit; the Settings frame adds the title.
|
|
5
5
|
*/
|
|
6
6
|
import { type Html } from "./html.js";
|
|
7
7
|
import { type RetentionCount, type RetentionKind, type RetentionPeriods } from "./retention.js";
|
|
8
|
-
export
|
|
8
|
+
/** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
|
|
9
|
+
export declare const RETENTION_CSS = "";
|
|
9
10
|
/** `chosen`: the kinds someone chose a period for; the rest keep their default. */
|
|
10
11
|
export type RetentionView = {
|
|
11
12
|
periods: RetentionPeriods;
|
|
@@ -14,7 +15,7 @@ export type RetentionView = {
|
|
|
14
15
|
lastSweep: string | null;
|
|
15
16
|
csrf: string;
|
|
16
17
|
};
|
|
17
|
-
export declare function retentionHtml(view: RetentionView,
|
|
18
|
+
export declare function retentionHtml(view: RetentionView, said: {
|
|
18
19
|
said?: string | null;
|
|
19
20
|
problem?: string | null;
|
|
20
21
|
}): Html;
|
package/dist/retention-ui.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Settings → Retention (v105): how long each kind of data is kept, and what
|
|
3
3
|
* the next daily sweep would remove. An instance operator's page; changes
|
|
4
|
-
* take a step-up.
|
|
4
|
+
* take a step-up. Built from the revamp kit; the Settings frame adds the title.
|
|
5
5
|
*/
|
|
6
|
-
import { html,
|
|
6
|
+
import { html, joinHtml } from "./html.js";
|
|
7
7
|
import { bytesWords } from "./storage.js";
|
|
8
8
|
import { RETENTION_KINDS, countWords, periodChoices, periodLabel } from "./retention.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
`@media (max-width:560px){.retention .kind{grid-template-columns:minmax(0,1fr)}.retention .kind select{grid-column:1;grid-row:auto}}`;
|
|
9
|
+
import { formRow, infoLine, note, notice, select, settingsSection } from "./server/kit.js";
|
|
10
|
+
import { kitSectionForm } from "./settings-kit.js";
|
|
11
|
+
/** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
|
|
12
|
+
export const RETENTION_CSS = "";
|
|
14
13
|
const listWords = (parts) => parts.length < 2 ? parts.join("") : `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}`;
|
|
15
14
|
/** The next sweep in a line: what goes and about how much space, or that nothing is due. */
|
|
16
15
|
function nextWords(view) {
|
|
@@ -22,14 +21,18 @@ function nextWords(view) {
|
|
|
22
21
|
const bytes = due.reduce((sum, one) => sum + one.bytes, 0);
|
|
23
22
|
return `The next daily sweep removes ${listWords(due.map(countWords))} (about ${bytesWords(bytes)}).`;
|
|
24
23
|
}
|
|
25
|
-
export function retentionHtml(view,
|
|
26
|
-
const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
|
|
24
|
+
export function retentionHtml(view, said) {
|
|
27
25
|
const rows = RETENTION_KINDS.map(({ kind, label, detail }) => {
|
|
28
26
|
const current = view.periods[kind];
|
|
29
27
|
const chosen = view.chosen.includes(kind);
|
|
30
|
-
const
|
|
31
|
-
return
|
|
28
|
+
const items = periodChoices(kind, current).map(days => [days === null ? "forever" : String(days), periodLabel(kind, days, chosen || days !== current)]);
|
|
29
|
+
return formRow(label, detail, select({ id: `keep-${kind}`, name: kind, label, current: current === null ? "forever" : String(current), items }), "inline");
|
|
32
30
|
});
|
|
33
31
|
const last = view.lastSweep === null ? "" : ` Last sweep ${view.lastSweep.slice(0, 10)}.`;
|
|
34
|
-
return
|
|
32
|
+
return settingsSection({ id: "retention", title: "How long data is kept", lead: html `<span data-retention-next>${nextWords(view)}${last}</span>`,
|
|
33
|
+
body: joinHtml([
|
|
34
|
+
said.problem ? notice(said.problem) : said.said ? infoLine(said.said) : "",
|
|
35
|
+
kitSectionForm({ proof: "retention", action: "/settings/retention", id: "retention-form", gate: true, body: joinHtml(rows) }),
|
|
36
|
+
note("Never removed: the action ledger, and anything a task still needs (unfinished tasks, results Ready for review, anything on hold)."),
|
|
37
|
+
]) });
|
|
35
38
|
}
|
package/dist/scope.d.ts
CHANGED
|
@@ -597,6 +597,11 @@ export type ApproveResult = {
|
|
|
597
597
|
| {
|
|
598
598
|
ok: false;
|
|
599
599
|
reason: "machine-proof";
|
|
600
|
+
}
|
|
601
|
+
/** v145 (task-tidy.md): the task is put away; it is restored before anyone can approve it. */
|
|
602
|
+
| {
|
|
603
|
+
ok: false;
|
|
604
|
+
reason: "archived";
|
|
600
605
|
};
|
|
601
606
|
/**
|
|
602
607
|
* Whether this name-and-token pair is a person the store knows. Shared by
|
|
@@ -999,3 +1004,6 @@ export declare function releaseApprovalOf(store: Store, taskId: string, _now?: D
|
|
|
999
1004
|
ok: false;
|
|
1000
1005
|
why: string;
|
|
1001
1006
|
};
|
|
1007
|
+
/** When a password typed at `at` (ISO) stops standing in, for a page's words (ISO); null when `at` isn't a time. Words
|
|
1008
|
+
* only: freshnessFor decides whether it stands in, at the moment of use. */
|
|
1009
|
+
export declare function freshUntil(at: string): string | null;
|
package/dist/scope.js
CHANGED
|
@@ -48,6 +48,7 @@ import { z } from "zod";
|
|
|
48
48
|
import {} from "./contracts/acceptance-terms.js";
|
|
49
49
|
import { parseContract } from "./contracts/contract.js";
|
|
50
50
|
import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
|
|
51
|
+
import { taskArchived } from "./task-archive.js";
|
|
51
52
|
import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
|
|
52
53
|
import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
|
|
53
54
|
/** Same shape as a runner's credential, for the same reasons. */
|
|
@@ -1074,6 +1075,9 @@ export function approve(store, taskId, by, now, sawDigest, token, mutation = {})
|
|
|
1074
1075
|
const scope = store.getScope(taskId);
|
|
1075
1076
|
if (scope === null)
|
|
1076
1077
|
return { ok: false, reason: "no-scope" };
|
|
1078
|
+
// A put-away task (v145) is restored first; sealScopeApproval refuses it too, on every road.
|
|
1079
|
+
if (taskArchived(store, taskId))
|
|
1080
|
+
return { ok: false, reason: "archived" };
|
|
1077
1081
|
// The digest is required, not optional. An operator reads a scope, somebody
|
|
1078
1082
|
// rewrites it, and an approval that did not name what it saw would agree to
|
|
1079
1083
|
// the new one in silence — which is the whole failure this guards.
|
|
@@ -1472,6 +1476,9 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
|
|
|
1472
1476
|
return { applies: false };
|
|
1473
1477
|
const { authority: a, carry } = found;
|
|
1474
1478
|
const refuse = (why) => ({ applies: true, ok: false, why, authority: a });
|
|
1479
|
+
// A put-away task (v145, task-tidy.md), or a put-away anchor, deploys, merges and carries nothing: restore it first.
|
|
1480
|
+
if (taskArchived(store, taskId) || taskArchived(store, a.anchorTask))
|
|
1481
|
+
return refuse("the release task is put away");
|
|
1475
1482
|
const newest = newestReleaseGeneration(store, a.repo, a.version, a.pr);
|
|
1476
1483
|
if (newest === null || newest.id !== a.id)
|
|
1477
1484
|
return refuse("a newer approval of this release replaced this one");
|
|
@@ -1603,3 +1610,9 @@ export function releaseApprovalOf(store, taskId, _now = new Date()) {
|
|
|
1603
1610
|
const auto = autoReleaseApprovalOf(store, taskId, "auto-green");
|
|
1604
1611
|
return auto.ok ? { ok: true, by: auto.by, at: auto.at, method: "auto-green" } : auto;
|
|
1605
1612
|
}
|
|
1613
|
+
/** When a password typed at `at` (ISO) stops standing in, for a page's words (ISO); null when `at` isn't a time. Words
|
|
1614
|
+
* only: freshnessFor decides whether it stands in, at the moment of use. */
|
|
1615
|
+
export function freshUntil(at) {
|
|
1616
|
+
const ms = Date.parse(at);
|
|
1617
|
+
return Number.isFinite(ms) ? new Date(ms + FRESH_WINDOW_MS).toISOString() : null;
|
|
1618
|
+
}
|
package/dist/serve.d.ts
CHANGED
|
@@ -7,6 +7,5 @@ export { TASK_COMPOSER_MODES, type TaskComposerMode } from "./server/render-chat
|
|
|
7
7
|
export { inboxFingerprints, parseInboxTab, queueScript, type InboxTab } from "./server/render-pages.js";
|
|
8
8
|
export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
|
|
9
9
|
export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches, type ReviewFileRow, type ReviewPriority, type ReviewQueueFacts } from "./server/render-results.js";
|
|
10
|
-
export { settingsGroups, settingsTiles } from "./server/render-settings.js";
|
|
11
10
|
export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords, type RouteView } from "./server/render-tasks.js";
|
|
12
11
|
export { SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, type DecisionServer } from "./server/session.js";
|
package/dist/serve.js
CHANGED
|
@@ -157,9 +157,11 @@ import { chromeScript, DEMO_BANNER, DEMO_BANNER_SHORT, focusDocument, KBD_HELP,
|
|
|
157
157
|
import { BODY_CAP, form, loginHref, matchTaskPath, page, projectChatHref, redactedPath, redirect, respond, safeReturn, SHUTDOWN_WAIT_MS, taskChatHref } from "./server/http.js";
|
|
158
158
|
import { decisionsFor, LEAD_BY_DEFAULT_FACT, leadBrowserMessages, leadChatVersion, NOT_STARTED_SESSION, TASK_COMPOSER_MODES, teamProposalCardParts } from "./server/render-chat.js";
|
|
159
159
|
import {} from "./server/render-pages.js";
|
|
160
|
-
import { ssoStepUps, wrongHostPage } from "./server/render-people.js";
|
|
160
|
+
import { serverErrorPage, ssoStepUps, wrongHostPage } from "./server/render-people.js";
|
|
161
161
|
import { NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, PersistentSessions, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, sessionWithinLifetime, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
|
|
162
162
|
import { createTasksHandlers } from './server/tasks.js';
|
|
163
|
+
import { KIT_BEHAVIOR_MARKUP, KIT_STANDALONE_SCRIPT } from './server/kit-standalone.js';
|
|
164
|
+
import { createWelcomeHandlers } from './server/welcome.js';
|
|
163
165
|
import { DEFAULT_GUARD_POLICY, passwordGuardOf, provenPasswordAccount, SourceBudget, withPasswordSource } from "./sign-in-guard.js";
|
|
164
166
|
import { sourceKey } from "./source-key.js";
|
|
165
167
|
import { readSsoSettings } from "./sso-settings.js";
|
|
@@ -670,14 +672,26 @@ export function createDecisionServer(options) {
|
|
|
670
672
|
// A GET or HEAD only reads (docs/plans/get-side-effects.md): store reads that would tidy up as they go don't.
|
|
671
673
|
const answer = () => handle(request, response);
|
|
672
674
|
void withPasswordSource(joinSourceOf(request), () => request.method === "GET" || request.method === "HEAD" ? asReadOnlyRequest(answer) : answer()).catch(error => {
|
|
673
|
-
// Every unhandled error is logged (v99): the path and the message, never the request's body or query.
|
|
674
|
-
|
|
675
|
+
// Every unhandled error is logged (v99): the path and the message, never the request's body or query. The reference
|
|
676
|
+
// is what a person's error page shows, so the log line can be found from it.
|
|
677
|
+
const reference = `err_${randomBytes(4).toString("hex")}`;
|
|
678
|
+
logEvent("error", "serve.error", { method: request.method, path: redactedPath(new URL(request.url ?? "/", "http://placeholder").pathname), error: error instanceof Error ? error.message : String(error), reference });
|
|
675
679
|
if (envValue(process.env, "SERVE_DEBUG") === "1")
|
|
676
680
|
console.error("SERVE ERROR:", error);
|
|
677
681
|
if (!response.headersSent) {
|
|
678
682
|
const updating = error instanceof Error && error.message.includes(UPDATE_PAUSED);
|
|
679
683
|
if (updating)
|
|
680
684
|
response.setHeader("Retry-After", "5");
|
|
685
|
+
// A browser asking for a page gets a page (Wave E): what happened, whether anything changed, the reference. It
|
|
686
|
+
// holds no error text. Everything else keeps the plain answer.
|
|
687
|
+
if (/\btext\/html\b/.test(String(request.headers.accept ?? "")) && (request.method === "GET" || request.method === "POST")) {
|
|
688
|
+
const read = request.method === "GET";
|
|
689
|
+
const at = new URL(request.url ?? "/", "http://placeholder");
|
|
690
|
+
try {
|
|
691
|
+
return page(response, updating ? 503 : 500, serverErrorPage({ updating, read, reference, at: new Date(), retry: read ? at.pathname + at.search : null }));
|
|
692
|
+
}
|
|
693
|
+
catch { /* the plain answer below */ }
|
|
694
|
+
}
|
|
681
695
|
respond(response, updating ? 503 : 500, "text/plain; charset=utf-8", updating ? UPDATE_PAUSED : "something broke");
|
|
682
696
|
}
|
|
683
697
|
else {
|
|
@@ -1058,6 +1072,19 @@ export function createDecisionServer(options) {
|
|
|
1058
1072
|
}
|
|
1059
1073
|
project = wanted;
|
|
1060
1074
|
}
|
|
1075
|
+
// Home (UI revamp wave B) shows every project unless its address names one (proved the same way), and the rail's
|
|
1076
|
+
// and the dock's Needs-you count follow that same one scope.
|
|
1077
|
+
if (route.id === 'home') {
|
|
1078
|
+
const wanted = url.searchParams.get('project');
|
|
1079
|
+
if (wanted !== null && (url.searchParams.getAll('project').length !== 1 || !visible(wanted) ||
|
|
1080
|
+
![...managedRepos(), ...store.listProjects().map(one => one.path)].includes(wanted))) {
|
|
1081
|
+
return refuse(response, who, 404, 'That project is not available in this workspace.', '/');
|
|
1082
|
+
}
|
|
1083
|
+
project = wanted;
|
|
1084
|
+
}
|
|
1085
|
+
// All projects, asked for by name on Tasks (its project filter): the rolled-up list, admission-bound as ever.
|
|
1086
|
+
if (route.id === 'work' && url.searchParams.get('all') === '1' && !url.searchParams.has('project') && url.searchParams.getAll('all').length === 1)
|
|
1087
|
+
project = null;
|
|
1061
1088
|
// Every page's Tasks count covers this one project view, whatever project the page itself shows.
|
|
1062
1089
|
const facts = requestContext.getStore();
|
|
1063
1090
|
if (facts !== undefined)
|
|
@@ -1618,8 +1645,22 @@ export function createDecisionServer(options) {
|
|
|
1618
1645
|
catchUpHtml: extras.catchUpHtml === undefined ? '' : htmlString(extras.catchUpHtml), controlsHtml: extras.controlsHtml === undefined ? '' : htmlString(extras.controlsHtml), notices, view: extras.view ?? null,
|
|
1619
1646
|
...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
|
|
1620
1647
|
pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml === null ? null : htmlString(extras.pageHtml),
|
|
1621
|
-
navigation:
|
|
1648
|
+
navigation: browserNavigationOf(currentPath, s.chrome.project, needsYou, tasksProject, s.chrome.inboxLabel),
|
|
1649
|
+
// The one Needs-you count (tasks only), with the scope it covers: the Tasks list's own count for this project
|
|
1650
|
+
// view (or every project), so the rail, the phone header and Home all read the same number.
|
|
1651
|
+
needsYou: { count: needsYou, saturated: s.chrome.inboxSaturated === true, scope: tasksProject ?? null,
|
|
1652
|
+
label: s.chrome.inboxLabel ?? `${needsYou} ${needsYou === 1 ? 'needs' : 'need'} you`,
|
|
1653
|
+
// Every project, said by name (all=1), so a session's own open project never narrows the list the count opens.
|
|
1654
|
+
href: tasksProject === null ? '/work?all=1&view=needs-you' : `/work?project=${encodeURIComponent(tasksProject)}&view=needs-you` },
|
|
1622
1655
|
chats,
|
|
1656
|
+
// The same count per project (the rail's project rows), read from the same work index totals.
|
|
1657
|
+
...(() => { if (sensitive)
|
|
1658
|
+
return {}; try {
|
|
1659
|
+
return { needsYouByProject: projectCounts(clock()).map(one => ({ path: one.repo, count: one.totals['needs-you'] })) };
|
|
1660
|
+
}
|
|
1661
|
+
catch {
|
|
1662
|
+
return {};
|
|
1663
|
+
} })(),
|
|
1623
1664
|
...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
|
|
1624
1665
|
...(s.chrome.signIn === undefined ? {} : { signIn: s.chrome.signIn }),
|
|
1625
1666
|
...(s.chrome.update === undefined ? {} : { update: s.chrome.update }),
|
|
@@ -1662,7 +1703,10 @@ export function createDecisionServer(options) {
|
|
|
1662
1703
|
const functional = s.functional?.script ?? "";
|
|
1663
1704
|
// Sensitive pages strip the palette and keys but keep the sidebar toggle.
|
|
1664
1705
|
const sensitiveChrome = sensitive && s.chrome !== undefined ? sidebarScript() : "";
|
|
1665
|
-
|
|
1706
|
+
// Kit markup the workspace bundle would have wired (a composer, an overlay, a switch, an idea) gets the kit's own small
|
|
1707
|
+
// behaviour here (kit-standalone.ts); never on a sensitive page, which carries no chrome additions.
|
|
1708
|
+
const kitBehavior = chromeLayer && KIT_BEHAVIOR_MARKUP.test(htmlString(s.body)) ? KIT_STANDALONE_SCRIPT : "";
|
|
1709
|
+
const script = functional + (chromeLayer ? chromeScript() + WORKSPACE_MOTION_SCRIPT + kitBehavior : sensitiveChrome);
|
|
1666
1710
|
const nonce = script === "" ? undefined : randomBytes(16).toString("base64");
|
|
1667
1711
|
const body = chromeLayer
|
|
1668
1712
|
? html `${s.body}\n${paletteTagCached(s.chrome?.project ?? null)}\n${KBD_HELP}`
|
|
@@ -1774,7 +1818,7 @@ export function createDecisionServer(options) {
|
|
|
1774
1818
|
const update = updateFacts();
|
|
1775
1819
|
if (update === null || !update.newer || update.release === null || facts.updateSeen === update.release.version)
|
|
1776
1820
|
return {};
|
|
1777
|
-
return { update: { version: update.release.version, security: update.release.security, href: "/settings#updates", dismissHref: "/settings/updates/dismiss" } };
|
|
1821
|
+
return { update: { version: update.release.version, security: update.release.security, href: "/settings/system#updates", dismissHref: "/settings/updates/dismiss" } };
|
|
1778
1822
|
})(),
|
|
1779
1823
|
...(() => {
|
|
1780
1824
|
const databaseFile = store.databaseFile();
|
|
@@ -2301,6 +2345,7 @@ export function createDecisionServer(options) {
|
|
|
2301
2345
|
...tasksHandlers.registrations, ...flowsHandlers.registrations, ...chatHandlers.registrations,
|
|
2302
2346
|
...settingsHandlers.registrations, ...peopleHandlers.registrations, ...pagesHandlers.registrations,
|
|
2303
2347
|
...remoteHandlers.registrations, ...liveHandlers.registrations, ...terminalHandlers.registrations, ...createSweepHandlers(handlerRuntime).registrations,
|
|
2348
|
+
...createWelcomeHandlers(handlerRuntime).registrations,
|
|
2304
2349
|
]);
|
|
2305
2350
|
/** The row's role, after admission and before its one handler. */
|
|
2306
2351
|
function dispatchConsole(route, ctx) {
|
|
@@ -2333,7 +2378,6 @@ export { TASK_COMPOSER_MODES } from "./server/render-chat.js";
|
|
|
2333
2378
|
export { inboxFingerprints, parseInboxTab, queueScript } from "./server/render-pages.js";
|
|
2334
2379
|
export { ssoStepUps, wrongHostPage } from "./server/render-people.js";
|
|
2335
2380
|
export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainConclusionOf, rankReviewQueue, reviewFilePriority, reviewPriorityOf, runFactsFragment, withinSignedTouches } from "./server/render-results.js";
|
|
2336
|
-
export { settingsGroups, settingsTiles } from "./server/render-settings.js";
|
|
2337
2381
|
export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords } from "./server/render-tasks.js";
|
|
2338
2382
|
export { SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
|
|
2339
2383
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BrowserAgentPhase, BrowserAgentsSystem, BrowserComputers } from "../browser-workspace.js";
|
|
2
|
+
import type { Store, WorktreeRow } from "../store.js";
|
|
3
|
+
/** Every registered computer that isn't retired, plus a retired one still holding reserved work, as Agents lists it. */
|
|
4
|
+
export declare function computersOf(store: Store, now: Date, visible: (repo: string | null) => boolean, manage: boolean): BrowserComputers;
|
|
5
|
+
/** Which provider and model runs each phase for the open project (or the installation), and where that choice comes from. */
|
|
6
|
+
export declare function phasesOf(store: Store, project: string | null): BrowserAgentPhase[];
|
|
7
|
+
/** The rest of what /system showed: its workspaces, the background service on the open project, notifications waiting
|
|
8
|
+
* to go out and external work. */
|
|
9
|
+
export declare function systemOf(store: Store, project: string | null, worktreeShown: (tree: WorktreeRow) => boolean, visible: (repo: string | null) => boolean): BrowserAgentsSystem;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agents page's installation parts (UI revamp wave B): the computers that build (what /fleet showed), which model
|
|
3
|
+
* runs each phase and the rest of what /system showed. Saved facts only, filtered through the caller's `visible`, for
|
|
4
|
+
* a person with access to the whole installation; nothing here grants an act (register, retire, move and note keep
|
|
5
|
+
* their own handlers and checks).
|
|
6
|
+
*/
|
|
7
|
+
import { INSTALLATION_SCOPE, resolvePhaseAgent } from "../agentconfig.js";
|
|
8
|
+
import { agentModelName } from "../model-names.js";
|
|
9
|
+
import { projectName } from "../project.js";
|
|
10
|
+
import { isAlive } from "../runner.js";
|
|
11
|
+
import { taskHref } from "./http.js";
|
|
12
|
+
/** Every registered computer that isn't retired, plus a retired one still holding reserved work, as Agents lists it. */
|
|
13
|
+
export function computersOf(store, now, visible, manage) {
|
|
14
|
+
const queued = store.fleetQueue(now);
|
|
15
|
+
const claims = store.liveClaims(null, now);
|
|
16
|
+
const owned = new Set(queued.map(one => one.assignedRunner).filter((one) => one !== null));
|
|
17
|
+
const runners = store.listRunners().filter(one => one.retiredAt === null || owned.has(one.name));
|
|
18
|
+
const list = runners.map(runner => {
|
|
19
|
+
const live = claims.filter(claim => claim.runner === runner.name);
|
|
20
|
+
const shown = live.filter(claim => visible(claim.repo ?? null));
|
|
21
|
+
const alive = isAlive(runner, now);
|
|
22
|
+
return {
|
|
23
|
+
name: runner.name, host: runner.host, capacity: runner.capacity, retired: runner.retiredAt !== null,
|
|
24
|
+
state: !alive ? "offline" : live.length > 0 ? "building" : "idle",
|
|
25
|
+
building: shown.map(claim => ({ taskId: claim.taskId, title: store.getTask(claim.taskId)?.title ?? claim.taskId, href: taskHref(claim.taskId),
|
|
26
|
+
project: claim.repo == null ? null : projectName(claim.repo) })),
|
|
27
|
+
hidden: live.length - shown.length,
|
|
28
|
+
heardAt: runner.heartbeatAt, registeredAt: runner.registeredAt,
|
|
29
|
+
projects: runner.repos.filter(repo => visible(repo)).map(repo => projectName(repo)),
|
|
30
|
+
note: runner.queueNote ?? null,
|
|
31
|
+
reserved: queued.filter(one => one.assignedRunner === runner.name && visible(one.repo))
|
|
32
|
+
.map(one => ({ id: one.id, title: one.title, href: taskHref(one.id), project: one.repo === null ? null : projectName(one.repo), taken: one.taken })),
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
return { list, manage, queueRevision: store.queueRevision(), shared: queued.filter(one => one.assignedRunner === null && visible(one.repo)).length };
|
|
36
|
+
}
|
|
37
|
+
const PHASES = ["plan", "build", "repair", "review"];
|
|
38
|
+
/** Which provider and model runs each phase for the open project (or the installation), and where that choice comes from. */
|
|
39
|
+
export function phasesOf(store, project) {
|
|
40
|
+
return PHASES.map(phase => {
|
|
41
|
+
const answer = resolvePhaseAgent(store, phase, project, {});
|
|
42
|
+
const row = (project === null ? null : store.phaseConfig(project, phase)) ?? store.phaseConfig(INSTALLATION_SCOPE, phase);
|
|
43
|
+
const by = row?.updatedBy == null ? "" : ` by ${row.updatedBy}`;
|
|
44
|
+
if (!answer.ok)
|
|
45
|
+
return { phase, model: null, problem: answer.problem, source: "", noDollars: false };
|
|
46
|
+
const source = answer.source === "project" ? `Chosen for this project${by}`
|
|
47
|
+
: answer.source === "installation" ? `Set for the whole installation${by}` : "The default: nothing chosen";
|
|
48
|
+
return { phase, model: agentModelName(answer.spec.provider, answer.spec.model ?? null), problem: null, source, noDollars: answer.spec.provider !== "claude" };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** The rest of what /system showed: its workspaces, the background service on the open project, notifications waiting
|
|
52
|
+
* to go out and external work. */
|
|
53
|
+
export function systemOf(store, project, worktreeShown, visible) {
|
|
54
|
+
const workspaces = store.listWorktrees().filter(worktreeShown).map(tree => {
|
|
55
|
+
const leased = tree.leasedAt !== null && tree.releasedAt === null;
|
|
56
|
+
return { name: tree.path.split("/").pop() ?? tree.path, branch: tree.branch, state: leased ? "building" : tree.verified ? "free" : "needs review" };
|
|
57
|
+
});
|
|
58
|
+
const episode = project === null ? null : store.latestWatchEpisode(project);
|
|
59
|
+
const service = episode === null ? null : { running: episode.endedAt === null, at: episode.endedAt ?? episode.startedAt, built: episode.built, broke: episode.broke };
|
|
60
|
+
const external = store.listGrants()
|
|
61
|
+
.filter(one => one.dispatch === true && one.remoteRepo != null && visible(one.repo))
|
|
62
|
+
.map(one => {
|
|
63
|
+
const open = store.listNotifications("all").find(n => n.kind === "sync-failed" && n.resolvedAt === null && n.dedupeKey.startsWith(`sync:${one.remoteRepo}:`))?.subject ?? null;
|
|
64
|
+
return { repo: String(one.remoteRepo), state: one.dispatchBlocked != null ? "Dispatch blocked: the tracker connection needs repair (toolroll sync says why)" : open !== null ? `Sync failing: ${open}` : "Syncing normally" };
|
|
65
|
+
});
|
|
66
|
+
return { workspaces, service, outbox: store.pendingForAttention().length, external };
|
|
67
|
+
}
|