toolroll 0.9.82 → 0.9.84
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 +41 -0
- package/README.md +4 -3
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -22
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +7 -0
- package/dist/contracts/console-api.d.ts +2 -1
- package/dist/contracts/console-api.js +5 -1
- 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/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/first-tasks-child.d.ts +1 -0
- package/dist/first-tasks-child.js +11 -0
- package/dist/folder-look.d.ts +110 -0
- package/dist/folder-look.js +306 -0
- package/dist/guides.js +4 -3
- package/dist/learning-child.d.ts +1 -0
- package/dist/learning-child.js +34 -0
- package/dist/learning-look.d.ts +25 -0
- package/dist/learning-look.js +72 -0
- 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 +53 -0
- package/dist/operate.js +277 -49
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-identity.d.ts +44 -0
- package/dist/project-identity.js +153 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/project-tools.d.ts +4 -1
- package/dist/project-tools.js +7 -2
- package/dist/project.d.ts +4 -1
- package/dist/project.js +11 -4
- package/dist/repos.d.ts +7 -0
- package/dist/repos.js +57 -0
- package/dist/serve.js +91 -17
- 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 +5 -2
- package/dist/server/flows.js +13 -3
- package/dist/server/guards.d.ts +4 -0
- package/dist/server/guards.js +12 -3
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.js +9 -0
- package/dist/server/pages.js +102 -92
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-pages.d.ts +12 -16
- package/dist/server/render-pages.js +30 -69
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.js +7 -14
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +8 -1
- package/dist/server/runtime.d.ts +4 -0
- package/dist/server/runtime.js +1 -0
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +211 -71
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/tasks.js +19 -5
- 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/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/dist/up-processes.d.ts +13 -0
- package/dist/up-processes.js +6 -2
- package/package.json +1 -1
|
@@ -9,20 +9,20 @@ import {} from "../dispatch.js";
|
|
|
9
9
|
import {} from "../first-run.js";
|
|
10
10
|
import {} from "../gaps.js";
|
|
11
11
|
import { limitsHtml } from "../limits-ui.js";
|
|
12
|
-
import { ASK_LABEL, ASKS } from "../needs-you.js";
|
|
12
|
+
import { ASK_LABEL, ASKS, START_BUILDER_COMMAND } from "../needs-you.js";
|
|
13
13
|
import {} from "../onboard.js";
|
|
14
14
|
import {} from "../peek.js";
|
|
15
15
|
import { projectName } from "../project.js";
|
|
16
16
|
import {} from "../proof.js";
|
|
17
17
|
import {} from "../quality.js";
|
|
18
18
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
19
|
+
import { agentModelName, modelName } from "../model-names.js";
|
|
19
20
|
import {} from "../scope.js";
|
|
20
21
|
import {} from "../store.js";
|
|
21
22
|
import { runCostWords, spendLine, tally } from "../summary.js";
|
|
22
23
|
import {} from "../work-index.js";
|
|
23
24
|
import { WORK_VIEWS } from "../workspace-ui.js";
|
|
24
25
|
import { createHash } from "node:crypto";
|
|
25
|
-
import { closeSync, constants as fsConstants, lstatSync, openSync, readSync } from "node:fs";
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { join } from "node:path";
|
|
28
28
|
import { buildsViews, CHEVRON_ICON, consoleSpend, FOLDER_PATHS, GRIP_HANDLE, isOverdue, projectChip, relativeAge, safePrUrl, screen, sentenceCase, strokeIcon, TO_FRONT_ICON, when, whenTime } from "./chrome.js";
|
|
@@ -81,9 +81,10 @@ export function inboxPage(chrome, data) {
|
|
|
81
81
|
: 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")}`;
|
|
82
82
|
// The first-run steps, until the first Ready result: each is done or
|
|
83
83
|
// shows the one action that does it.
|
|
84
|
-
const
|
|
84
|
+
const steps = data.wizard;
|
|
85
|
+
const wizard = steps === null
|
|
85
86
|
? null
|
|
86
|
-
: html `<div class="card" data-first-run><p><strong>Get to your first result</strong></p>${joinHtml(
|
|
87
|
+
: 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>`
|
|
87
88
|
: step.action.kind === "link" ? html ` <a href="${step.action.href}">${step.action.label}</a>`
|
|
88
89
|
: html ` <code>${step.action.command}</code>`}</p>`), "\n")}</div>`;
|
|
89
90
|
const noWorker = data.worker.answering > 0
|
|
@@ -147,6 +148,20 @@ export function inboxPage(chrome, data) {
|
|
|
147
148
|
});
|
|
148
149
|
}
|
|
149
150
|
/** System: the machinery — workers, background service, workspaces. */
|
|
151
|
+
/** Start a builder: the one command, as text to select (/start-builder, where every "Connect a builder" leads, and
|
|
152
|
+
* /system#builder). `project`: the open project's name, when the viewer may see it; `connected`: whether a builder
|
|
153
|
+
* that may build there is checking in (null: say nothing); `managed`: whether this person can start one themselves
|
|
154
|
+
* (registering a builder takes an instance operator), else who to ask. */
|
|
155
|
+
export function startBuilderHtml(data) {
|
|
156
|
+
const where = data.project === null ? null : html ` to <strong>${data.project}</strong>`;
|
|
157
|
+
const state = data.connected === null ? null
|
|
158
|
+
: data.connected ? html `<p>A builder is connected${where}. Approved tasks there start on their own.</p>`
|
|
159
|
+
: html `<p><strong>No builder is connected</strong>${where}. Tasks wait until one is.</p>`;
|
|
160
|
+
const title = data.heading === "h1" ? html `<h1 id="builder-heading">Start a builder</h1>` : html `<h2 id="builder-heading">Start a builder</h2>`;
|
|
161
|
+
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
|
|
162
|
+
? html `<p class="meta">Keep it running. Approved tasks start on their own. The console alone (<code>toolroll serve</code>) doesn't build.</p>`
|
|
163
|
+
: 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>`;
|
|
164
|
+
}
|
|
150
165
|
export function systemPage(chrome, data) {
|
|
151
166
|
const nowMs = data.now.getTime();
|
|
152
167
|
const runnerCards = data.runners
|
|
@@ -187,16 +202,15 @@ export function systemPage(chrome, data) {
|
|
|
187
202
|
? html `set for the whole installation${one.setBy === null ? null : html ` by ${one.setBy}`}`
|
|
188
203
|
: html `the default — nothing configured`;
|
|
189
204
|
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>`;
|
|
190
|
-
return html `<p class="row"><span class="mono">${one.phase}</span> <strong>${one.provider
|
|
205
|
+
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>`;
|
|
191
206
|
}), "\n");
|
|
192
|
-
const agentsCard = html `<h2>Agents</h2><p class="meta">Which AI provider runs each phase
|
|
207
|
+
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>`;
|
|
193
208
|
return screen("system", joinHtml([
|
|
194
209
|
html `<h1>System</h1>`,
|
|
195
210
|
html `<p class="hint">builders execute tasks in isolated temporary copies of each project</p>`,
|
|
196
211
|
agentsCard,
|
|
197
|
-
cards.length === 0
|
|
198
|
-
|
|
199
|
-
: html `<div class="cards">${cards}</div>`,
|
|
212
|
+
cards.length === 0 ? null : html `<div class="cards">${cards}</div>`,
|
|
213
|
+
startBuilderHtml({ heading: "h2", project: null, connected: data.runners.some(one => one.retiredAt === null && runnerAlive(one, data.now)) ? null : false, managed: true }),
|
|
200
214
|
data.outboxPending > 0 ? html `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : null,
|
|
201
215
|
(data.externalWork ?? []).length === 0
|
|
202
216
|
? null
|
|
@@ -373,11 +387,11 @@ export function boardBody(data, ciRed) {
|
|
|
373
387
|
? null
|
|
374
388
|
: 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>`;
|
|
375
389
|
// Who, which model and which branch, as one muted line: words in Geist, the model and branch in mono.
|
|
376
|
-
const model = claim.model === null ? null :
|
|
390
|
+
const model = claim.model === null ? null : claim.provider === null ? modelName(null, claim.model) : agentModelName(claim.provider, claim.model);
|
|
377
391
|
const branch = claim.branch === null ? null : `${claim.branch}${workspace === null ? "" : ` · ${workspace}`}`;
|
|
378
392
|
const who = joinHtml([
|
|
379
393
|
html `<span>${claim.runner}</span>`,
|
|
380
|
-
model === null ? null : html `<span
|
|
394
|
+
model === null ? null : html `<span>${model}</span>`,
|
|
381
395
|
branch === null ? null : html `<span class="mono">${branch}</span>`,
|
|
382
396
|
].filter((one) => one !== null), html `<span aria-hidden="true"> · </span>`);
|
|
383
397
|
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([
|
|
@@ -478,7 +492,7 @@ export function homePage(chrome, data) {
|
|
|
478
492
|
// an operator actually watches this page, so it re-renders itself.
|
|
479
493
|
const building = data.building.length === 0
|
|
480
494
|
? null
|
|
481
|
-
: 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>`;
|
|
495
|
+
: 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>`;
|
|
482
496
|
// THE FLEET: runners by heartbeat age, worktrees by lease state, the watch.
|
|
483
497
|
const nowMs = data.now.getTime();
|
|
484
498
|
const runnerCards = data.runners
|
|
@@ -798,7 +812,7 @@ export function portfolioOverview(data) {
|
|
|
798
812
|
const outcomeWords = groups.filter(([, count]) => count > 0).map(([word, count]) => `${count} ${word}`).join(" · ");
|
|
799
813
|
const summary = tally(data.runs24);
|
|
800
814
|
// ---- running: current live claims, project chips on ----
|
|
801
|
-
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="
|
|
815
|
+
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");
|
|
802
816
|
// ---- the ledger: terminal runs started in the window, one chip each ----
|
|
803
817
|
const chipClass = (outcome) => outcome === "built" || outcome === "no-change" ? "badge-done" : outcome === "failed" ? "badge-failed" : "";
|
|
804
818
|
const prLink = (one) => {
|
|
@@ -809,7 +823,7 @@ export function portfolioOverview(data) {
|
|
|
809
823
|
const safe = safePrUrl(one.prUrl);
|
|
810
824
|
return safe === null ? html ` · PR #${one.prNumber}` : html ` · <a href="${safe}">PR #${one.prNumber}</a>`;
|
|
811
825
|
};
|
|
812
|
-
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="
|
|
826
|
+
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");
|
|
813
827
|
return joinHtml([
|
|
814
828
|
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>`,
|
|
815
829
|
data.saturated ? html `<div class="problem">This overview reached its 200-task display cap; the task list holds the rest.</div>` : null,
|
|
@@ -865,60 +879,6 @@ export function githubIdentityOf(remoteUrl) {
|
|
|
865
879
|
return `${ssh[1]}/${ssh[2]}`;
|
|
866
880
|
return null;
|
|
867
881
|
}
|
|
868
|
-
/**
|
|
869
|
-
* The `origin` remote's url from a repository's OWN .git/config. BOUNDED
|
|
870
|
-
* I/O by construction (ghlist review, finding 1): the final component may
|
|
871
|
-
* not be a symlink (O_NOFOLLOW + lstat), must be a REGULAR file under a
|
|
872
|
-
* size cap, and at most 64 KiB are ever read — a sparse monster or a fifo
|
|
873
|
-
* planted as a "config" reads as null, never as a hang. Parsed LINE BY
|
|
874
|
-
* LINE with real section tracking (finding 2): a `[remote "origin"]`
|
|
875
|
-
* embedded inside some other value never opens the section. null when
|
|
876
|
-
* unreadable or origin-less; a worktree-style `.git` FILE (gitdir pointer)
|
|
877
|
-
* reads null too, which is honest — its identity lives elsewhere. The
|
|
878
|
-
* answer is ADVISORY metadata for offering a button: the /projects/open
|
|
879
|
-
* road re-proves path, ceiling, and git-ness before anything mutates, and
|
|
880
|
-
* a config that lies about its origin can only mislabel a repository the
|
|
881
|
-
* operator was already allowed to open.
|
|
882
|
-
*/
|
|
883
|
-
export function originUrlOf(repoPath) {
|
|
884
|
-
const file = join(repoPath, ".git", "config");
|
|
885
|
-
let fd = null;
|
|
886
|
-
try {
|
|
887
|
-
const stat = lstatSync(file);
|
|
888
|
-
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
889
|
-
return null;
|
|
890
|
-
fd = openSync(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
891
|
-
const buffer = Buffer.alloc(64 * 1024);
|
|
892
|
-
const read = readSync(fd, buffer, 0, buffer.length, 0);
|
|
893
|
-
const config = buffer.toString("utf8", 0, read);
|
|
894
|
-
let inOrigin = false;
|
|
895
|
-
for (const line of config.split("\n")) {
|
|
896
|
-
if (/^\s*\[/.test(line)) {
|
|
897
|
-
inOrigin = /^\s*\[remote "origin"\]\s*$/.test(line);
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
if (!inOrigin)
|
|
901
|
-
continue;
|
|
902
|
-
const url = /^\s*url\s*=\s*(.+)$/.exec(line)?.[1];
|
|
903
|
-
if (url !== undefined)
|
|
904
|
-
return url.trim();
|
|
905
|
-
}
|
|
906
|
-
return null;
|
|
907
|
-
}
|
|
908
|
-
catch {
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
finally {
|
|
912
|
-
if (fd !== null) {
|
|
913
|
-
try {
|
|
914
|
-
closeSync(fd);
|
|
915
|
-
}
|
|
916
|
-
catch {
|
|
917
|
-
// already closed
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
882
|
/** The GitHub listing page: the account's repositories with ONE honest
|
|
923
883
|
* action each. Names, descriptions, and paths are gh/filesystem DATA —
|
|
924
884
|
* escaped at the sink like everything else. */
|
|
@@ -955,6 +915,7 @@ export function githubReposPage(chrome, data) {
|
|
|
955
915
|
return screen("projects", joinHtml([
|
|
956
916
|
html `<h1>Your GitHub repositories</h1>`,
|
|
957
917
|
html `<p class="meta">Repositories available to the GitHub account signed in on this machine — open one you already have, or clone a new one after a quick preview.</p>`,
|
|
918
|
+
data.localUnanswered === true ? html `<p class="meta">Your projects folder didn't answer, so copies already on this machine aren't matched yet. Try again in a moment.</p>` : null,
|
|
958
919
|
rows,
|
|
959
920
|
data.listed === null ? null : list("Refresh list"),
|
|
960
921
|
html `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
|
|
@@ -1153,7 +1114,7 @@ export function fleetBody(queued, building, runners, queueRevision, visibleRepo)
|
|
|
1153
1114
|
const head = retired
|
|
1154
1115
|
? html `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
|
|
1155
1116
|
: 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>`;
|
|
1156
|
-
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");
|
|
1117
|
+
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");
|
|
1157
1118
|
const queuedCards = joinHtml(own.map(queuedCardOf), "\n");
|
|
1158
1119
|
const empty = live.length === 0 && own.length === 0
|
|
1159
1120
|
? 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>`,
|
|
@@ -511,6 +511,7 @@ export const SETTINGS_GROUPS = [
|
|
|
511
511
|
["Agents", [
|
|
512
512
|
["/settings/lead", "Lead", html `<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 9h8M8 13h5"/>`],
|
|
513
513
|
["/settings/models", "Models", html `<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2"/>`],
|
|
514
|
+
["/settings/spend", "Spend", html `<circle cx="12" cy="12" r="9"/><path d="M15 9.5c-.6-.9-1.7-1.5-3-1.5-1.7 0-3 .9-3 2s1.3 1.7 3 2 3 .9 3 2-1.3 2-3 2c-1.3 0-2.4-.6-3-1.5M12 6v2M12 16v2"/>`],
|
|
514
515
|
["/settings/skills", "Skills", html `<path d="m12 3 1.9 5.8L20 10l-5 3.6L16.8 20 12 16.4 7.2 20 9 13.6 4 10l6.1-1.2z"/>`],
|
|
515
516
|
["/settings/tools", "Tools", html `<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>`],
|
|
516
517
|
["/settings/knowledge", "Knowledge", html `<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20V3H6.5A2.5 2.5 0 0 0 4 5.5z"/><path d="M4 19.5A2.5 2.5 0 0 0 6.5 22H20v-5"/>`],
|
|
@@ -25,13 +25,14 @@ import { taskRoad } from "../first-run.js";
|
|
|
25
25
|
import { starterForWork } from "../flow-starters.js";
|
|
26
26
|
import {} from "../gaps.js";
|
|
27
27
|
import { readAuthModeStrict } from "../keys.js";
|
|
28
|
-
import { failedAttemptSentence, retryNoteOf } from "../needs-you.js";
|
|
28
|
+
import { failedAttemptSentence, retryNoteOf, START_BUILDER_COMMAND, startBuilderHref } from "../needs-you.js";
|
|
29
29
|
import { chosenWords, makesNoPlan, PHASES as ROUTE_PHASES, TASK_SIZES } from "../phase-routing.js";
|
|
30
30
|
import { parseExecutionPlanDocument } from "../plan.js";
|
|
31
31
|
import {} from "../planner-source.js";
|
|
32
32
|
import { projectName } from "../project.js";
|
|
33
33
|
import { dispatchStatusToken, manualReviewOnly, plainReasonWords, reviewConflict } from "../proof.js";
|
|
34
34
|
import { providerName } from "../provider-auth.js";
|
|
35
|
+
import { agentModelName } from "../model-names.js";
|
|
35
36
|
import {} from "../provider.js";
|
|
36
37
|
import { qualityModeTitle } from "../quality.js";
|
|
37
38
|
import { evidenceShortenedWords } from "../result-review.js";
|
|
@@ -134,17 +135,7 @@ export function acceptanceCeremonyHtml(criteria) {
|
|
|
134
135
|
}
|
|
135
136
|
/** "claude" + "opus" → "Claude Opus"; "claude-opus-5-5" → "Claude Opus 5.5"; anything else keeps its exact id. */
|
|
136
137
|
export function agentNameWords(provider, model) {
|
|
137
|
-
|
|
138
|
-
if (model === null || model === "" || model === "default")
|
|
139
|
-
return name;
|
|
140
|
-
const bare = model.startsWith(`${provider}-`) ? model.slice(provider.length + 1) : model;
|
|
141
|
-
const title = (word) => `${word[0].toUpperCase()}${word.slice(1)}`;
|
|
142
|
-
if (/^[a-z]+$/.test(bare))
|
|
143
|
-
return `${name} ${title(bare)}`;
|
|
144
|
-
const versioned = /^([a-z]+)-(\d+(?:-\d+)*)$/.exec(bare);
|
|
145
|
-
if (versioned !== null)
|
|
146
|
-
return `${name} ${title(versioned[1])} ${versioned[2].replace(/-/g, ".")}`;
|
|
147
|
-
return `${name} ${model}`;
|
|
138
|
+
return agentModelName(provider, model);
|
|
148
139
|
}
|
|
149
140
|
/** The permission a sealed profile grants, in two or three words. */
|
|
150
141
|
export function permissionWordsOf(profile) {
|
|
@@ -815,7 +806,9 @@ export function taskRecoveryHref(taskId, diagnosis) {
|
|
|
815
806
|
return null;
|
|
816
807
|
const task = taskHref(taskId);
|
|
817
808
|
switch (diagnosis.action) {
|
|
809
|
+
// The task page only repeats that no builder is connected: lead to the command that connects one.
|
|
818
810
|
case "start-worker":
|
|
811
|
+
return startBuilderHref(taskId);
|
|
819
812
|
case "repair-dependency":
|
|
820
813
|
return `${task}#run-status`;
|
|
821
814
|
case "retry-task":
|
|
@@ -1350,11 +1343,11 @@ export function taskBodyParts(data) {
|
|
|
1350
1343
|
firstConnection
|
|
1351
1344
|
? html `<p>Toolroll is open, but this project has not been connected to a builder yet.</p><p>On the machine where the project lives, open that folder and run:</p>`
|
|
1352
1345
|
: html `<p>Toolroll is open, but this project's builder stopped checking in. Reopen Toolroll on the machine where the project lives.</p><p>If you normally start it from a terminal, open the project folder and run:</p>`,
|
|
1353
|
-
html `<code class="dispatch-recovery-command"
|
|
1346
|
+
html `<code class="dispatch-recovery-command">${START_BUILDER_COMMAND}</code>`,
|
|
1354
1347
|
firstConnection
|
|
1355
1348
|
? html `<p class="meta">This is the normal start command: it opens the app, connects the project, and starts its builder. Keep Toolroll running; approved tasks begin automatically.</p>`
|
|
1356
1349
|
: html `<p class="meta">This task resumes automatically when the builder reconnects. You do not need to file or approve it again.</p>`,
|
|
1357
|
-
html `<p><a href="
|
|
1350
|
+
html `<p><a href="${startBuilderHref(task.id)}">See connection status →</a></p></div></details>`,
|
|
1358
1351
|
]);
|
|
1359
1352
|
}
|
|
1360
1353
|
// Never a link to this page's own acts: Retry sits on the status card and below.
|
|
@@ -43,6 +43,8 @@ export type RequestFacts = {
|
|
|
43
43
|
};
|
|
44
44
|
/** Passwordless is on and this is a signed-in browser: ceremony password fields render as nothing (html.ts reauth). */
|
|
45
45
|
passwordless?: boolean;
|
|
46
|
+
/** The browser session's open project (display only: the sidebar's picker names it). */
|
|
47
|
+
openProject?: string | null;
|
|
46
48
|
};
|
|
47
49
|
/**
|
|
48
50
|
* The request's own session facts, readable from anywhere below the
|
|
@@ -133,12 +133,15 @@ get("pages", "workbench", "/workbench", free);
|
|
|
133
133
|
get("tasks", "board", "/board", { limited: "collection" });
|
|
134
134
|
get("tasks", "review", "/review", { limited: "collection" });
|
|
135
135
|
get("settings", "spend", "/spend", { ...free, ...anyOperator("An instance operator sees spend.", "/") });
|
|
136
|
+
get("settings", "settings.spend", "/settings/spend", { ...free, ...anyOperator("An instance operator sets spending limits.", "/settings") });
|
|
136
137
|
get("pages", "ledger", "/ledger", { limited: "unscoped", ...free });
|
|
137
138
|
get("pages", "activity", "/activity");
|
|
138
139
|
get("tasks", "done", "/done", { limited: "collection" });
|
|
139
140
|
get("settings", "mode", "/mode");
|
|
140
141
|
get("people", "people.page", "/people", { limited: "unscoped", ...free });
|
|
141
142
|
get("pages", "system", "/system");
|
|
143
|
+
// Where every "Connect a builder" leads: read-only, open to a project-limited person (the handler shows only a visible project).
|
|
144
|
+
get("pages", "start-builder", "/start-builder", { limited: "unscoped", ...free });
|
|
142
145
|
get("tasks", "tasks", "/tasks", { limited: "collection", ...free }); // a redirect to /work now: never sent to the project opener first
|
|
143
146
|
get("tasks", "queue", "/queue");
|
|
144
147
|
get("pages", "peek", "/peek", free);
|
|
@@ -334,8 +337,12 @@ for (const one of ["policy", "approval", "request-limits", "sessions", "sign-in"
|
|
|
334
337
|
}
|
|
335
338
|
post("settings", "settings.flows-on", "/settings/flows/on", { limited: "collection", ...approver("An approver switches starter flows on.", "/settings/flows") });
|
|
336
339
|
const MODELS = approver("Sign in as an approver to change models.", "/settings/models");
|
|
337
|
-
for (const one of ["check", "watch", "update"
|
|
340
|
+
for (const one of ["check", "watch", "update"])
|
|
338
341
|
post("settings", `settings.models-${one}`, `/settings/models/${one}`, MODELS);
|
|
342
|
+
// Changing which model a role uses: an instance operator's browser step-up (the handler proves the password, or the
|
|
343
|
+
// Passwordless session; model-choices.ts re-proves the session and standing inside its one transaction).
|
|
344
|
+
for (const one of ["agent", "apply"])
|
|
345
|
+
post("settings", `settings.models-${one}`, `/settings/models/${one}`, { scope: "step-up", ...operator("An instance operator changes models.", "/settings/models") });
|
|
339
346
|
// Any other models action answers the models area's own refusal.
|
|
340
347
|
post("settings", "settings.models-act", "^\\/settings\\/models\\/.*$", { sample: "/settings/models/unknown", ...MODELS });
|
|
341
348
|
post("settings", "settings.knowledge-refresh", "/settings/knowledge/refresh", { project: "form", limited: "unscoped", ...approver("Sign in as an approver to refresh project context.", "/settings/knowledge") });
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProjectIdentities } from "../project-identity.js";
|
|
1
2
|
import type { FreshFacts } from '../scope.js';
|
|
2
3
|
import type { createPersonalQueueRuntime } from '../lead-queue-runtime.js';
|
|
3
4
|
import type { FlowRooms } from '../flow-live.js';
|
|
@@ -95,7 +96,10 @@ export interface ServerRuntime {
|
|
|
95
96
|
liveCeiling: () => {
|
|
96
97
|
repos: string[];
|
|
97
98
|
roots: readonly string[];
|
|
99
|
+
canon?: (path: string) => string | null;
|
|
98
100
|
};
|
|
101
|
+
/** Project identities without reading folders on the console's thread (project-identity.ts). */
|
|
102
|
+
identities: ProjectIdentities;
|
|
99
103
|
taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean, page?: TaskPageExtras) => void;
|
|
100
104
|
runVisible: (run: Run) => boolean;
|
|
101
105
|
runIsLive: (run: Pick<Run, "outcome" | "leaseId" | "taskRef">) => boolean;
|
package/dist/server/runtime.js
CHANGED
|
@@ -2,7 +2,7 @@ import type { ServerRuntime } from './runtime.js';
|
|
|
2
2
|
import { type RoleView } from "../models-ui.js";
|
|
3
3
|
export declare function createSettingsHandlers(runtime: ServerRuntime): {
|
|
4
4
|
registrations: import("./handler-registry.js").Registration[];
|
|
5
|
-
modelsView: (canManage: boolean, csrf: string, now: Date, said: string | null, problem: string | null) => {
|
|
5
|
+
modelsView: (canManage: boolean, csrf: string, now: Date, said: string | null, problem: string | null, query?: URLSearchParams) => {
|
|
6
6
|
runtimes: import("../model-catalog.js").RuntimeState[];
|
|
7
7
|
watch: import("../model-catalog.js").WatchState;
|
|
8
8
|
roles: RoleView[];
|
|
@@ -14,5 +14,62 @@ export declare function createSettingsHandlers(runtime: ServerRuntime): {
|
|
|
14
14
|
words: string;
|
|
15
15
|
} | null;
|
|
16
16
|
fresh: import("../model-catalog.js").SeenModel[];
|
|
17
|
+
scope: {
|
|
18
|
+
value: string;
|
|
19
|
+
label: string;
|
|
20
|
+
digest: string;
|
|
21
|
+
options: {
|
|
22
|
+
value: string;
|
|
23
|
+
label: string;
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
26
|
+
inUse: string | null;
|
|
27
|
+
presets: {
|
|
28
|
+
name: "opus" | "sized";
|
|
29
|
+
label: string;
|
|
30
|
+
words: string;
|
|
31
|
+
inUse: boolean;
|
|
32
|
+
recommended: boolean;
|
|
33
|
+
}[];
|
|
34
|
+
newer: {
|
|
35
|
+
from: string;
|
|
36
|
+
to: string;
|
|
37
|
+
name: string;
|
|
38
|
+
roles: string;
|
|
39
|
+
}[];
|
|
40
|
+
preview: {
|
|
41
|
+
title: string;
|
|
42
|
+
rows: {
|
|
43
|
+
role: string;
|
|
44
|
+
before: string;
|
|
45
|
+
after: string;
|
|
46
|
+
notes: string[];
|
|
47
|
+
}[];
|
|
48
|
+
hidden: {
|
|
49
|
+
scope: string;
|
|
50
|
+
digest: string;
|
|
51
|
+
change: string;
|
|
52
|
+
};
|
|
53
|
+
} | null;
|
|
54
|
+
unresolved: string | null;
|
|
55
|
+
sizing: {
|
|
56
|
+
small: string;
|
|
57
|
+
routine: string;
|
|
58
|
+
large: string;
|
|
59
|
+
risky: string;
|
|
60
|
+
classifier: string;
|
|
61
|
+
};
|
|
62
|
+
routing: string;
|
|
63
|
+
recent: {
|
|
64
|
+
taskId: string;
|
|
65
|
+
runId: number;
|
|
66
|
+
role: string;
|
|
67
|
+
chosen: string;
|
|
68
|
+
ranOn: string;
|
|
69
|
+
}[];
|
|
70
|
+
waiting: {
|
|
71
|
+
taskId: string;
|
|
72
|
+
title: string;
|
|
73
|
+
}[];
|
|
17
74
|
};
|
|
18
75
|
};
|