toolroll 0.9.83 → 0.9.85
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 +69 -0
- package/README.md +94 -1192
- package/dist/agent-fence.d.ts +3 -3
- package/dist/agent-fence.js +3 -3
- package/dist/agent-onboard.d.ts +2 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +12 -0
- package/dist/chat-channel.d.ts +10 -1
- package/dist/chat-channel.js +35 -5
- package/dist/chat-decide.d.ts +16 -4
- package/dist/chat-decide.js +239 -31
- package/dist/chat-messages.d.ts +5 -0
- package/dist/chat-messages.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-review.d.ts +4 -0
- package/dist/chat-review.js +13 -4
- package/dist/chat-terms.d.ts +9 -0
- package/dist/chat-terms.js +46 -0
- package/dist/contracts/console-api.d.ts +4 -2
- package/dist/contracts/console-api.js +7 -2
- package/dist/contracts/lead-tools.d.ts +30 -0
- package/dist/contracts/lead-tools.js +3 -0
- package/dist/contracts/youiyoux-request.d.ts +248 -0
- package/dist/contracts/youiyoux-request.js +152 -0
- package/dist/control-setup.d.ts +9 -0
- package/dist/control-setup.js +31 -4
- package/dist/control-ui.d.ts +7 -1
- package/dist/control-ui.js +3 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.d.ts +15 -0
- package/dist/dispatch.js +26 -4
- package/dist/first-run.d.ts +5 -2
- package/dist/first-run.js +13 -2
- package/dist/flow-cadence.d.ts +8 -2
- package/dist/flow-cadence.js +16 -3
- package/dist/flow-card-task.d.ts +9 -0
- package/dist/flow-card-task.js +15 -6
- package/dist/folder-look.d.ts +3 -0
- package/dist/folder-look.js +8 -4
- package/dist/guides.js +4 -3
- package/dist/integrations-ui.js +9 -4
- package/dist/integrations.d.ts +9 -1
- package/dist/integrations.js +43 -2
- package/dist/landed-cleanup.d.ts +4 -0
- package/dist/landed-cleanup.js +3 -2
- package/dist/lead-capabilities.js +2 -1
- package/dist/lead-cli.js +3 -1
- package/dist/lead-tools.d.ts +2 -0
- package/dist/lead-tools.js +30 -4
- package/dist/lead.d.ts +2 -0
- package/dist/lead.js +3 -3
- package/dist/model-catalog.d.ts +5 -0
- package/dist/model-catalog.js +11 -2
- package/dist/model-choices.d.ts +188 -0
- package/dist/model-choices.js +405 -0
- package/dist/model-names.d.ts +6 -0
- package/dist/model-names.js +34 -0
- package/dist/models-ui.d.ts +69 -5
- package/dist/models-ui.js +53 -10
- package/dist/needs-you.d.ts +8 -0
- package/dist/needs-you.js +8 -0
- package/dist/operate.d.ts +22 -2
- package/dist/operate.js +107 -41
- package/dist/project-activity.d.ts +20 -0
- package/dist/project-activity.js +23 -0
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-cli.d.ts +1 -0
- package/dist/project-cli.js +36 -19
- package/dist/project-identity.d.ts +9 -2
- package/dist/project-identity.js +80 -33
- package/dist/project-knowledge.d.ts +9 -9
- package/dist/project-outcomes.d.ts +119 -0
- package/dist/project-outcomes.js +79 -0
- package/dist/result-actions.d.ts +4 -0
- package/dist/result-actions.js +41 -9
- package/dist/result-review.d.ts +9 -0
- package/dist/result-review.js +45 -5
- package/dist/serve.js +60 -13
- package/dist/server/chat.js +5 -4
- package/dist/server/chrome.d.ts +3 -0
- package/dist/server/chrome.js +5 -2
- package/dist/server/console.css +17 -2
- package/dist/server/flows.js +4 -0
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +6 -0
- package/dist/server/pages.js +53 -7
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-chat.js +3 -2
- package/dist/server/render-pages.d.ts +12 -2
- package/dist/server/render-pages.js +36 -16
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +20 -7
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.d.ts +1 -0
- package/dist/server/render-tasks.js +22 -19
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +9 -1
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +216 -70
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/task-views.js +6 -0
- package/dist/server/tasks.js +54 -8
- package/dist/settings-kit-script.d.ts +21 -0
- package/dist/settings-kit-script.js +67 -0
- package/dist/settings-kit.d.ts +137 -0
- package/dist/settings-kit.js +186 -0
- package/dist/settings-proofs.d.ts +64 -0
- package/dist/settings-proofs.js +101 -0
- package/dist/setup-guide.js +4 -2
- package/dist/spend-report.d.ts +87 -0
- package/dist/spend-report.js +265 -0
- package/dist/spend-ui.d.ts +29 -8
- package/dist/spend-ui.js +74 -39
- package/dist/store.d.ts +46 -3
- package/dist/store.js +165 -6
- package/dist/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/dist/task-source.d.ts +13 -1
- package/dist/task-source.js +17 -1
- package/dist/telegram-lead.js +7 -5
- package/dist/telegram.js +1 -1
- package/dist/toolroll-update.js +2 -2
- package/dist/webhooks.d.ts +6 -0
- package/dist/webhooks.js +25 -0
- package/dist/work-index.d.ts +4 -0
- package/dist/work-index.js +32 -0
- package/dist/youiyoux-intake.d.ts +234 -0
- package/dist/youiyoux-intake.js +908 -0
- package/package.json +11 -8
package/dist/server/pages.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectOutcomes } from '../project-outcomes.js';
|
|
1
2
|
import { steerControlsFor } from "../steer-controls.js";
|
|
2
3
|
import { canReveal, cdCommandOf, localCheckoutOf } from "../checkout-proof.js";
|
|
3
4
|
import { instanceHttpAnswer } from "../console-instance.js";
|
|
@@ -8,7 +9,8 @@ import { createHmac, randomBytes } from "node:crypto";
|
|
|
8
9
|
import { realpathSync } from "node:fs";
|
|
9
10
|
import {} from "node:http";
|
|
10
11
|
import { homedir, hostname } from "node:os";
|
|
11
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
13
|
+
import { buildersFor } from "../dispatch.js";
|
|
12
14
|
import { listFolder, localOrigins } from "../folder-look.js";
|
|
13
15
|
import { Readable } from "node:stream";
|
|
14
16
|
import { pipeline } from "node:stream/promises";
|
|
@@ -50,7 +52,7 @@ import { refuse, regionScript, screen, settingsRows, WORKSPACE_STYLE, workToolRo
|
|
|
50
52
|
import { redirect, respond, safeReturn, SAFETY, taskHref } from "./http.js";
|
|
51
53
|
import { assignmentOf } from "../assignment.js";
|
|
52
54
|
import { assignmentTaskStatusOf } from "../assignment-presentation.js";
|
|
53
|
-
import { browsePage, fleetBody, fleetScript, githubIdentityOf, githubReposPage, homePage, portfolioOverview, projectsPage, systemPage, workbenchRail } from "./render-pages.js";
|
|
55
|
+
import { browsePage, fleetBody, fleetScript, githubIdentityOf, githubReposPage, homePage, portfolioOverview, projectsPage, startBuilderHtml, systemPage, workbenchRail } from "./render-pages.js";
|
|
54
56
|
import { capsPage } from "./render-settings.js";
|
|
55
57
|
import { decisionAnswerScript, taskBody } from "./render-tasks.js";
|
|
56
58
|
import {} from "./session.js";
|
|
@@ -640,6 +642,7 @@ export function createPagesHandlers(runtime) {
|
|
|
640
642
|
}
|
|
641
643
|
const askedPath = (body.get("path") ?? "").trim();
|
|
642
644
|
// Resolved off the console's thread (project-identity.ts): a folder that doesn't answer is said, never waited on.
|
|
645
|
+
const decidedOn = identities.generation();
|
|
643
646
|
const picked = askedPath === "" ? null : await identities.resolveNow(askedPath);
|
|
644
647
|
if (picked === "no-answer")
|
|
645
648
|
return refuse(response, who, 503, "That project's folder didn't answer. It may be on a drive that's disconnected, or waiting on macOS. Try again in a moment.");
|
|
@@ -647,6 +650,9 @@ export function createPagesHandlers(runtime) {
|
|
|
647
650
|
if ((askedPath !== "" && canonicalPick === null) || (canonicalPick !== null && (!visible(canonicalPick) || !(await authorizedProject(liveCeiling(), canonicalPick))))) {
|
|
648
651
|
return refuse(response, who, 403, "that project is outside this console's reach");
|
|
649
652
|
}
|
|
653
|
+
// An identity that changed during the awaits above undoes what they proved.
|
|
654
|
+
if (identities.generation() !== decidedOn)
|
|
655
|
+
return refuse(response, who, 409, "a project folder changed while this was being checked — try again");
|
|
650
656
|
who.session.project = canonicalPick;
|
|
651
657
|
who.session.projectRevision += 1;
|
|
652
658
|
sessions.persist(who.session);
|
|
@@ -680,6 +686,7 @@ export function createPagesHandlers(runtime) {
|
|
|
680
686
|
return refuse(response, who, 403, "opening a project is a browser session's act");
|
|
681
687
|
}
|
|
682
688
|
const asked = (body.get("path") ?? "").trim();
|
|
689
|
+
const decidedOn = identities.generation();
|
|
683
690
|
const resolved = asked === "" ? null : await identities.resolveNow(asked);
|
|
684
691
|
if (resolved === "no-answer") {
|
|
685
692
|
return void projectsScreen(response, who, "that folder didn't answer — it may be on a drive that's disconnected, or waiting on macOS; try again in a moment", 503, safeReturn(body.get("return")));
|
|
@@ -702,13 +709,21 @@ export function createPagesHandlers(runtime) {
|
|
|
702
709
|
if (!visible(canonical)) {
|
|
703
710
|
return void projectsScreen(response, who, "That project is outside your access.", 403, safeReturn(body.get("return")));
|
|
704
711
|
}
|
|
712
|
+
// An identity that changed during the awaits above undoes what they proved.
|
|
713
|
+
if (identities.generation() !== decidedOn)
|
|
714
|
+
return void projectsScreen(response, who, "a project folder changed while this was being checked — try again", 409, safeReturn(body.get("return")));
|
|
705
715
|
// Opening an allowed repository admits it the same way `repos add` does
|
|
706
716
|
// (project-admission.ts). A co-located `up` notices the durable list and
|
|
707
717
|
// connects its builder; there is no separate restart or binding step.
|
|
708
|
-
|
|
718
|
+
// The admission asks again, inside its registry lock and its transaction, about the repo it proves.
|
|
719
|
+
const stillAllowed = (repo) => identities.generation() === decidedOn && visible(repo);
|
|
720
|
+
const admitted = await admitProject(store, { registryFile: options.registryPath ?? null, path: canonical, actor: { label: who.name, origin: "console" }, now, stillAllowed });
|
|
709
721
|
if (!admitted.ok) {
|
|
710
|
-
return void projectsScreen(response, who, admitted.reason === "not-git" ? "that path is not a git repository" : `that project is valid, but it could not be added — ${admitted.message}`, 400, safeReturn(body.get("return")));
|
|
722
|
+
return void projectsScreen(response, who, admitted.reason === "not-git" ? "that path is not a git repository" : `that project is valid, but it could not be added — ${admitted.message}`, admitted.reason === "moved" ? 409 : 400, safeReturn(body.get("return")));
|
|
711
723
|
}
|
|
724
|
+
// And once more before the session follows it.
|
|
725
|
+
if (!stillAllowed(admitted.repo))
|
|
726
|
+
return void projectsScreen(response, who, "a project folder changed while this was being checked — try again", 409, safeReturn(body.get("return")));
|
|
712
727
|
deletedRepos.delete(admitted.repo);
|
|
713
728
|
who.session.project = admitted.repo;
|
|
714
729
|
who.session.projectRevision++;
|
|
@@ -858,6 +873,8 @@ export function createPagesHandlers(runtime) {
|
|
|
858
873
|
return projectsScreen(response, who, "that preview expired or was already used — preview again", 400);
|
|
859
874
|
}
|
|
860
875
|
live.onboard?.delete(nonce); // consumed BEFORE the first await
|
|
876
|
+
// Identities as this confirmation found them: the admission below re-checks against them (project-identity.ts).
|
|
877
|
+
const decidedOn = identities.generation();
|
|
861
878
|
if (record.large && body.get("big-ok") !== "1") {
|
|
862
879
|
return projectsScreen(response, who, "that repository is large (or its size is unknown) — tick the box to clone it anyway", 400);
|
|
863
880
|
}
|
|
@@ -887,10 +904,13 @@ export function createPagesHandlers(runtime) {
|
|
|
887
904
|
if (admitted === null) {
|
|
888
905
|
return projectsScreen(response, who, `the clone landed at ${cloned.target} but did not prove under the ceiling — it was left in place; enroll it by hand`, 400);
|
|
889
906
|
}
|
|
890
|
-
const
|
|
907
|
+
const stillAllowed = (repo) => identities.generation() === decidedOn && visible(repo);
|
|
908
|
+
const joined = await admitProject(store, { registryFile: options.registryPath ?? null, path: admitted, actor: { label: who.name, origin: "console" }, now, stillAllowed });
|
|
891
909
|
if (!joined.ok) {
|
|
892
|
-
return projectsScreen(response, who, `${cloned.target} is cloned but not added — ${joined.message}`, 400);
|
|
910
|
+
return projectsScreen(response, who, `${cloned.target} is cloned but not added — ${joined.message}`, joined.reason === "moved" ? 409 : 400);
|
|
893
911
|
}
|
|
912
|
+
if (!stillAllowed(joined.repo))
|
|
913
|
+
return projectsScreen(response, who, `${cloned.target} is cloned but wasn't opened — a project folder changed while it was being checked; try again`, 409);
|
|
894
914
|
deletedRepos.delete(admitted);
|
|
895
915
|
who.session.project = admitted;
|
|
896
916
|
who.session.projectRevision += 1;
|
|
@@ -948,7 +968,16 @@ export function createPagesHandlers(runtime) {
|
|
|
948
968
|
peeks[one.path] = peekOf(one.path);
|
|
949
969
|
for (const path of candidates)
|
|
950
970
|
peeks[path] = peekOf(path);
|
|
951
|
-
|
|
971
|
+
let activity;
|
|
972
|
+
if (open !== null && !returnTo.startsWith("/tasks/new") && visible(open) && store.accountCanAccess(who.name, open)) {
|
|
973
|
+
try {
|
|
974
|
+
activity = { project: projectName(open), report: projectOutcomes(store, open, now, { principal: 'operator', repos: [open] }) };
|
|
975
|
+
}
|
|
976
|
+
catch {
|
|
977
|
+
activity = { project: projectName(open), report: null };
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return sendScreen(response, status, projectsPage(chromeFor(open, "projects"), recent, [...candidates], open, problem, !restricted() && unscopedMode, !restricted() && (ceiling.roots.length > 0 || unscopedMode), onboardState, peeks, returnTo, who.via === "cookie" ? (path) => publishingOf(store, path).on : undefined, who.via === "cookie" ? (path) => projectCheckLevel(store, path).level : undefined, activity));
|
|
952
981
|
}
|
|
953
982
|
async function instanceEdge(ctx) {
|
|
954
983
|
const { request, response, url } = ctx;
|
|
@@ -1058,6 +1087,22 @@ export function createPagesHandlers(runtime) {
|
|
|
1058
1087
|
return respond(response, 404, "application/json", JSON.stringify({ error: "run" }));
|
|
1059
1088
|
return respond(response, 200, "application/json", JSON.stringify(window.ok ? window : { error: window.reason }));
|
|
1060
1089
|
}
|
|
1090
|
+
/** Start a builder (Settings revamp S4): the command, and whether a builder that may build the open project is
|
|
1091
|
+
* checking in, as dispatch decides. Read-only; a project-limited person sees only a project they may see. */
|
|
1092
|
+
async function startBuilderPage(ctx) {
|
|
1093
|
+
const { response, now, project, url, who } = ctx;
|
|
1094
|
+
// ?task=<id>: that task's own saved project (a builder there, as dispatch decides for it), never the open one; the
|
|
1095
|
+
// session's selection is left alone. Unknown, unplaced or not visible to this person: 404, the same answer.
|
|
1096
|
+
const taskId = url.searchParams.get("task");
|
|
1097
|
+
const ref = taskId === null ? null : store.lookupRef(taskId.slice(0, 200));
|
|
1098
|
+
if (taskId !== null && (ref === null || ref.repo === null || !visible(ref.repo)))
|
|
1099
|
+
return refuse(response, who, 404, "That task is not available.", "/work");
|
|
1100
|
+
const shown = ref !== null ? ref.repo : project !== null && visible(project) ? project : null;
|
|
1101
|
+
const connected = shown === null ? null : buildersFor(store, shown, now, ref?.assignedRunner ?? null).alive.length > 0;
|
|
1102
|
+
return sendScreen(response, 200, screen("Start a builder", startBuilderHtml({
|
|
1103
|
+
heading: "h1", project: shown === null ? null : basename(shown), connected, managed: !restricted(),
|
|
1104
|
+
}), { chrome: chromeFor(project, "none") }));
|
|
1105
|
+
}
|
|
1061
1106
|
const registrations = handlersOf("pages", {
|
|
1062
1107
|
"projects.browse": projectsBrowse,
|
|
1063
1108
|
"projects.github": projectsGithub,
|
|
@@ -1069,6 +1114,7 @@ export function createPagesHandlers(runtime) {
|
|
|
1069
1114
|
"ledger": ledgerPage,
|
|
1070
1115
|
"activity": activityPage,
|
|
1071
1116
|
"system": systemScreen,
|
|
1117
|
+
"start-builder": startBuilderPage,
|
|
1072
1118
|
"peek": peekPage,
|
|
1073
1119
|
"agents": agentsPage,
|
|
1074
1120
|
"agents.live": agentsLive,
|
|
@@ -12,7 +12,7 @@ import { SSO_CALLBACK } from "../sso-settings.js";
|
|
|
12
12
|
import { personChip, refuse, screen } from "./chrome.js";
|
|
13
13
|
import { redirect, respond, sayTo } from "./http.js";
|
|
14
14
|
/** people handlers, moved without changing their route bodies. */
|
|
15
|
-
import { randomBytes
|
|
15
|
+
import { randomBytes } from "node:crypto";
|
|
16
16
|
import {} from "node:http";
|
|
17
17
|
import {} from "../contracts/console-api.js";
|
|
18
18
|
import { listCoordinators } from "../coordinator.js";
|
|
@@ -22,8 +22,9 @@ import { personAuditHtml, personHref } from "../people-audit-ui.js";
|
|
|
22
22
|
import { cursorOf as auditCursorOf, remoteAudit } from "../remote-audit.js";
|
|
23
23
|
import { credentialProof, hashPassword, proveAccountNow } from "../scope.js";
|
|
24
24
|
import { form, HANDOFF_STYLE, loginHref, loginReturn, page, SIGN_IN_COOKIE, signInSpent, startedHere } from "./http.js";
|
|
25
|
-
import { joinDeadPage, joinFormPage, loginPage, signupPage } from "./render-people.js";
|
|
25
|
+
import { joinDeadPage, joinFormPage, loginPage, SIGNUP_NAME, SIGNUP_PASSWORD_MIN, signupPage } from "./render-people.js";
|
|
26
26
|
import { SESSION_COOKIE, SIGN_IN_LINK_PATH } from "./session.js";
|
|
27
|
+
import { setupCodeMatches, signupCookie, signupFromHere } from "./signup-gate.js";
|
|
27
28
|
/** Clock skew allowed between the provider and this server when reading auth_time — forward only (a provider clock
|
|
28
29
|
* running ahead). auth_time is in whole seconds, so a check made in the visit's own second is floored below startedAt:
|
|
29
30
|
* the one allowance backward is that rounding, never a check made before the visit began. */
|
|
@@ -39,7 +40,7 @@ export function sameVisitCheck(authTime, startedAt, now) {
|
|
|
39
40
|
return Math.min(Math.max(at, startedAt), now);
|
|
40
41
|
}
|
|
41
42
|
export function createPeopleHandlers(runtime) {
|
|
42
|
-
const { joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival,
|
|
43
|
+
const { joinBySource, ssoOffer, ssoVisits, ssoSettings, options, clock, ssoHandoffs, managedRepos, store, visible, admissionList, sendScreen, chromeFor, restricted, sessions, consoleProjects, authenticateApprover, authenticateCredential, bustBadge, projectOf, signInLinks, linkKey, fromThisComputer, recordSignIn, passwordAllowed, arrival, cookieSecure, consoleOrigin, providerFor, providerKnown, joinSourceOf, signInBudget, signInActor, minutesWords } = runtime;
|
|
43
44
|
// v101: Settings → Sessions & tokens: where you're signed in, and your API tokens. An instance operator can see everyone's.
|
|
44
45
|
async function sessionsPage(ctx) {
|
|
45
46
|
const { url, who, response, project } = ctx;
|
|
@@ -307,6 +308,11 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
307
308
|
async function signInPage(ctx) {
|
|
308
309
|
const { url, response } = ctx;
|
|
309
310
|
if (options.setupCode !== undefined && store.listApprovers().length === 0) {
|
|
311
|
+
// The form's same-site proof (signup-gate.ts): a post without it never reaches the setup code.
|
|
312
|
+
if (runtime.setupAttemptsLeft > 0)
|
|
313
|
+
response.setHeader("Set-Cookie", signupCookie(cookieSecure));
|
|
314
|
+
// Its own post then carries this console's Origin (the console's default no-referrer sends `null`).
|
|
315
|
+
response.setHeader("Referrer-Policy", "same-origin");
|
|
310
316
|
return page(response, 200, signupPage(null, runtime.setupAttemptsLeft));
|
|
311
317
|
}
|
|
312
318
|
return page(response, 200, loginPage(null, loginReturn(url.searchParams.get("return")), ssoOffer()));
|
|
@@ -334,7 +340,7 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
334
340
|
return page(response, 403, loginPage(`Sign in with ${ssoSettings()?.label ?? "your identity provider"}.`, "/chat", ssoOffer()));
|
|
335
341
|
const id = randomBytes(32).toString("hex");
|
|
336
342
|
sessions.set(id, { ...arrival(request), name: held.account, csrf: randomBytes(32).toString("hex"), role: account.role, generation: account.generation, createdAt: Date.now(), sawBoardAt: null, lastSeen: Date.now(),
|
|
337
|
-
project: defaultProject, projectRevision: 1 });
|
|
343
|
+
project: runtime.defaultProject, projectRevision: 1 });
|
|
338
344
|
recordSignIn(held.account, "signed in", "one-time link");
|
|
339
345
|
response.setHeader("Set-Cookie", `${SESSION_COOKIE}=${id}; HttpOnly; SameSite=Strict; Path=/${cookieSecure}`);
|
|
340
346
|
return redirect(response, "/chat");
|
|
@@ -429,44 +435,58 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
429
435
|
const account = store.accountOf(signedIn.account);
|
|
430
436
|
const id = randomBytes(32).toString("hex");
|
|
431
437
|
sessions.set(id, { ...arrival(request), name: signedIn.account, csrf: randomBytes(32).toString("hex"), role: account.role, generation: account.generation, createdAt: Date.now(), sawBoardAt: null, lastSeen: Date.now(),
|
|
432
|
-
project: defaultProject, projectRevision: 1, sso: { at: handoff.checkedAt ?? 0 } });
|
|
438
|
+
project: runtime.defaultProject, projectRevision: 1, sso: { at: handoff.checkedAt ?? 0 } });
|
|
433
439
|
recordSignIn(signedIn.account, "signed in", settings.label);
|
|
434
440
|
response.setHeader("Set-Cookie", [signInSpent("/login/sso/finish"), `${SESSION_COOKIE}=${id}; HttpOnly; SameSite=Strict; Path=/${cookieSecure}`]);
|
|
435
441
|
return redirect(response, handoff.returnTo);
|
|
436
442
|
}
|
|
437
443
|
async function signupSend(ctx) {
|
|
438
444
|
const { request, response } = ctx;
|
|
445
|
+
// A re-shown form posts again: its Origin must name this console too (signup-gate.ts).
|
|
446
|
+
response.setHeader("Referrer-Policy", "same-origin");
|
|
439
447
|
// Only while the table is empty, only with the printed code, only
|
|
440
448
|
// five tries: the bootstrap door underneath is atomic, so two first
|
|
441
449
|
// visitors serialize to one owner.
|
|
442
450
|
if (options.setupCode === undefined || store.listApprovers().length > 0) {
|
|
443
|
-
return page(response, 409, loginPage("
|
|
451
|
+
return page(response, 409, loginPage("An account already exists. Sign in."));
|
|
444
452
|
}
|
|
445
453
|
if (runtime.setupAttemptsLeft <= 0) {
|
|
446
|
-
return page(response, 403, signupPage(
|
|
454
|
+
return page(response, 403, signupPage(null, 0));
|
|
455
|
+
}
|
|
456
|
+
// Another site's post (or one without this page's cookie) is refused before it can spend a try.
|
|
457
|
+
if (!signupFromHere(request, runtime.allowedHost)) {
|
|
458
|
+
request.resume();
|
|
459
|
+
response.setHeader("Set-Cookie", signupCookie(cookieSecure));
|
|
460
|
+
return page(response, 403, signupPage("This form had expired, so it was reloaded. Enter the details again.", runtime.setupAttemptsLeft));
|
|
447
461
|
}
|
|
448
462
|
const body = readForm(await form(request), CONSOLE_FORMS.signup);
|
|
449
463
|
const code = (body.get("code") ?? "").trim();
|
|
450
464
|
const name = (body.get("name") ?? "").trim();
|
|
451
465
|
const password = body.get("password") ?? "";
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if (
|
|
466
|
+
// Re-read after the body: concurrent posts all waited above, and only the tries still left may be compared.
|
|
467
|
+
// From here to the decrement nothing awaits, so check and spend are one step.
|
|
468
|
+
if (runtime.setupAttemptsLeft <= 0) {
|
|
469
|
+
return page(response, 403, signupPage(null, 0));
|
|
470
|
+
}
|
|
471
|
+
if (!setupCodeMatches(code, options.setupCode)) {
|
|
455
472
|
runtime.setupAttemptsLeft -= 1;
|
|
456
|
-
|
|
473
|
+
// What was typed comes back except the code and the password (never echoed); the count rides the code's note.
|
|
474
|
+
return page(response, 403, signupPage(runtime.setupAttemptsLeft > 0
|
|
475
|
+
? { field: "code", text: "That isn't the setup code Toolroll printed. Check the terminal and try again." }
|
|
476
|
+
: "That wasn't the setup code, and that was the last try. Restart Toolroll to get a new code.", runtime.setupAttemptsLeft, name));
|
|
457
477
|
}
|
|
458
|
-
if (
|
|
459
|
-
return page(response, 400, signupPage("
|
|
478
|
+
if (!SIGNUP_NAME.test(name)) {
|
|
479
|
+
return page(response, 400, signupPage({ field: "name", text: "Use only letters, digits, dots, dashes or underscores, starting with a letter or digit." }, runtime.setupAttemptsLeft, name));
|
|
460
480
|
}
|
|
461
|
-
if (password.length <
|
|
462
|
-
return page(response, 400, signupPage("
|
|
481
|
+
if (password.length < SIGNUP_PASSWORD_MIN) {
|
|
482
|
+
return page(response, 400, signupPage({ field: "password", text: `Use at least ${SIGNUP_PASSWORD_MIN} characters.` }, runtime.setupAttemptsLeft, name));
|
|
463
483
|
}
|
|
464
484
|
const made = store.bootstrapApproverIfNone(name, hashPassword(password), clock());
|
|
465
485
|
if (!made.ok)
|
|
466
|
-
return page(response, 409, loginPage("
|
|
486
|
+
return page(response, 409, loginPage("An account already exists. Sign in."));
|
|
467
487
|
const authenticated = authenticateAccount(store, name, password);
|
|
468
488
|
if (authenticated === null || !authenticated.ok)
|
|
469
|
-
return page(response, 500, loginPage("
|
|
489
|
+
return page(response, 500, loginPage("The account was created, but signing in failed. Sign in with it now."));
|
|
470
490
|
const id = randomBytes(32).toString("hex");
|
|
471
491
|
sessions.set(id, {
|
|
472
492
|
...arrival(request),
|
|
@@ -477,7 +497,7 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
477
497
|
createdAt: Date.now(),
|
|
478
498
|
sawBoardAt: null,
|
|
479
499
|
lastSeen: Date.now(),
|
|
480
|
-
project: defaultProject,
|
|
500
|
+
project: runtime.defaultProject,
|
|
481
501
|
projectRevision: 1,
|
|
482
502
|
// A password typed on this computer starts this session's freshness window (cockpit B2); from anywhere else, never.
|
|
483
503
|
...(fromThisComputer(request) ? { passwordAt: Date.now() } : {}),
|
|
@@ -528,7 +548,7 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
528
548
|
createdAt: Date.now(),
|
|
529
549
|
sawBoardAt: null,
|
|
530
550
|
lastSeen: Date.now(),
|
|
531
|
-
project: defaultProject,
|
|
551
|
+
project: runtime.defaultProject,
|
|
532
552
|
projectRevision: 1,
|
|
533
553
|
// A password typed to sign in on this computer starts this session's freshness window (cockpit B2); from anywhere
|
|
534
554
|
// else (a phone, a proxy) it signs in and proves nothing more.
|
|
@@ -615,7 +635,7 @@ ${reauth(html `<label>Password <input type="password" name="token" autocomplete=
|
|
|
615
635
|
createdAt: Date.now(),
|
|
616
636
|
sawBoardAt: null,
|
|
617
637
|
lastSeen: Date.now(),
|
|
618
|
-
project: defaultProject,
|
|
638
|
+
project: runtime.defaultProject,
|
|
619
639
|
projectRevision: 1,
|
|
620
640
|
});
|
|
621
641
|
response.setHeader("Set-Cookie", `${SESSION_COOKIE}=${id}; HttpOnly; SameSite=Strict; Path=/${cookieSecure}`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseFeedbackImage } from "../result-review.js";
|
|
1
2
|
import { assignmentSummaryHtml } from "../assignment-ui.js";
|
|
2
3
|
import {} from "../assignment.js";
|
|
3
4
|
import {} from "../browser-workspace.js";
|
|
@@ -610,8 +611,8 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
|
610
611
|
const snapshot = payload["snapshot"];
|
|
611
612
|
const ids = Array.isArray(payload["notes"]) ? payload["notes"] : [];
|
|
612
613
|
const selected = snapshot?.notes.filter(one => ids.includes(one.id)) ?? [];
|
|
613
|
-
const notes = [...selected, ...(text("note") === "" ? [] : [{ note: text("note"), path: text("path") || null, line: typeof payload["line"] === "number" ? payload["line"] : null }])];
|
|
614
|
-
what = html `<h3>${text("taskTitle") || task}</h3><ul class="proposal-review-notes">${notes.map(one => html `<li>${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : html `:${one.line}`}</span> · `}${one.note}</li>`)}</ul>${text("operation") === "revise" ? html `<p class="meta">Updates this task. Your approval settings apply.</p>` : html `<p class="meta">No work starts.</p>`}`;
|
|
614
|
+
const notes = [...selected, ...(text("note") === "" ? [] : [{ note: text("note"), path: text("path") || null, line: typeof payload["line"] === "number" ? payload["line"] : null, image: parseFeedbackImage(payload["image"]) || null }])];
|
|
615
|
+
what = html `<h3>${text("taskTitle") || task}</h3><ul class="proposal-review-notes">${notes.map(one => html `<li>${one.image == null ? "" : html `<a href="/r/${one.image.run}/evidence/${one.image.artifact}">Screenshot ${one.image.artifact} · result #${one.image.run}</a><br>`}${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : html `:${one.line}`}</span> · `}${one.note}</li>`)}</ul>${text("operation") === "revise" ? html `<p class="meta">Updates this task. Your approval settings apply.</p>` : html `<p class="meta">No work starts.</p>`}`;
|
|
615
616
|
}
|
|
616
617
|
else if (view.kind === "next") {
|
|
617
618
|
what = html `<h3>Move <a href="${taskHref(task)}">${task}</a> to the front</h3>${facts(["current position", html `${String(payload["position"] ?? "?")} of ${String(payload["of"] ?? "?")}`], ["project", mono(repoId)])}`;
|
|
@@ -3,7 +3,7 @@ import { type TaskOrigin } from "../task-source.js";
|
|
|
3
3
|
import { type Html } from "../html.js";
|
|
4
4
|
import { type AssignmentSnapshot } from "../assignment.js";
|
|
5
5
|
import { type BoardCard } from "../board.js";
|
|
6
|
-
import { type BrowserLimits } from "../browser-workspace.js";
|
|
6
|
+
import { type BrowserLimits, type BrowserProjectsView } from "../browser-workspace.js";
|
|
7
7
|
import { type CheckLevel } from "../check-levels.js";
|
|
8
8
|
import { type DispatchDiagnosis } from "../dispatch.js";
|
|
9
9
|
import { type FirstRunStep } from "../first-run.js";
|
|
@@ -112,6 +112,16 @@ export declare function inboxPage(chrome: Chrome, data: {
|
|
|
112
112
|
unread?: readonly InboxTab[];
|
|
113
113
|
}): Screen;
|
|
114
114
|
/** System: the machinery — workers, background service, workspaces. */
|
|
115
|
+
/** Start a builder: the one command, as text to select (/start-builder, where every "Connect a builder" leads, and
|
|
116
|
+
* /system#builder). `project`: the open project's name, when the viewer may see it; `connected`: whether a builder
|
|
117
|
+
* that may build there is checking in (null: say nothing); `managed`: whether this person can start one themselves
|
|
118
|
+
* (registering a builder takes an instance operator), else who to ask. */
|
|
119
|
+
export declare function startBuilderHtml(data: {
|
|
120
|
+
heading: "h1" | "h2";
|
|
121
|
+
project: string | null;
|
|
122
|
+
connected: boolean | null;
|
|
123
|
+
managed: boolean;
|
|
124
|
+
}): Html;
|
|
115
125
|
export declare function systemPage(chrome: Chrome, data: {
|
|
116
126
|
agents: {
|
|
117
127
|
phase: "plan" | "build" | "repair" | "review";
|
|
@@ -441,7 +451,7 @@ export declare function projectsPage(chrome: Chrome, recent: {
|
|
|
441
451
|
path: string;
|
|
442
452
|
name: string;
|
|
443
453
|
lastOpenedAt: string;
|
|
444
|
-
}[], candidates: string[], open: string | null, problem: string | null, _unscopedMode: boolean, browsable?: boolean, onboard?: OnboardCardState | null, peeks?: Record<string, ProjectPeek | null>, returnTo?: string, pullRequestsOn?: (path: string) => boolean, checkLevelOf?: (path: string) => CheckLevel): Screen;
|
|
454
|
+
}[], candidates: string[], open: string | null, problem: string | null, _unscopedMode: boolean, browsable?: boolean, onboard?: OnboardCardState | null, peeks?: Record<string, ProjectPeek | null>, returnTo?: string, pullRequestsOn?: (path: string) => boolean, checkLevelOf?: (path: string) => CheckLevel, activity?: BrowserProjectsView["activity"]): Screen;
|
|
445
455
|
/** The no-script "move to the front" sentinel: the form cannot name the
|
|
446
456
|
* front of a partition, so the handler resolves it (slice 1b, fix 1). */
|
|
447
457
|
export declare const QUEUE_FRONT = "__TOP__";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { activityMetrics, activityScope, activityLimit } from '../project-activity.js';
|
|
1
2
|
/** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
|
|
2
3
|
import { originNote } from "../task-source.js";
|
|
3
4
|
import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
|
|
@@ -9,13 +10,14 @@ import {} from "../dispatch.js";
|
|
|
9
10
|
import {} from "../first-run.js";
|
|
10
11
|
import {} from "../gaps.js";
|
|
11
12
|
import { limitsHtml } from "../limits-ui.js";
|
|
12
|
-
import { ASK_LABEL, ASKS } from "../needs-you.js";
|
|
13
|
+
import { ASK_LABEL, ASKS, START_BUILDER_COMMAND } from "../needs-you.js";
|
|
13
14
|
import {} from "../onboard.js";
|
|
14
15
|
import {} from "../peek.js";
|
|
15
16
|
import { projectName } from "../project.js";
|
|
16
17
|
import {} from "../proof.js";
|
|
17
18
|
import {} from "../quality.js";
|
|
18
19
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
20
|
+
import { agentModelName, modelName } from "../model-names.js";
|
|
19
21
|
import {} from "../scope.js";
|
|
20
22
|
import {} from "../store.js";
|
|
21
23
|
import { runCostWords, spendLine, tally } from "../summary.js";
|
|
@@ -80,9 +82,10 @@ export function inboxPage(chrome, data) {
|
|
|
80
82
|
: html `<h2>Supply a requirement</h2><p class="hint">approved work is ready except for these — fill one and its tasks start</p>${joinHtml(data.gaps.map(gap => html `<p class="row"><a href="/caps">${gap.key}</a> <span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span><span class="right meta">how to fix →</span></p>`), "\n")}`;
|
|
81
83
|
// The first-run steps, until the first Ready result: each is done or
|
|
82
84
|
// shows the one action that does it.
|
|
83
|
-
const
|
|
85
|
+
const steps = data.wizard;
|
|
86
|
+
const wizard = steps === null
|
|
84
87
|
? null
|
|
85
|
-
: html `<div class="card" data-first-run><p><strong>Get to your first result</strong></p>${joinHtml(
|
|
88
|
+
: html `<div class="card" data-first-run><p><strong>Get to your first result</strong></p>${joinHtml(steps.map(step => html `<p class="row" data-step="${step.key}"><span class="mono" aria-hidden="true">${step.done ? "✓" : "○"}</span> <strong>${step.title}</strong>${step.checking ? html ` <span class="meta">checking…</span>` : step.action === null ? html ` <span class="meta">done</span>`
|
|
86
89
|
: step.action.kind === "link" ? html ` <a href="${step.action.href}">${step.action.label}</a>`
|
|
87
90
|
: html ` <code>${step.action.command}</code>`}</p>`), "\n")}</div>`;
|
|
88
91
|
const noWorker = data.worker.answering > 0
|
|
@@ -146,6 +149,20 @@ export function inboxPage(chrome, data) {
|
|
|
146
149
|
});
|
|
147
150
|
}
|
|
148
151
|
/** System: the machinery — workers, background service, workspaces. */
|
|
152
|
+
/** Start a builder: the one command, as text to select (/start-builder, where every "Connect a builder" leads, and
|
|
153
|
+
* /system#builder). `project`: the open project's name, when the viewer may see it; `connected`: whether a builder
|
|
154
|
+
* that may build there is checking in (null: say nothing); `managed`: whether this person can start one themselves
|
|
155
|
+
* (registering a builder takes an instance operator), else who to ask. */
|
|
156
|
+
export function startBuilderHtml(data) {
|
|
157
|
+
const where = data.project === null ? null : html ` to <strong>${data.project}</strong>`;
|
|
158
|
+
const state = data.connected === null ? null
|
|
159
|
+
: data.connected ? html `<p>A builder is connected${where}. Approved tasks there start on their own.</p>`
|
|
160
|
+
: html `<p><strong>No builder is connected</strong>${where}. Tasks wait until one is.</p>`;
|
|
161
|
+
const title = data.heading === "h1" ? html `<h1 id="builder-heading">Start a builder</h1>` : html `<h2 id="builder-heading">Start a builder</h2>`;
|
|
162
|
+
return html `<section id="builder" aria-labelledby="builder-heading">${title}<div class="card">${state}<p>On the computer where ${data.project ?? "the project"} lives, open the project's folder in a terminal and run:</p><p><code class="start-builder-command" style="user-select:all">${START_BUILDER_COMMAND}</code></p>${data.managed
|
|
163
|
+
? html `<p class="meta">Keep it running. Approved tasks start on their own. The console alone (<code>toolroll serve</code>) doesn't build.</p>`
|
|
164
|
+
: html `<p class="meta">Starting a builder takes someone who manages this Toolroll. Ask them to run it there; your tasks start on their own once it connects.</p>`}</div></section>`;
|
|
165
|
+
}
|
|
149
166
|
export function systemPage(chrome, data) {
|
|
150
167
|
const nowMs = data.now.getTime();
|
|
151
168
|
const runnerCards = data.runners
|
|
@@ -186,16 +203,15 @@ export function systemPage(chrome, data) {
|
|
|
186
203
|
? html `set for the whole installation${one.setBy === null ? null : html ` by ${one.setBy}`}`
|
|
187
204
|
: html `the default — nothing configured`;
|
|
188
205
|
const dollars = one.provider === "claude" ? null : html ` · <span title="this provider reports tokens, not dollars — its runs land as unmeasured spend">no dollar costs</span>`;
|
|
189
|
-
return html `<p class="row"><span class="mono">${one.phase}</span> <strong>${one.provider
|
|
206
|
+
return html `<p class="row"><span class="mono">${one.phase}</span> <strong>${one.provider === null || one.provider === undefined ? "" : agentModelName(one.provider, one.model ?? null)}</strong>${one.model === null || one.model === undefined ? html ` <span class="meta">(its default model)</span>` : null}<span class="right meta">${who}${dollars}</span></p><p class="meta" style="margin-top:0">${PHASE_SAID[one.phase] ?? ""}</p>`;
|
|
190
207
|
}), "\n");
|
|
191
|
-
const agentsCard = html `<h2>Agents</h2><p class="meta">Which AI provider runs each phase
|
|
208
|
+
const agentsCard = html `<h2>Agents</h2><p class="meta">Which AI provider runs each phase. An instance operator changes them in <a href="/settings/models">Settings → Models</a> with their password, or with <code>toolroll config</code>.</p><div class="card">${agentLines}<p class="meta">Repair always stays on the provider that built — only its model can differ. A schedule's task filed under an approval moved from a routine is pinned to the agents approved then.</p></div>`;
|
|
192
209
|
return screen("system", joinHtml([
|
|
193
210
|
html `<h1>System</h1>`,
|
|
194
211
|
html `<p class="hint">builders execute tasks in isolated temporary copies of each project</p>`,
|
|
195
212
|
agentsCard,
|
|
196
|
-
cards.length === 0
|
|
197
|
-
|
|
198
|
-
: html `<div class="cards">${cards}</div>`,
|
|
213
|
+
cards.length === 0 ? null : html `<div class="cards">${cards}</div>`,
|
|
214
|
+
startBuilderHtml({ heading: "h2", project: null, connected: data.runners.some(one => one.retiredAt === null && runnerAlive(one, data.now)) ? null : false, managed: true }),
|
|
199
215
|
data.outboxPending > 0 ? html `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : null,
|
|
200
216
|
(data.externalWork ?? []).length === 0
|
|
201
217
|
? null
|
|
@@ -372,11 +388,11 @@ export function boardBody(data, ciRed) {
|
|
|
372
388
|
? null
|
|
373
389
|
: html `<span class="steps" role="img" aria-label="${`step ${step + 1} of ${BUILD_STEPS.length}: ${BUILD_STEPS[step]}`}">${BUILD_STEPS.map((_, index) => html `<i class="${index < step ? "on" : index === step ? "now" : ""}"></i>`)}</span>`;
|
|
374
390
|
// Who, which model and which branch, as one muted line: words in Geist, the model and branch in mono.
|
|
375
|
-
const model = claim.model === null ? null :
|
|
391
|
+
const model = claim.model === null ? null : claim.provider === null ? modelName(null, claim.model) : agentModelName(claim.provider, claim.model);
|
|
376
392
|
const branch = claim.branch === null ? null : `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`;
|
|
377
393
|
const who = joinHtml([
|
|
378
394
|
html `<span>${claim.runner}</span>`,
|
|
379
|
-
model === null ? null : html `<span
|
|
395
|
+
model === null ? null : html `<span>${model}</span>`,
|
|
380
396
|
branch === null ? null : html `<span class="mono">${branch}</span>`,
|
|
381
397
|
].filter((one) => one !== null), html `<span aria-hidden="true"> · </span>`);
|
|
382
398
|
return html `<a class="lane-card building" href="${card.href}" data-vt="${card.taskId}"><span class="t"><span class="dot dot-ok pulse"></span>${card.title}</span>${steps}<span class="live-line"><span class="stage">${live}</span>${tickingClock(claim.claimedAt, data.now)}</span><span class="id">${card.taskId}</span><span class="who">${who}</span>${chips([
|
|
@@ -477,7 +493,7 @@ export function homePage(chrome, data) {
|
|
|
477
493
|
// an operator actually watches this page, so it re-renders itself.
|
|
478
494
|
const building = data.building.length === 0
|
|
479
495
|
? null
|
|
480
|
-
: html `<h2>Building now</h2><p class="hint">live builds — this page refreshes itself every 10 seconds while anything runs</p><div class="cards">${data.building.map(claim => html `<a class="stat-card" href="${taskHref(claim.taskId)}" style="text-decoration:none"><span class="k"><span class="dot dot-ok pulse"></span>${claim.taskId}</span><span class="v">${claim.runner} · ${Math.max(1, Math.round((data.now.getTime() - new Date(claim.claimedAt).getTime()) / 60_000))}m elapsed${claim.model === null ? null : html ` · ${claim.model}`}</span></a>`)}</div>`;
|
|
496
|
+
: html `<h2>Building now</h2><p class="hint">live builds — this page refreshes itself every 10 seconds while anything runs</p><div class="cards">${data.building.map(claim => html `<a class="stat-card" href="${taskHref(claim.taskId)}" style="text-decoration:none"><span class="k"><span class="dot dot-ok pulse"></span>${claim.taskId}</span><span class="v">${claim.runner} · ${Math.max(1, Math.round((data.now.getTime() - new Date(claim.claimedAt).getTime()) / 60_000))}m elapsed${claim.model === null ? null : html ` · ${modelName(null, claim.model)}`}</span></a>`)}</div>`;
|
|
481
497
|
// THE FLEET: runners by heartbeat age, worktrees by lease state, the watch.
|
|
482
498
|
const nowMs = data.now.getTime();
|
|
483
499
|
const runnerCards = data.runners
|
|
@@ -797,7 +813,7 @@ export function portfolioOverview(data) {
|
|
|
797
813
|
const outcomeWords = groups.filter(([, count]) => count > 0).map(([word, count]) => `${count} ${word}`).join(" · ");
|
|
798
814
|
const summary = tally(data.runs24);
|
|
799
815
|
// ---- running: current live claims, project chips on ----
|
|
800
|
-
const liveRows = joinHtml(data.live.map(one => html `<p class="row"><a href="${taskHref(one.taskId)}">${one.taskId}</a>${projectChip(one.repo)} <span class="badge badge-running">Running</span> <span class="
|
|
816
|
+
const liveRows = joinHtml(data.live.map(one => html `<p class="row"><a href="${taskHref(one.taskId)}">${one.taskId}</a>${projectChip(one.repo)} <span class="badge badge-running">Running</span> <span class="meta"><span class="mono">${one.runner}</span>${one.model === null ? null : html ` · ${modelName(null, one.model)}`} · <time data-elapsed-since="${one.claimedAt}"></time></span></p>`), "\n");
|
|
801
817
|
// ---- the ledger: terminal runs started in the window, one chip each ----
|
|
802
818
|
const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
|
|
803
819
|
const prLink = (one) => {
|
|
@@ -808,7 +824,7 @@ export function portfolioOverview(data) {
|
|
|
808
824
|
const safe = safePrUrl(one.prUrl);
|
|
809
825
|
return safe === null ? html ` · PR #${one.prNumber}` : html ` · <a href="${safe}">PR #${one.prNumber}</a>`;
|
|
810
826
|
};
|
|
811
|
-
const ledgerRows = joinHtml(data.ledger.map(one => html `<p class="row"><a href="/r/${one.runId}">${one.title}</a>${projectChip(one.repo)} <span class="badge ${chipClass(one.outcome)}">${sentenceCase(one.outcome)}</span> ${one.role === "scout" ? html `<a class="badge" href="${taskHref(one.taskId)}#report">Report</a> ` : null}<span class="
|
|
827
|
+
const ledgerRows = joinHtml(data.ledger.map(one => html `<p class="row"><a href="/r/${one.runId}">${one.title}</a>${projectChip(one.repo)} <span class="badge ${chipClass(one.outcome)}">${sentenceCase(one.outcome)}</span> ${one.role === "scout" ? html `<a class="badge" href="${taskHref(one.taskId)}#report">Report</a> ` : null}<span class="meta">${one.provider === null ? (one.model === null ? "" : modelName(null, one.model)) : agentModelName(one.provider, one.model)}${one.ranMinutes === null ? null : html ` · ${one.ranMinutes}m`} · ${runCostWords({ authMode: one.authMode, costUsd: one.costUsd, tokensIn: null, tokensOut: null })}${prLink(one)}</span></p>`), "\n");
|
|
812
828
|
return joinHtml([
|
|
813
829
|
html `<div class="control-room-head"><div><h1>Portfolio</h1><p class="meta">Every project and live build in one place</p></div><div class="actions"><a class="badge" href="/tasks/new">+ New task</a><a class="badge" href="/board?scope=all">Full board →</a></div></div>`,
|
|
814
830
|
data.saturated ? html `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : null,
|
|
@@ -906,7 +922,10 @@ export function githubReposPage(chrome, data) {
|
|
|
906
922
|
html `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
|
|
907
923
|
], "\n"), { chrome });
|
|
908
924
|
}
|
|
909
|
-
export function projectsPage(chrome, recent, candidates, open, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn, checkLevelOf) {
|
|
925
|
+
export function projectsPage(chrome, recent, candidates, open, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn, checkLevelOf, activity) {
|
|
926
|
+
const activityHtml = activity === undefined ? null : html `<details><summary>Activity · ${activity.project}</summary>${activity.report === null
|
|
927
|
+
? html `<p role="alert">Activity is unavailable.</p><a href="/projects">Reload</a>`
|
|
928
|
+
: html `<p>${activityScope(activity.report)}</p>${activity.report.truncated ? html `<p>${activityLimit(activity.report)}</p>` : null}${activityMetrics(activity.report).map(metric => html `<details><summary>${metric.label} · ${metric.value}</summary><p>${metric.detail}</p>${metric.rows.length === 0 ? html `<p>No tasks in this group.</p>` : html `<ul>${metric.rows.map(row => html `<li><a href="${row.href}">${row.title}</a> · Recorded checks: ${row.checks}</li>`)}</ul>`}</details>`)}`}</details>`;
|
|
910
929
|
// The onboarding card (repo onboarding, findings 1-39): preview first,
|
|
911
930
|
// then a password-confirmed clone into a configured root. Disabled
|
|
912
931
|
// states explain themselves in words (finding 28/39).
|
|
@@ -1032,11 +1051,12 @@ export function projectsPage(chrome, recent, candidates, open, problem, _unscope
|
|
|
1032
1051
|
recentItems.length === 0 && candidateItems.length === 0
|
|
1033
1052
|
? html `<div class="card"><p><strong>Nothing to open yet.</strong></p><p class="meta">Add one below — opening it registers it here for next time.</p></div>`
|
|
1034
1053
|
: null,
|
|
1054
|
+
activityHtml,
|
|
1035
1055
|
recentItems.length > 0 ? html `<h2>Recent</h2>${cards(recentItems)}` : null,
|
|
1036
1056
|
candidateItems.length > 0 ? html `<h2>Available</h2>${cards(candidateItems)}` : null,
|
|
1037
1057
|
addCard,
|
|
1038
1058
|
], "\n"), { chrome, workspace: { view: {
|
|
1039
|
-
kind: "projects", choosing, problem, returnTo,
|
|
1059
|
+
kind: "projects", choosing, problem, returnTo, ...(activity === undefined ? {} : { activity }),
|
|
1040
1060
|
recent: recent.map(one => rowOf(one.path, one.name, one.lastOpenedAt)),
|
|
1041
1061
|
available: candidates.map(path => rowOf(path, projectName(path), null)),
|
|
1042
1062
|
add: { browse: browsable ? "/projects/browse" : null, github: onboard === null ? null : "/projects/github", html: htmlString(addForms) },
|
|
@@ -1099,7 +1119,7 @@ export function fleetBody(queued, building, runners, queueRevision, visibleRepo)
|
|
|
1099
1119
|
const head = retired
|
|
1100
1120
|
? html `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
|
|
1101
1121
|
: html `${postForm("/queue/note", html `<input type="text" name="note" class="runner-note" value="${runner.queueNote ?? ""}" data-initial="${runner.queueNote ?? ""}" placeholder="what this worker is working through" aria-label="column note" maxlength="200">`, { attrs: { class: "row" }, hidden: { from: "fleet", runner: runner.name } })}<p class="meta">${runnerAlive(runner, new Date()) ? "alive" : "quiet"} · ${live.length}/${runner.capacity} building</p>`;
|
|
1102
|
-
const buildingCards = joinHtml(live.map(claim => html `<div class="lane-card" data-taken="1"><p class="row"><span class="dot dot-ok pulse"></span> ${claim.taskId}</p><p class="row meta">building${chip(claim.repo ?? null)}${claim.model === null ? null : html ` · ${claim.model}`} · ${Math.max(1, Math.round((Date.now() - new Date(claim.claimedAt).getTime()) / 60_000))}m</p></div>`), "\n");
|
|
1122
|
+
const buildingCards = joinHtml(live.map(claim => html `<div class="lane-card" data-taken="1"><p class="row"><span class="dot dot-ok pulse"></span> ${claim.taskId}</p><p class="row meta">building${chip(claim.repo ?? null)}${claim.model === null ? null : html ` · ${modelName(null, claim.model)}`} · ${Math.max(1, Math.round((Date.now() - new Date(claim.claimedAt).getTime()) / 60_000))}m</p></div>`), "\n");
|
|
1103
1123
|
const queuedCards = joinHtml(own.map(queuedCardOf), "\n");
|
|
1104
1124
|
const empty = live.length === 0 && own.length === 0
|
|
1105
1125
|
? html `<p class="meta lane-empty">${retired ? "nothing left" : "idle — will take from the shared queue"}</p>`
|
|
@@ -29,5 +29,17 @@ export declare function wrongHostPage(facts: {
|
|
|
29
29
|
served: string;
|
|
30
30
|
command: string | null;
|
|
31
31
|
}): Html;
|
|
32
|
-
/**
|
|
33
|
-
export
|
|
32
|
+
/** Which field a first-account refusal is about (null: the page itself, as when the tries are spent). */
|
|
33
|
+
export type SignupProblem = {
|
|
34
|
+
field: "code" | "name" | "password" | null;
|
|
35
|
+
text: string;
|
|
36
|
+
};
|
|
37
|
+
/** The username rule, said once for the server's check, the input's `pattern` (anchored by the browser; `\-` so the
|
|
38
|
+
* v-flag parse accepts it) and the hint. */
|
|
39
|
+
export declare const SIGNUP_NAME: RegExp;
|
|
40
|
+
export declare const SIGNUP_PASSWORD_MIN = 8;
|
|
41
|
+
/** The first-account page (setup review): shown only while no approver exists. Rules are stated up front and match
|
|
42
|
+
* the inputs' own constraints. On a refusal the typed username comes back; the setup code and the password never do.
|
|
43
|
+
* The message sits under the field it is about, announced (role="alert"), with that field aria-invalid, described by
|
|
44
|
+
* it and focused, so it works without script (this page has none). */
|
|
45
|
+
export declare function signupPage(problem: SignupProblem | string | null, attemptsLeft: number, typedName?: string): Html;
|
|
@@ -99,21 +99,44 @@ ${facts.command === null ? "" : html `<p>To use this address, stop Toolroll (Ctr
|
|
|
99
99
|
<p class="meta">On that computer, ${code(`http://${facts.served}/`)} always works. Other addresses need your say-so, so no other website can reach it.</p>
|
|
100
100
|
</main></body></html>`;
|
|
101
101
|
}
|
|
102
|
-
/** The
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
102
|
+
/** The username rule, said once for the server's check, the input's `pattern` (anchored by the browser; `\-` so the
|
|
103
|
+
* v-flag parse accepts it) and the hint. */
|
|
104
|
+
export const SIGNUP_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
105
|
+
const SIGNUP_NAME_PATTERN = "[A-Za-z0-9][A-Za-z0-9._\\-]{0,63}";
|
|
106
|
+
export const SIGNUP_PASSWORD_MIN = 8;
|
|
107
|
+
/** The first-account page (setup review): shown only while no approver exists. Rules are stated up front and match
|
|
108
|
+
* the inputs' own constraints. On a refusal the typed username comes back; the setup code and the password never do.
|
|
109
|
+
* The message sits under the field it is about, announced (role="alert"), with that field aria-invalid, described by
|
|
110
|
+
* it and focused, so it works without script (this page has none). */
|
|
111
|
+
export function signupPage(problem, attemptsLeft, typedName = "") {
|
|
112
|
+
const open = attemptsLeft > 0;
|
|
113
|
+
// Out of tries, the page always says why and what to do, also when it is simply opened again.
|
|
114
|
+
const said = typeof problem === "string" ? { field: null, text: problem }
|
|
115
|
+
: problem ?? (open ? null : { field: null, text: "Too many wrong setup codes. Restart Toolroll to get a new code." });
|
|
116
|
+
const field = open ? said?.field ?? null : null;
|
|
117
|
+
const error = (which) => field === which && said !== null ? html `<p class="problem signup-problem" id="signup-problem" role="alert">${said.text}</p>` : "";
|
|
118
|
+
const describe = (which, hint) => field === which ? html ` aria-invalid="true" aria-describedby="${hint} signup-problem" autofocus` : html ` aria-describedby="${hint}"`;
|
|
119
|
+
const tries = attemptsLeft === 1 ? "1 try left" : `${attemptsLeft} tries left`;
|
|
120
|
+
return shell("Create the first account · Toolroll", joinHtml([
|
|
121
|
+
// Inputs on this page reach a 44px target; the console's shared input rule stops at 36px.
|
|
122
|
+
html `<style>.signup h1{margin-bottom:1rem}.signup input{min-height:2.75rem}.signup .field-note{margin:.375rem 0 0;font-size:.75rem;line-height:1.45;color:var(--muted-foreground)}.signup .signup-problem{margin:.5rem 0 0}</style>`,
|
|
123
|
+
html `<div class="login-viewport"><div class="login-shell signup">`,
|
|
124
|
+
html `<p class="so-wordmark login-brand">${BRAND_HTML}</p>`,
|
|
125
|
+
html `<h1>Create the first account</h1>`,
|
|
107
126
|
html `<div class="login-card">`,
|
|
108
|
-
html `<
|
|
109
|
-
|
|
110
|
-
problem === null ? "" : html `<div class="problem">${problem}</div>`,
|
|
111
|
-
attemptsLeft <= 0
|
|
127
|
+
!open || field === null ? (said === null ? "" : html `<div class="problem" role="alert">${said.text}</div>`) : "",
|
|
128
|
+
!open
|
|
112
129
|
? ""
|
|
113
130
|
: postForm("/signup", joinHtml([
|
|
114
|
-
html `<label>Setup code<input type="text" name="code" inputmode="numeric" autocomplete="one-time-code" autofocus></label>`,
|
|
115
|
-
html `<
|
|
116
|
-
|
|
131
|
+
html `<label>Setup code<input type="text" name="code" inputmode="numeric" autocomplete="one-time-code" autocapitalize="none" spellcheck="false" required${describe("code", "signup-code-note")}${field === null ? " autofocus" : ""}></label>`,
|
|
132
|
+
html `<p class="field-note" id="signup-code-note">Printed in the terminal that started Toolroll. ${tries}.</p>`,
|
|
133
|
+
error("code"),
|
|
134
|
+
html `<label>Username<input type="text" name="name" value="${typedName}" autocomplete="username" autocapitalize="none" spellcheck="false" required maxlength="64" pattern="${SIGNUP_NAME_PATTERN}"${describe("name", "signup-name-note")}></label>`,
|
|
135
|
+
html `<p class="field-note" id="signup-name-note">Letters, digits, dots, dashes or underscores, starting with a letter or digit. Up to 64.</p>`,
|
|
136
|
+
error("name"),
|
|
137
|
+
html `<label>Password<input type="password" name="password" autocomplete="new-password" required minlength="${SIGNUP_PASSWORD_MIN}"${describe("password", "signup-password-note")}></label>`,
|
|
138
|
+
html `<p class="field-note" id="signup-password-note">At least ${SIGNUP_PASSWORD_MIN} characters.</p>`,
|
|
139
|
+
error("password"),
|
|
117
140
|
html `<button type="submit">Create account and sign in</button>`,
|
|
118
141
|
], "\n"), { signedOut: true }),
|
|
119
142
|
html `</div>`,
|