toolroll 0.8.2 → 0.9.1
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 +49 -0
- package/dist/agent-onboard.d.ts +24 -1
- package/dist/agent-onboard.js +53 -1
- package/dist/backup-ui.js +4 -2
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +33 -32
- package/dist/browser-workspace.d.ts +22 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +43 -3
- package/dist/chat-delivery.js +51 -3
- package/dist/chat-quiet.d.ts +28 -0
- package/dist/chat-quiet.js +161 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/digest-times.d.ts +3 -0
- package/dist/digest-times.js +7 -0
- package/dist/dispose.js +2 -0
- package/dist/export.js +1 -1
- package/dist/flow-engine.js +13 -1
- package/dist/flow-pull-request.d.ts +11 -0
- package/dist/flow-pull-request.js +123 -0
- package/dist/flow-starters-ui.d.ts +20 -0
- package/dist/flow-starters-ui.js +35 -0
- package/dist/flow-starters.d.ts +55 -0
- package/dist/flow-starters.js +104 -0
- package/dist/flow-steps.js +14 -0
- package/dist/flows-ui.d.ts +1 -1
- package/dist/flows-ui.js +4 -3
- package/dist/flows.d.ts +30 -6
- package/dist/flows.js +110 -6
- package/dist/integrations-ui.d.ts +13 -0
- package/dist/integrations-ui.js +70 -0
- package/dist/integrations.d.ts +113 -0
- package/dist/integrations.js +526 -0
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +1 -1
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +18 -5
- package/dist/models-ui.js +3 -2
- package/dist/monitoring-ui.js +2 -1
- package/dist/operate.d.ts +5 -2
- package/dist/operate.js +169 -4
- package/dist/policy-ui.js +2 -1
- package/dist/project-delete.js +1 -0
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +9 -2
- package/dist/pull-request-flow.d.ts +172 -0
- package/dist/pull-request-flow.js +475 -0
- package/dist/pull-request-schema.d.ts +5 -0
- package/dist/pull-request-schema.js +21 -0
- package/dist/pull-request-ui.d.ts +23 -0
- package/dist/pull-request-ui.js +55 -0
- package/dist/pulls.d.ts +5 -0
- package/dist/pulls.js +11 -0
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +371 -58
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +2 -1
- package/dist/store.d.ts +61 -0
- package/dist/store.js +144 -3
- package/dist/surface.js +3 -1
- package/dist/telegram-progress.d.ts +1 -0
- package/dist/telegram-progress.js +1 -1
- package/dist/telegram.js +95 -15
- package/dist/when-html.d.ts +8 -0
- package/dist/when-html.js +36 -0
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -25,6 +25,8 @@ import { grantTool, revokeTool, rulesFromForm, setToolRules } from "./teammate-t
|
|
|
25
25
|
import { addKitGithubTrigger, addKitSample, kitInstalled, kitOf, setUpKit } from "./kits.js";
|
|
26
26
|
import { CONNECT_CALLBACK, connectionsOf, finishConnect, oneClickOf, startConnect } from "./mcp-connect.js";
|
|
27
27
|
import { KITS_CSS, kitPageHtml, kitsGalleryHtml } from "./kits-ui.js";
|
|
28
|
+
import { STARTERS_CSS, startersHtml } from "./flow-starters-ui.js";
|
|
29
|
+
import { starterForWork, starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
28
30
|
import { createTeammateFrom, labelOf, nameOf, saveSoul, setTeammateState, teammateSettings, sendTeammateSummaries } from "./teammate-admin.js";
|
|
29
31
|
import { editMemory, forgetMemory, tellTeammate } from "./teammate-memory.js";
|
|
30
32
|
import { addRoutine, removeRoutine, runRoutine } from "./teammate-desk.js";
|
|
@@ -56,6 +58,8 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
|
|
|
56
58
|
import { workIndexPage, workCountsByProject, WorkIndexCursorError } from "./work-index.js";
|
|
57
59
|
import { openWorkDecisionOf } from "./work-summary.js";
|
|
58
60
|
import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
|
|
61
|
+
import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
|
|
62
|
+
import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequestBlocker, pullRequestViewOf, publishingOf, saveMergeSettings, savePublishing, MERGE_METHODS } from './pull-request-flow.js';
|
|
59
63
|
import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
|
|
60
64
|
import { assignmentCatchUp } from './assignment-brief.js';
|
|
61
65
|
import { assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
|
|
@@ -167,7 +171,8 @@ import { dirname } from "node:path";
|
|
|
167
171
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
168
172
|
import { loadOrCreateVapidKeys, validatePushEndpoint } from "./push.js";
|
|
169
173
|
import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, isLargeRepo } from "./onboard.js";
|
|
170
|
-
import { verifiedAuthor, LEAD_THREAD } from "./store.js";
|
|
174
|
+
import { verifiedAuthor, LEAD_THREAD, isDigestTime } from "./store.js";
|
|
175
|
+
import { digestTimes } from "./digest-times.js";
|
|
171
176
|
import { updateRepos, addRepos, removeRepos } from "./repos.js";
|
|
172
177
|
import { run as execRun } from "./exec.js";
|
|
173
178
|
import { findFirstTasks, firstResultWords, firstRunSteps, firstTaskSuggestions, SANDBOX_COMMAND } from "./first-run.js";
|
|
@@ -201,6 +206,8 @@ import { accessFromGroups, accountNameFor, discoverOidc, exchangeOidcCode, newOi
|
|
|
201
206
|
import { readSsoSettings, removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from "./sso-settings.js";
|
|
202
207
|
import { monitoringChange, readMonitoring, saveMonitoring } from "./monitoring-settings.js";
|
|
203
208
|
import { MONITORING_CSS, monitoringHtml, signingSecretHtml } from "./monitoring-ui.js";
|
|
209
|
+
import { INTEGRATIONS_CSS, integrationsHtml } from "./integrations-ui.js";
|
|
210
|
+
import { createIntegrationMonitor } from "./integrations.js";
|
|
204
211
|
import { BACKUP_CSS, backupHtml } from "./backup-ui.js";
|
|
205
212
|
import { BACKUP_EVERY_HOURS, MAX_KEEP, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder } from "./backup.js";
|
|
206
213
|
import { prometheusMetrics } from "./metrics.js";
|
|
@@ -241,6 +248,7 @@ import { cachedRelease, isNewer, runnerVersions, setUpdateChecks, updateChecksOf
|
|
|
241
248
|
import { installMethod } from "./install-method.js";
|
|
242
249
|
import { PACKAGE_VERSION } from "./version.js";
|
|
243
250
|
import { updateNoticeWords } from "./update-notice.js";
|
|
251
|
+
import { whenHtml } from "./when-html.js";
|
|
244
252
|
const SESSION_COOKIE = "standing-orders_session";
|
|
245
253
|
const BODY_CAP = 16 * 1024;
|
|
246
254
|
// URL encoding can triple UTF-8 bytes. Admit the existing bounded task
|
|
@@ -438,6 +446,19 @@ export function createDecisionServer(options) {
|
|
|
438
446
|
catch { /* the pause keeps its own state */ }
|
|
439
447
|
return value;
|
|
440
448
|
};
|
|
449
|
+
// Settings → Integrations: checked in the background with a short cache, as the first-run suggestions are.
|
|
450
|
+
const integrations = createIntegrationMonitor(() => ({
|
|
451
|
+
store,
|
|
452
|
+
dir: options.configDir ?? null,
|
|
453
|
+
telegramTokenFile: options.telegramTokenFile ?? null,
|
|
454
|
+
env: process.env,
|
|
455
|
+
repos: managedRepos(),
|
|
456
|
+
gh: options.firstTaskRunner ?? execRun,
|
|
457
|
+
checkConnection: connectionCheck,
|
|
458
|
+
toolHome,
|
|
459
|
+
clock,
|
|
460
|
+
...options.integrationIo,
|
|
461
|
+
}));
|
|
441
462
|
const modelCatalog = openRouterModelsCache(options.modelCatalogFetcher);
|
|
442
463
|
const modelSeams = {
|
|
443
464
|
home: providerHome,
|
|
@@ -1564,9 +1585,9 @@ export function createDecisionServer(options) {
|
|
|
1564
1585
|
refuse(response, who, 404, 'No such action in your projects.', '/projects');
|
|
1565
1586
|
return false;
|
|
1566
1587
|
}
|
|
1567
|
-
const read = new Set(["/settings/skills", "/settings/knowledge", "/settings", "/settings/learning", "/recipes", "/recipes/run", "/recipes/start", "/recipes/new", "/recipes/edit", "/recipes/from-task", "/recipes/preview", "/recipes/export", "/", "/inbox", "/work", "/projects", "/people", "/ledger", "/ledger/export", "/next", "/board", "/tasks", "/tasks/new", "/runs", "/review", "/done", "/routines", "/menu"]);
|
|
1568
|
-
const write = new Set(["/settings/skills/import", "/settings/skills/change", "/settings/skills/revise", "/settings/knowledge/change", "/settings/knowledge/refresh", "/settings/learning/change", "/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/projects/select", "/tasks/add", "/routines/add"]);
|
|
1569
|
-
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|stop|resume-arm|resume)$");
|
|
1588
|
+
const read = new Set(["/settings/flows", "/settings/skills", "/settings/knowledge", "/settings", "/settings/learning", "/recipes", "/recipes/run", "/recipes/start", "/recipes/new", "/recipes/edit", "/recipes/from-task", "/recipes/preview", "/recipes/export", "/", "/inbox", "/work", "/projects", "/people", "/ledger", "/ledger/export", "/next", "/board", "/tasks", "/tasks/new", "/runs", "/review", "/done", "/routines", "/menu"]);
|
|
1589
|
+
const write = new Set(["/settings/flows/on", "/settings/skills/import", "/settings/skills/change", "/settings/skills/revise", "/settings/knowledge/change", "/settings/knowledge/refresh", "/settings/learning/change", "/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/projects/select", "/tasks/add", "/routines/add"]);
|
|
1590
|
+
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|stop|resume-arm|resume)$");
|
|
1570
1591
|
const resource = request.method === "GET"
|
|
1571
1592
|
? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path)
|
|
1572
1593
|
: /^\/d\/[0-9]{1,15}\/answer$/.test(path) || /^\/routines\/[0-9]{1,15}\/(approve|refresh|pause|resume|run-now)$/.test(path) || path === "/flows/new" || /^\/flows\/[0-9]{1,15}\/(save|cards|archive|scripts)$/.test(path) || /^\/flows\/[0-9]{1,15}\/cards\/[0-9]{1,15}\/(move|decide|cancel|comment|assign|watch)$/.test(path) || /^\/flows\/[0-9]{1,15}\/triggers(\/[0-9]{1,15}\/(pause|resume|remove|check|press|renew|secret|share|unshare))?$/.test(path) || /^\/r\/[0-9]{1,15}\/(note|comment|revise|draft-repair)$/.test(path);
|
|
@@ -3305,7 +3326,7 @@ export function createDecisionServer(options) {
|
|
|
3305
3326
|
back: { href: roomId ? "/chat?conversation=" + encodeURIComponent(roomId) : taskChatHref(focusTask?.id ?? ""), label: "Back to chat" },
|
|
3306
3327
|
}) + (who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check'
|
|
3307
3328
|
&& focusTask.assignment.receipt?.runId === resultRun.id
|
|
3308
|
-
? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf) : '');
|
|
3329
|
+
? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf, pullRequestTargetOf(resultRun.id)) : '');
|
|
3309
3330
|
const focusProblem = requestedTask !== null && focusTask === null
|
|
3310
3331
|
? "That task is not available in this workspace."
|
|
3311
3332
|
: requestedResult !== null && focusTask !== null && resultRun === null
|
|
@@ -3621,6 +3642,27 @@ export function createDecisionServer(options) {
|
|
|
3621
3642
|
sessions: store.webSessions(everyone ? null : who.name).map(one => ({ ...one, here: one.idHash === here })), tokens: store.apiTokens(everyone ? null : who.name) };
|
|
3622
3643
|
return sendScreen(response, 200, screen("Sessions & tokens", `<p><a href="/settings">Settings</a></p><h1>Sessions & tokens</h1>${credentialsHtml(view, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
|
|
3623
3644
|
}
|
|
3645
|
+
// Settings → Flows: the starter flows, each switched on with one yes. ?starter= marks the one a task's
|
|
3646
|
+
// "Do this every time…" asked about.
|
|
3647
|
+
if (url.pathname === "/settings/flows") {
|
|
3648
|
+
const projects = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])].filter(visible);
|
|
3649
|
+
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3650
|
+
if (chosen && !projects.includes(chosen))
|
|
3651
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings");
|
|
3652
|
+
const content = chosen === "" ? "<p>Add a project to switch on starter flows.</p>" : startersHtml({
|
|
3653
|
+
repo: chosen, projects: projects.map(path => ({ path, name: projectName(path) })), starters: startersFor(store, chosen), csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
3654
|
+
canSwitch: who.via === "cookie" && who.role === "approver" && !store.isDemo(), suggested: starterOf(url.searchParams.get("starter") ?? "")?.id ?? null,
|
|
3655
|
+
said: url.searchParams.get("said"), problem: url.searchParams.get("problem")
|
|
3656
|
+
});
|
|
3657
|
+
return sendScreen(response, 200, screen("Flows", `<p><a href="/settings">Settings</a></p><h1>Flows</h1>${content}`, { chrome: chromeFor(chosen || project, "settings") }));
|
|
3658
|
+
}
|
|
3659
|
+
// Settings → Integrations: which integrations work. The list is the last checks; a render never waits on one.
|
|
3660
|
+
if (url.pathname === "/settings/integrations") {
|
|
3661
|
+
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
3662
|
+
return refuse(response, who, 403, "An installation approver sees integrations.", "/settings");
|
|
3663
|
+
const list = integrations.list();
|
|
3664
|
+
return sendScreen(response, 200, screen("Integrations", `<p><a href="/settings">Settings</a></p><h1>Integrations</h1>${integrationsHtml(list, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), checking: integrations.checking })}`, { chrome: chromeFor(project, "settings") }));
|
|
3665
|
+
}
|
|
3624
3666
|
// v104: Settings → Monitoring, where the audit stream and traces go. An instance operator's page.
|
|
3625
3667
|
if (url.pathname === "/settings/monitoring") {
|
|
3626
3668
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
@@ -3659,6 +3701,20 @@ export function createDecisionServer(options) {
|
|
|
3659
3701
|
const plan = await checkoutPlan(store, storagePool(databaseFile), now, { manual: true });
|
|
3660
3702
|
return sendScreen(response, 200, screen("Storage", `<p><a href="/settings">Settings</a></p><h1>Storage</h1>${storageHtml({ plan, csrf: who.session.csrf }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
|
|
3661
3703
|
}
|
|
3704
|
+
// Settings → Projects → Pull requests: one project's setup. Off, the checks run on open, so the page says
|
|
3705
|
+
// exactly what turning on would do, or what to fix first.
|
|
3706
|
+
if (url.pathname === "/settings/pull-requests") {
|
|
3707
|
+
const repo = url.searchParams.get("repo") ?? "";
|
|
3708
|
+
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
3709
|
+
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
3710
|
+
return refuse(response, who, 404, "No such project.", "/projects");
|
|
3711
|
+
const publishing = publishingOf(store, repo);
|
|
3712
|
+
const canChange = who.role === "approver" && !store.isDemo();
|
|
3713
|
+
const check = publishing.on || !canChange ? null : await checkPublishing(repo, options.publishExec === undefined ? {} : { exec: options.publishExec });
|
|
3714
|
+
const html = pullRequestSettingsHtml({ repo, name: projectName(repo), csrf: who.session.csrf, canChange, publishing, check,
|
|
3715
|
+
said: url.searchParams.get("said"), problem: url.searchParams.get("problem") });
|
|
3716
|
+
return sendScreen(response, 200, screen("Pull requests", `<p><a href="/projects">Projects</a></p><h1>Pull requests</h1>${html}`, { chrome: chromeFor(project, "projects") }));
|
|
3717
|
+
}
|
|
3662
3718
|
// Sprint 8: Settings → Backups, how the last backup went and the schedule. An instance operator's page.
|
|
3663
3719
|
if (url.pathname === "/settings/backups") {
|
|
3664
3720
|
const databaseFile = store.databaseFile();
|
|
@@ -3833,6 +3889,9 @@ export function createDecisionServer(options) {
|
|
|
3833
3889
|
// The installation fact: when the first Ready result arrived, measured from the first account.
|
|
3834
3890
|
const at = store.firstSuccessAt(now), since = store.installationStartedAt();
|
|
3835
3891
|
return at === null || since === null ? null : firstResultWords(since, at);
|
|
3892
|
+
})(), (() => {
|
|
3893
|
+
const chosen = store.notificationPreference(who.name);
|
|
3894
|
+
return { mode: chosen.mode, digestAt: chosen.digestAt };
|
|
3836
3895
|
})()));
|
|
3837
3896
|
}
|
|
3838
3897
|
if (url.pathname === "/push/key") {
|
|
@@ -4848,7 +4907,7 @@ export function createDecisionServer(options) {
|
|
|
4848
4907
|
.map(run => `<a class="item${run.id === currentId ? " current" : ""}" href="/r/${run.id}">` +
|
|
4849
4908
|
`<span class="t">#${run.id} \u00b7 ${escape(run.taskId)}</span>` +
|
|
4850
4909
|
`<span class="m">${runOutcomeBadge(run, live.has(run.id))}` +
|
|
4851
|
-
`<span class="mono">${
|
|
4910
|
+
`<span class="mono">${whenTime(run.startedAt)}</span></span></a>`)
|
|
4852
4911
|
.join("\n");
|
|
4853
4912
|
return `<h2>builds</h2>\n${items === "" ? `<p class="meta">none yet</p>` : items}`;
|
|
4854
4913
|
}
|
|
@@ -4903,7 +4962,7 @@ export function createDecisionServer(options) {
|
|
|
4903
4962
|
peeks[one.path] = peekOf(one.path);
|
|
4904
4963
|
for (const path of candidates)
|
|
4905
4964
|
peeks[path] = peekOf(path);
|
|
4906
|
-
return sendScreen(response, status, projectsPage(chromeFor(open, "projects"), recent, [...candidates], open, csrf, problem, !restricted() && unscopedMode, !restricted() && (ceiling.roots.length > 0 || unscopedMode), onboardState, peeks, returnTo));
|
|
4965
|
+
return sendScreen(response, status, projectsPage(chromeFor(open, "projects"), recent, [...candidates], open, csrf, problem, !restricted() && unscopedMode, !restricted() && (ceiling.roots.length > 0 || unscopedMode), onboardState, peeks, returnTo, who.via === "cookie" ? (path) => publishingOf(store, path).on : undefined));
|
|
4907
4966
|
}
|
|
4908
4967
|
/** The task screen, shared by the GET and by every refusal that re-renders it. */
|
|
4909
4968
|
/**
|
|
@@ -5361,6 +5420,41 @@ export function createDecisionServer(options) {
|
|
|
5361
5420
|
: completionReceiptView(store, latest, store.artifactsFor(latest.id), evidenceRoot, reviewFactsFor(latest.id)),
|
|
5362
5421
|
};
|
|
5363
5422
|
}
|
|
5423
|
+
/** The newest pull request any version of this task opened through Complete; else, for a result completed
|
|
5424
|
+
* without one, the offer to open it (an approver's browser session only). */
|
|
5425
|
+
/** Where "Complete and open a pull request" would open one for this result, or null when it isn't offered. */
|
|
5426
|
+
function pullRequestTargetOf(runId) {
|
|
5427
|
+
const run = store.getRun(runId);
|
|
5428
|
+
const publishing = publishingOf(store, run === null ? null : store.refById(run.taskRef)?.repo ?? null);
|
|
5429
|
+
if (!publishing.on || publishing.legacy || store.publicationForRun(runId) !== null || pullRequestBlocker(store, runId) !== null)
|
|
5430
|
+
return null;
|
|
5431
|
+
return `${publishing.githubRepo} into ${publishing.base}`;
|
|
5432
|
+
}
|
|
5433
|
+
function taskPullRequestOf(taskId, who) {
|
|
5434
|
+
const family = familyOf(taskId);
|
|
5435
|
+
const rootId = family?.root.id ?? taskId;
|
|
5436
|
+
const ids = family === null ? [taskId] : [...family.versions.map(one => one.id)].reverse();
|
|
5437
|
+
const repo = store.lookupRef(taskId)?.repo ?? null;
|
|
5438
|
+
const publishing = publishingOf(store, repo);
|
|
5439
|
+
const target = publishing.on ? publishing.base : null;
|
|
5440
|
+
for (const id of ids) {
|
|
5441
|
+
const ref = store.lookupRef(id);
|
|
5442
|
+
if (ref === null)
|
|
5443
|
+
continue;
|
|
5444
|
+
for (const run of store.runsFor(ref.id)) {
|
|
5445
|
+
const view = pullRequestViewOf(store, run.id);
|
|
5446
|
+
if (view !== null)
|
|
5447
|
+
return { taskId: rootId, view: { ...view, canMerge: view.canMerge && who.role === "approver" && who.via === "cookie" }, offer: null, target };
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
|
|
5451
|
+
return null;
|
|
5452
|
+
const assignment = assignmentOf(store, taskId, clock(), { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
|
|
5453
|
+
const receipt = assignment?.receipt ?? null;
|
|
5454
|
+
if (assignment?.state !== "complete" || receipt === null || store.publicationForRun(receipt.runId) !== null || pullRequestBlocker(store, receipt.runId) !== null)
|
|
5455
|
+
return null;
|
|
5456
|
+
return { taskId: rootId, view: null, offer: { taskId: receipt.taskId, runId: receipt.runId, digest: receipt.digest }, target };
|
|
5457
|
+
}
|
|
5364
5458
|
function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft) {
|
|
5365
5459
|
const admittedFamily = familyOf(taskId);
|
|
5366
5460
|
if (admittedFamily?.problem != null)
|
|
@@ -5376,6 +5470,7 @@ export function createDecisionServer(options) {
|
|
|
5376
5470
|
presentedData.scopeDraft = scopeDraft;
|
|
5377
5471
|
if (cancelDraft !== undefined)
|
|
5378
5472
|
presentedData.cancelDraft = cancelDraft;
|
|
5473
|
+
presentedData.pullRequest = taskPullRequestOf(taskId, who);
|
|
5379
5474
|
const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
5380
5475
|
const page = taskPage(paneProject === null && !unscopedMode
|
|
5381
5476
|
? chromeFor(paneProject, "tasks")
|
|
@@ -6196,7 +6291,12 @@ export function createDecisionServer(options) {
|
|
|
6196
6291
|
return redirect(response, `/flows?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
6197
6292
|
const template = FLOW_TEMPLATES.find(one => one.id === body.get("template")) ?? FLOW_TEMPLATES[0];
|
|
6198
6293
|
const name = (body.get("name") ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || template.label;
|
|
6199
|
-
|
|
6294
|
+
const made = store.createFlow({ repo, name, definitionJson: JSON.stringify(template.definition), by: who.name }, now);
|
|
6295
|
+
// A template that starts from something (Issues to PRs: labelled issues) comes with its trigger.
|
|
6296
|
+
const trigger = template.trigger === undefined ? null : addFlowTriggerTo(store, store.getFlow(made), template.trigger, who.name, now, options.configDir ?? null);
|
|
6297
|
+
if (trigger !== null && !trigger.ok)
|
|
6298
|
+
return redirect(response, `/flows?problem=${encodeURIComponent(`${name} was made, but its trigger wasn't: ${trigger.message} Add one on its Triggers panel.`)}`);
|
|
6299
|
+
return redirect(response, `/flows/${made}`);
|
|
6200
6300
|
}
|
|
6201
6301
|
const flow = store.getFlow(Number((flowPost ?? triggerPost)[1]));
|
|
6202
6302
|
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
@@ -6668,6 +6768,43 @@ export function createDecisionServer(options) {
|
|
|
6668
6768
|
const done = await discardCheckout(store, pool, body.get("path") ?? "", now, who.name);
|
|
6669
6769
|
return done.ok ? back("said", `Discarded, about ${bytesWords(done.bytes)}. Its branch stays.`) : back("problem", done.message);
|
|
6670
6770
|
}
|
|
6771
|
+
// Pull requests for one project: turn on (the checks run again and must match what the page showed), merge
|
|
6772
|
+
// settings, or turn off. Each takes the password and lands in the ledger.
|
|
6773
|
+
if (url.pathname === "/settings/pull-requests") {
|
|
6774
|
+
const repo = body.get("repo") ?? "";
|
|
6775
|
+
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
6776
|
+
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
6777
|
+
return refuse(response, who, 404, "No such project.", "/projects");
|
|
6778
|
+
if (who.role !== "approver" || store.isDemo())
|
|
6779
|
+
return refuse(response, who, 403, "An approver sets up pull requests.", "/projects");
|
|
6780
|
+
const back = (key, words) => redirect(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
6781
|
+
const act = body.get("act");
|
|
6782
|
+
if (!authenticateApprover(store, who.name, body.get("password") ?? "", repo).ok)
|
|
6783
|
+
return back("problem", "Enter your Toolroll password to change pull requests.");
|
|
6784
|
+
if (act === "on") {
|
|
6785
|
+
const checked = await checkPublishing(repo, options.publishExec === undefined ? {} : { exec: options.publishExec });
|
|
6786
|
+
if (!checked.ok)
|
|
6787
|
+
return back("problem", checked.message);
|
|
6788
|
+
if (checked.plan.githubRepo !== body.get("github") || checked.plan.base !== body.get("base"))
|
|
6789
|
+
return back("problem", "GitHub answered differently from what you saw. Check the details again.");
|
|
6790
|
+
savePublishing(store, checked.plan, who.name, {}, now);
|
|
6791
|
+
return back("said", "Pull requests are on.");
|
|
6792
|
+
}
|
|
6793
|
+
if (act === "settings") {
|
|
6794
|
+
const method = body.get("method") ?? "";
|
|
6795
|
+
if (!MERGE_METHODS.includes(method))
|
|
6796
|
+
return back("problem", "Choose a merge method.");
|
|
6797
|
+
const saved = saveMergeSettings(store, repo, { mergeMethod: method, mergeWhenGreen: body.get("when-green") === "1" }, who.name, now);
|
|
6798
|
+
return saved.ok ? back("said", "Saved.") : back("problem", saved.message);
|
|
6799
|
+
}
|
|
6800
|
+
if (act === "off") {
|
|
6801
|
+
const revoked = store.revokePublicationGrant(repo, who.name, now);
|
|
6802
|
+
if (revoked)
|
|
6803
|
+
store.recordAction({ at: now.toISOString(), actor: who.name, repo, taskId: null, runId: null, action: "pull requests turned off", outcome: "off", source: "policy" });
|
|
6804
|
+
return back("said", revoked ? "Pull requests are off." : "Pull requests were already off.");
|
|
6805
|
+
}
|
|
6806
|
+
return back("problem", "That change isn't available.");
|
|
6807
|
+
}
|
|
6671
6808
|
// Sprint 8: back up now. An instance operator; the outcome is kept like a scheduled one's.
|
|
6672
6809
|
if (url.pathname === "/settings/backups/now") {
|
|
6673
6810
|
const databaseFile = store.databaseFile();
|
|
@@ -6716,6 +6853,33 @@ export function createDecisionServer(options) {
|
|
|
6716
6853
|
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
6717
6854
|
return void response.end(zip);
|
|
6718
6855
|
}
|
|
6856
|
+
// Settings → Flows → Switch on: a starter flow's zones and trigger, made together.
|
|
6857
|
+
if (url.pathname === "/settings/flows/on") {
|
|
6858
|
+
const repo = body.get("repo") ?? "";
|
|
6859
|
+
const back = (key, words) => redirect(response, `/settings/flows?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
6860
|
+
const known = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])];
|
|
6861
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo())
|
|
6862
|
+
return refuse(response, who, 403, "An approver switches starter flows on.", "/settings/flows");
|
|
6863
|
+
if (!known.includes(repo) || !visible(repo))
|
|
6864
|
+
return refuse(response, who, 404, "No such project.", "/settings/flows");
|
|
6865
|
+
const starter = starterOf(body.get("starter") ?? "");
|
|
6866
|
+
if (starter === null)
|
|
6867
|
+
return back("problem", "Choose a starter flow.");
|
|
6868
|
+
const switched = switchOnStarter(store, starter, repo, who.name, clock(), options.configDir ?? null);
|
|
6869
|
+
return switched.ok ? back("said", switched.said) : back("problem", switched.said);
|
|
6870
|
+
}
|
|
6871
|
+
// Settings → Integrations → Send test: one harmless read-only check of one integration, now.
|
|
6872
|
+
if (url.pathname === "/settings/integrations/test") {
|
|
6873
|
+
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
6874
|
+
return refuse(response, who, 403, "An installation approver tests integrations.", "/settings");
|
|
6875
|
+
const key = body.get("key") ?? "";
|
|
6876
|
+
const back = (name, words) => redirect(response, `/settings/integrations?${name}=${encodeURIComponent(words)}`);
|
|
6877
|
+
const checked = (await integrations.check([key])).find(one => one.key === key);
|
|
6878
|
+
if (checked === undefined)
|
|
6879
|
+
return back("problem", "That integration isn't here any more.");
|
|
6880
|
+
return checked.state === "connected" ? back("said", `${checked.name} works${checked.account === null ? "" : ` (${checked.account})`}.`)
|
|
6881
|
+
: back("problem", `${checked.name}: ${checked.action.kind === "fix" ? checked.action.words : "not set up."}`);
|
|
6882
|
+
}
|
|
6719
6883
|
// v104: change where monitoring goes. A step-up; the ledger keeps before → after (addresses, never secrets).
|
|
6720
6884
|
if (url.pathname === "/settings/monitoring") {
|
|
6721
6885
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
@@ -7275,6 +7439,20 @@ export function createDecisionServer(options) {
|
|
|
7275
7439
|
? "New tasks now use Strict / release. Approved tasks keep their setting."
|
|
7276
7440
|
: "New tasks now use Default quality. Approved tasks keep their setting.")}`);
|
|
7277
7441
|
}
|
|
7442
|
+
if (url.pathname === "/settings/notifications") {
|
|
7443
|
+
// Each person's own choice: a closed list of modes and a 24-hour HH:MM, or off.
|
|
7444
|
+
const mode = body.get("mode"), time = (body.get("digest") ?? "").trim();
|
|
7445
|
+
if (mode !== null && mode !== "quiet" && mode !== "all")
|
|
7446
|
+
return refuse(response, who, 400, "choose Only when I'm needed or Every step", "/settings");
|
|
7447
|
+
if (time !== "" && time !== "off" && !isDigestTime(time))
|
|
7448
|
+
return refuse(response, who, 400, "the evening digest time is HH:MM, or off", "/settings");
|
|
7449
|
+
const before = store.notificationPreference(who.name);
|
|
7450
|
+
const after = store.setNotificationPreference(who.name, { ...(mode === null ? {} : { mode }), ...(time === "" ? {} : { digestAt: time === "off" ? null : time }) }, who.name, now);
|
|
7451
|
+
const said = after.mode !== before.mode
|
|
7452
|
+
? after.mode === "quiet" ? "Chats now message you only when you're needed." : "Chats now message you at every step."
|
|
7453
|
+
: after.digestAt === null ? "Evening digest off." : `Evening digest at ${after.digestAt}.`;
|
|
7454
|
+
return redirect(response, `/settings?said=${encodeURIComponent(said)}`);
|
|
7455
|
+
}
|
|
7278
7456
|
if (url.pathname === "/settings/telegram-digest" && options.telegramTokenFile !== undefined) {
|
|
7279
7457
|
// The cadence is a closed list of minutes — never a free number from
|
|
7280
7458
|
// a form; "off" clears it. Any approver session may set it.
|
|
@@ -8006,7 +8184,28 @@ export function createDecisionServer(options) {
|
|
|
8006
8184
|
if (attendAct !== null) {
|
|
8007
8185
|
return attendMutation(response, who, attendAct.taskId, attendAct.verb, body, now);
|
|
8008
8186
|
}
|
|
8009
|
-
const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|stop|resume-arm|resume)$");
|
|
8187
|
+
const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|stop|resume-arm|resume)$");
|
|
8188
|
+
if (act !== null && act.verb === "merge") {
|
|
8189
|
+
// Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
|
|
8190
|
+
// result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
|
|
8191
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
8192
|
+
return refuse(response, who, 403, "Only an approver can merge.");
|
|
8193
|
+
if (store.isDemo())
|
|
8194
|
+
return taskScreen(response, who, act.taskId, "The demo merges nothing.", 403);
|
|
8195
|
+
const ref = store.lookupRef(act.taskId);
|
|
8196
|
+
if (ref === null || !visible(ref.repo))
|
|
8197
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
8198
|
+
const named = (body.get("run") ?? "").trim();
|
|
8199
|
+
const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
|
|
8200
|
+
const runTask = run === null ? null : store.externalIdFor(run.taskRef);
|
|
8201
|
+
if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
|
|
8202
|
+
return taskScreen(response, who, act.taskId, "That pull request isn't part of this task.", 409);
|
|
8203
|
+
}
|
|
8204
|
+
const merged = await mergeAsPerson(store, { runId: run.id, name: who.name, password: body.get("token") ?? "", ...(options.publishExec === undefined ? {} : { exec: options.publishExec }), clock });
|
|
8205
|
+
if (!merged.ok)
|
|
8206
|
+
return taskScreen(response, who, act.taskId, merged.message, merged.reason === "password" ? 403 : 409);
|
|
8207
|
+
return redirect(response, `${taskHref(act.taskId)}#merge`);
|
|
8208
|
+
}
|
|
8010
8209
|
if (act !== null) {
|
|
8011
8210
|
return taskMutation(response, who, act.taskId, act.verb, body, now);
|
|
8012
8211
|
}
|
|
@@ -9232,7 +9431,7 @@ export function createDecisionServer(options) {
|
|
|
9232
9431
|
`<p class="meta">worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
|
|
9233
9432
|
`<p class="meta">spending</p><p class="recap" style="margin-top:0">up to about $${(terms.budgetMicrousd / 1_000_000).toFixed(2)} — the agent stops as soon as its total crosses this; the final step may run a little past it</p>` +
|
|
9234
9433
|
`<p class="meta">conversation</p><p class="recap" style="margin-top:0">at most ${terms.maxSessionTurns} messages to the agent, this whole session; each may work up to ${Math.round(terms.turnTimeoutSeconds / 60)} minutes — one that runs past that ends the whole session. If it needs a repair, the repair uses the same session, model, and clock.</p>` +
|
|
9235
|
-
`<p class="meta">while you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${
|
|
9434
|
+
`<p class="meta">while you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${whenTime(terms.absoluteExpiry)} regardless. One attempt; it never converts into unattended work.</p>` +
|
|
9236
9435
|
(quick
|
|
9237
9436
|
? `<p class="meta">your signed mode covers this mint — no password; the mode is re-proved as you confirm, and the session is stamped with its signature</p>`
|
|
9238
9437
|
: `<label>your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
|
|
@@ -10043,6 +10242,14 @@ export function createDecisionServer(options) {
|
|
|
10043
10242
|
if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
|
|
10044
10243
|
return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
|
|
10045
10244
|
}
|
|
10245
|
+
if (body.get("publish") === "1") {
|
|
10246
|
+
// "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
|
|
10247
|
+
// exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
|
|
10248
|
+
const opened = completeAndOpenPullRequest(store, { taskId, digest, runId: Number(namedRun), who: principal, root: evidenceRoot }, now);
|
|
10249
|
+
if (!opened.ok)
|
|
10250
|
+
return taskScreen(response, who, taskId, opened.message, 409);
|
|
10251
|
+
return redirect(response, `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge`);
|
|
10252
|
+
}
|
|
10046
10253
|
const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
|
|
10047
10254
|
if (!completed.ok)
|
|
10048
10255
|
return taskScreen(response, who, taskId, completed.message, 409);
|
|
@@ -10567,6 +10774,7 @@ export function createDecisionServer(options) {
|
|
|
10567
10774
|
proof,
|
|
10568
10775
|
terminal,
|
|
10569
10776
|
publication,
|
|
10777
|
+
pullRequestTo: pullRequestTargetOf(run.id),
|
|
10570
10778
|
ciFailing: publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber),
|
|
10571
10779
|
files,
|
|
10572
10780
|
outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
|
|
@@ -10806,6 +11014,10 @@ function consoleSpend(summary) {
|
|
|
10806
11014
|
function when(iso) {
|
|
10807
11015
|
return iso === null ? "" : iso.slice(0, 16).replace("T", " ");
|
|
10808
11016
|
}
|
|
11017
|
+
/** The same minute as a `<time>`: the full stamp on a desk, "16:39" / "Yesterday 16:39" / "Sep 28" on a phone. */
|
|
11018
|
+
function whenTime(iso) {
|
|
11019
|
+
return iso === null ? "" : whenHtml(iso, when(iso));
|
|
11020
|
+
}
|
|
10809
11021
|
/** Overdue is derived at render — display never writes. */
|
|
10810
11022
|
function isOverdue(decision, now) {
|
|
10811
11023
|
if (decision.state === "expired")
|
|
@@ -11293,7 +11505,7 @@ function agentsAvailabilityHtml(projection) {
|
|
|
11293
11505
|
const items = [...seen.values()].map(leg => {
|
|
11294
11506
|
const state = leg.readiness === "ready" ? "ready" : leg.readiness === "unavailable" ? "unavailable" : "not yet checked";
|
|
11295
11507
|
const detail = leg.readiness === "unavailable" && leg.readinessReason !== null ? ` (${escape(leg.readinessReason)})` : "";
|
|
11296
|
-
return `<li class="agents-availability-${escape(leg.readiness)}"><span class="mono">${escape(leg.provider)}</span> ${state}${detail}${leg.readinessRunner === null ? "" : ` <span class="meta">— ${escape(leg.readinessRunner)}${leg.observedAt === null ? "" : `, ${
|
|
11508
|
+
return `<li class="agents-availability-${escape(leg.readiness)}"><span class="mono">${escape(leg.provider)}</span> ${state}${detail}${leg.readinessRunner === null ? "" : ` <span class="meta">— ${escape(leg.readinessRunner)}${leg.observedAt === null ? "" : `, ${whenTime(leg.observedAt)}`}</span>`}</li>`;
|
|
11297
11509
|
});
|
|
11298
11510
|
return `<p class="meta agents-availability-label">Availability right now</p><ul class="agents-availability">${items.join("")}</ul>`;
|
|
11299
11511
|
}
|
|
@@ -11383,7 +11595,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
11383
11595
|
const overrides = view.overrides.length === 0
|
|
11384
11596
|
? ""
|
|
11385
11597
|
: `<ul class="agents-overrides">${view.overrides
|
|
11386
|
-
.map(one => `<li><span>${escape(ROLE_NOUN[one.phase])} → <span class="mono">${escape(one.provider)} · ${escape(one.model)}</span> <span class="meta">by ${escape(one.by)} ${
|
|
11598
|
+
.map(one => `<li><span>${escape(ROLE_NOUN[one.phase])} → <span class="mono">${escape(one.provider)} · ${escape(one.model)}</span> <span class="meta">by ${escape(one.by)} ${whenTime(one.at)}</span></span>` +
|
|
11387
11599
|
(canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">clear</button></form>` : "") +
|
|
11388
11600
|
`</li>`)
|
|
11389
11601
|
.join("")}</ul>`;
|
|
@@ -11556,6 +11768,9 @@ ${THEME_MAPPING}
|
|
|
11556
11768
|
--so-ease-out: cubic-bezier(.23, 1, .32, 1);
|
|
11557
11769
|
--font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
11558
11770
|
--font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
11771
|
+
/* Phone spacing steps (760px and narrower): side gutter, gap under the
|
|
11772
|
+
header, gap between page blocks, between rows, and inside a card. */
|
|
11773
|
+
--so-phone-gutter: 14px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
|
|
11559
11774
|
}
|
|
11560
11775
|
@media (prefers-color-scheme: dark) {
|
|
11561
11776
|
:root:not([data-theme="light"]) {
|
|
@@ -12946,6 +13161,23 @@ ${THEME_DARK}
|
|
|
12946
13161
|
.status-line[data-tone="ready"] .status-dot, .status-line[data-tone="done"] .status-dot { background: var(--success); }
|
|
12947
13162
|
.status-line[data-tone="muted"] { color: var(--muted-foreground); font-weight: 500; }
|
|
12948
13163
|
.status-line[data-tone="muted"] .status-dot { background: var(--border); }
|
|
13164
|
+
.pull-request { display: grid; gap: .35rem; }
|
|
13165
|
+
.pull-request p { margin: 0; overflow-wrap: anywhere; }
|
|
13166
|
+
.pull-request-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .25rem .75rem; }
|
|
13167
|
+
.pull-request-head a, .pull-request .meta a { display: inline-flex; align-items: center; min-height: 44px; }
|
|
13168
|
+
.pull-request-head strong { font-size: 1.0625rem; font-weight: 600; color: var(--foreground); display: inline-flex; align-items: center; gap: .5rem; }
|
|
13169
|
+
.pull-request-head strong::before { content: ""; width: .5rem; height: .5rem; border-radius: 999px; background: var(--muted-foreground); }
|
|
13170
|
+
.pull-request--ok .pull-request-head strong::before { background: var(--success, #15803d); }
|
|
13171
|
+
.pull-request--problem .pull-request-head strong::before { background: var(--destructive, #b91c1c); }
|
|
13172
|
+
.pull-request-act { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .75rem; margin: .25rem 0 0; }
|
|
13173
|
+
.pull-request-act .meta { flex: 1 1 100%; }
|
|
13174
|
+
.pull-request-act label { display: grid; gap: .25rem; flex: 1 1 12rem; margin: 0; }
|
|
13175
|
+
.pull-request-act label input { margin: 0; min-height: 44px; box-sizing: border-box; width: 100%; }
|
|
13176
|
+
.pull-request-act button { min-height: 44px; white-space: nowrap; }
|
|
13177
|
+
.result-complete-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
|
|
13178
|
+
.result-complete-actions button { white-space: nowrap; }
|
|
13179
|
+
.result-complete-actions button[name=publish], .pull-request-act button { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
|
|
13180
|
+
@media (max-width: 480px) { .pull-request-act button, .result-complete-actions button { flex: 1 1 100%; } }
|
|
12949
13181
|
.receipt-publication { position: relative; z-index: 1; margin: .35rem 0 0; font-size: .8125rem; color: var(--muted-foreground); }
|
|
12950
13182
|
/* A raw repository path in ordinary copy wraps at any point rather than
|
|
12951
13183
|
widening the page (the phone task-list overflow, package 1). */
|
|
@@ -14109,12 +14341,27 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
|
|
|
14109
14341
|
}
|
|
14110
14342
|
.sticky-actions button { margin: 0; }
|
|
14111
14343
|
}
|
|
14344
|
+
|
|
14345
|
+
/* A time a person reads (when-html.ts): the full stamp on a desk, the short
|
|
14346
|
+
one on a phone; the full stamp stays in the title. */
|
|
14347
|
+
.so-when-short { display: none; }
|
|
14348
|
+
@media (max-width: 760px) {
|
|
14349
|
+
.so-when-full { display: none; }
|
|
14350
|
+
.so-when-short { display: inline; }
|
|
14351
|
+
/* Server pages outside the workspace keep the same phone rhythm. */
|
|
14352
|
+
main { padding: var(--so-phone-gap) var(--so-phone-gutter) 3rem; }
|
|
14353
|
+
h2 { margin: 1.25rem 0 .375rem; }
|
|
14354
|
+
.card { padding: var(--so-phone-card); margin: var(--so-phone-row) 0; }
|
|
14355
|
+
.problem { padding: .5rem .625rem; margin: var(--so-phone-row) 0; }
|
|
14356
|
+
.meta, .hint { line-height: 1.35; }
|
|
14357
|
+
details { margin: var(--so-phone-row) 0; }
|
|
14358
|
+
}
|
|
14112
14359
|
`;
|
|
14113
14360
|
/** Appearance: a three-way segmented switch, one tap per choice. */
|
|
14114
14361
|
const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem;margin:0 0 2rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
|
|
14115
14362
|
/** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
|
|
14116
14363
|
export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
|
|
14117
|
-
const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + LEAD_CONTEXT_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
|
|
14364
|
+
const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
|
|
14118
14365
|
/**
|
|
14119
14366
|
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
14120
14367
|
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
@@ -14784,7 +15031,7 @@ function inboxPage(chrome, data) {
|
|
|
14784
15031
|
: `<div class="card builder-notice" data-builder-status="${data.worker.registered === 0 ? "not-connected" : "disconnected"}">` +
|
|
14785
15032
|
(data.worker.registered === 0
|
|
14786
15033
|
? `<strong>No builder is connected yet.</strong> Toolroll is open, but no machine is connected to do project work. On the machine where the project lives, open that folder and run <span class="mono">toolroll up</span>. Keep Toolroll running; approved work starts automatically.`
|
|
14787
|
-
: `<strong>Builder disconnected.</strong> ${data.worker.registered} builder${data.worker.registered === 1 ? " is" : "s are"} configured, last checked in ${data.worker.lastHeard === null ? "never" :
|
|
15034
|
+
: `<strong>Builder disconnected.</strong> ${data.worker.registered} builder${data.worker.registered === 1 ? " is" : "s are"} configured, last checked in ${data.worker.lastHeard === null ? "never" : whenTime(data.worker.lastHeard)}. Reopen Toolroll on that machine. Queued work starts automatically when a builder reconnects.`) +
|
|
14788
15035
|
`</div>`;
|
|
14789
15036
|
return screen("inbox", [
|
|
14790
15037
|
`<h1>Inbox</h1>`,
|
|
@@ -14852,8 +15099,8 @@ function systemPage(chrome, data) {
|
|
|
14852
15099
|
? ""
|
|
14853
15100
|
: `<div class="stat-card"><span class="k"><span class="dot ${data.episode.endedAt === null ? "dot-ok pulse" : "dot-off"}"></span>Toolroll</span>` +
|
|
14854
15101
|
`<span class="v">${data.episode.endedAt === null
|
|
14855
|
-
? `running since ${
|
|
14856
|
-
: `last run: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${
|
|
15102
|
+
? `running since ${whenTime(data.episode.startedAt)}`
|
|
15103
|
+
: `last run: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
|
|
14857
15104
|
const cards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
|
|
14858
15105
|
const PHASE_SAID = {
|
|
14859
15106
|
plan: "planning sessions ask questions and draft the plan you approve",
|
|
@@ -15123,7 +15370,7 @@ function donePage(chrome, rows, ciRed) {
|
|
|
15123
15370
|
return (`<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${escape(row.title)}</strong></a>` +
|
|
15124
15371
|
`${row.outcome === "no-change" ? ` <span class="badge">no change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
|
|
15125
15372
|
`${row.handoff === null ? "" : `<p class="meta">${escape(row.handoff.length > 200 ? row.handoff.slice(0, 200) + "\u2026" : row.handoff)}</p>`}` +
|
|
15126
|
-
`<p class="meta mono">${escape(row.taskId)} \u00b7 ${
|
|
15373
|
+
`<p class="meta mono">${escape(row.taskId)} \u00b7 ${whenTime(row.completedAt)}${row.ranMinutes === null ? "" : ` \u00b7 ran ${row.ranMinutes}m`}${row.provider === null ? "" : ` \u00b7 ${escape(runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null }))}`}` +
|
|
15127
15374
|
` \u00b7 <a href="${reviewHref(row.taskId)}">review \u2192</a></p></div>`);
|
|
15128
15375
|
})
|
|
15129
15376
|
.join("\n");
|
|
@@ -16560,7 +16807,7 @@ function routineScreenPage(chrome, data) {
|
|
|
16560
16807
|
`<p class="meta">${routine.paused ? "resuming fires again at the next due slot" : "pausing stops firing instantly; a running instance finishes"}${runNowForm === "" ? "" : " · run now spawns an extra instance without touching the schedule — spend outside the schedule takes your password again"}</p>` +
|
|
16561
16808
|
`</div>`;
|
|
16562
16809
|
const ledger = fires.length === 0
|
|
16563
|
-
? `<p class="meta">No firings yet${approved && routine.nextFireAt !== null ? ` — first at ${
|
|
16810
|
+
? `<p class="meta">No firings yet${approved && routine.nextFireAt !== null ? ` — first at ${whenTime(routine.nextFireAt)}` : ""}.</p>`
|
|
16564
16811
|
: fires
|
|
16565
16812
|
.map(fire => {
|
|
16566
16813
|
const said = fire.outcome === "fired"
|
|
@@ -16569,7 +16816,7 @@ function routineScreenPage(chrome, data) {
|
|
|
16569
16816
|
: `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(fire.instanceState)}</span>`}`
|
|
16570
16817
|
: `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
|
|
16571
16818
|
const slot = fire.scheduledFor.replace(/^manual:/, "");
|
|
16572
|
-
return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${
|
|
16819
|
+
return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
|
|
16573
16820
|
})
|
|
16574
16821
|
.join("\n");
|
|
16575
16822
|
return screen(`routine · ${routine.name}`, [
|
|
@@ -16581,7 +16828,7 @@ function routineScreenPage(chrome, data) {
|
|
|
16581
16828
|
? `<div class="problem">stopped behind <a href="${taskHref(data.blocker.taskId)}" class="mono">${escape(data.blocker.taskId)}</a> (${escape(data.blocker.state)}) — the track resumes when it finishes or is cancelled</div>`
|
|
16582
16829
|
: "",
|
|
16583
16830
|
approved && !routine.paused && routine.nextFireAt !== null
|
|
16584
|
-
? `<p class="meta">next fire ${
|
|
16831
|
+
? `<p class="meta">next fire ${whenTime(routine.nextFireAt)}</p>`
|
|
16585
16832
|
: "",
|
|
16586
16833
|
"<h2>the standing order</h2>",
|
|
16587
16834
|
terms,
|
|
@@ -16675,8 +16922,8 @@ function homePage(chrome, data) {
|
|
|
16675
16922
|
? ""
|
|
16676
16923
|
: `<div class="stat-card"><span class="k"><span class="dot ${data.episode.endedAt === null ? "dot-ok pulse" : "dot-off"}"></span>Toolroll</span>` +
|
|
16677
16924
|
`<span class="v">${data.episode.endedAt === null
|
|
16678
|
-
? `running since ${
|
|
16679
|
-
: `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${
|
|
16925
|
+
? `running since ${whenTime(data.episode.startedAt)}`
|
|
16926
|
+
: `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
|
|
16680
16927
|
const fleetCards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
|
|
16681
16928
|
const fleet = fleetCards.length === 0
|
|
16682
16929
|
? `<h2>system status</h2><p class="hint">No builder is connected yet. On the machine where the project lives, open that folder and run <code>toolroll up</code>.</p>`
|
|
@@ -17357,7 +17604,7 @@ function contestPage(chrome, data) {
|
|
|
17357
17604
|
`only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
|
|
17358
17605
|
data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
|
|
17359
17606
|
`<p class="row"><strong>${escape((CONTEST_STATE_WORDS[contest.state] ?? "the tournament is in an unexpected state — the records have the detail").replace(/tournament/g, contestNoun(contest.kind)))}</strong></p>`,
|
|
17360
|
-
contest.pickedBy === null ? "" : `<p class="meta">picked by ${escape(contest.pickedBy)} at ${
|
|
17607
|
+
contest.pickedBy === null ? "" : `<p class="meta">picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
|
|
17361
17608
|
contest.kind === "comparison"
|
|
17362
17609
|
? `<p class="row"><strong>spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
|
|
17363
17610
|
`${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
|
|
@@ -17554,7 +17801,7 @@ function githubReposPage(chrome, data) {
|
|
|
17554
17801
|
`<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">private</span>` : ""}`,
|
|
17555
17802
|
`<span class="right">${action}</span></div>`,
|
|
17556
17803
|
localPath === null
|
|
17557
|
-
? `<p class="meta">not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${
|
|
17804
|
+
? `<p class="meta">not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
|
|
17558
17805
|
: `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(localPath)}</p>`,
|
|
17559
17806
|
repo.description === "" ? "" : `<p class="meta">${escape(repo.description)}</p>`,
|
|
17560
17807
|
`</div>`,
|
|
@@ -17568,7 +17815,7 @@ function githubReposPage(chrome, data) {
|
|
|
17568
17815
|
`<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
|
|
17569
17816
|
].join("\n"), { chrome });
|
|
17570
17817
|
}
|
|
17571
|
-
function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/") {
|
|
17818
|
+
function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn) {
|
|
17572
17819
|
// The onboarding card (repo onboarding, findings 1-39): preview first,
|
|
17573
17820
|
// then a password-confirmed clone into a configured root. Disabled
|
|
17574
17821
|
// states explain themselves in words (finding 28/39).
|
|
@@ -17658,6 +17905,7 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
17658
17905
|
return {
|
|
17659
17906
|
name, path, shortPath: path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path, open: open !== null && open === path, openedAt,
|
|
17660
17907
|
knowledgeHref: `/settings/knowledge?repo=${encodeURIComponent(path)}`,
|
|
17908
|
+
...(pullRequestsOn === undefined ? {} : { pullRequests: { href: `/settings/pull-requests?repo=${encodeURIComponent(path)}`, on: pullRequestsOn(path) } }),
|
|
17661
17909
|
peek: peek === null ? null : [
|
|
17662
17910
|
...(peek.waiting > 0 ? [{ label: `${peek.waiting} waiting on you`, href: "/", tone: "attention" }] : []),
|
|
17663
17911
|
...(peek.running > 0 ? [{ label: `${peek.running} running`, href: "/runs", tone: "neutral" }] : []),
|
|
@@ -18254,7 +18502,7 @@ function planRevisionLedgerHtml(ledger, taskId, csrf) {
|
|
|
18254
18502
|
: `<details class="plan-revision-history"><summary>revision history (${ledger.history.length})</summary>` +
|
|
18255
18503
|
ledger.history
|
|
18256
18504
|
.map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(one.status)}</span> ` +
|
|
18257
|
-
`<span class="meta">${escape(one.author)} · ${
|
|
18505
|
+
`<span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> — ${escape(one.reason)}</p>`)
|
|
18258
18506
|
.join("\n") +
|
|
18259
18507
|
`</details>`;
|
|
18260
18508
|
return (`<div class="card plan-revision"><span class="eyebrow">plan updated</span><h2>Using plan revision ${current.revision}</h2>` +
|
|
@@ -18591,7 +18839,7 @@ function taskBodyParts(data) {
|
|
|
18591
18839
|
? `attached to build #${one.attachedRun} — delivery not yet proven`
|
|
18592
18840
|
: "waiting for the next attempt";
|
|
18593
18841
|
const steerRows = steering
|
|
18594
|
-
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${
|
|
18842
|
+
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)} · ${escape(steerState(one))}</span> ` +
|
|
18595
18843
|
`${escape(one.note)}</p>`)
|
|
18596
18844
|
.join("\n");
|
|
18597
18845
|
const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled" || (data.contest ?? null) !== null
|
|
@@ -18607,7 +18855,7 @@ function taskBodyParts(data) {
|
|
|
18607
18855
|
: `<h2>holds</h2>` +
|
|
18608
18856
|
data.holds
|
|
18609
18857
|
.map(hold => `<p class="row">${escape(holdOwnerWords(hold.ownerKind))} — ${escape(hold.reason)}` +
|
|
18610
|
-
`${hold.until === null ? "" : ` <span class="meta">until ${
|
|
18858
|
+
`${hold.until === null ? "" : ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`)
|
|
18611
18859
|
.join("\n") +
|
|
18612
18860
|
`<p class="meta">only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
|
|
18613
18861
|
const approval = approvalOf(scope);
|
|
@@ -18633,7 +18881,7 @@ function taskBodyParts(data) {
|
|
|
18633
18881
|
})(),
|
|
18634
18882
|
acceptanceCeremonyHtml(scope.acceptance),
|
|
18635
18883
|
approval.approved
|
|
18636
|
-
? `<p class="meta scope-seal">Approved by ${escape(approval.by)} · ${
|
|
18884
|
+
? `<p class="meta scope-seal">Approved by ${escape(approval.by)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
|
|
18637
18885
|
: `<p class="meta"><span class="seal">signs ${shortDigest(scope.digest)}</span> — approval binds to this exact wording</p>` +
|
|
18638
18886
|
`<p class="meta">Not approved${approval.reason === "changed" ? " — approved once, then rewritten" : ""}</p>`,
|
|
18639
18887
|
`</div>`,
|
|
@@ -18839,7 +19087,7 @@ function taskBodyParts(data) {
|
|
|
18839
19087
|
? [
|
|
18840
19088
|
`<div class="card" data-attended="${escape(attended.open.id)}">`,
|
|
18841
19089
|
`<p><strong>attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
|
|
18842
|
-
`<p class="meta">${attended.open.running ? "the agent is running — your open console keeps it live" : "waiting to dispatch to this machine"} · ${attended.open.turnsUsed}/${attended.open.cap} messages · $${(attended.open.spentMicrousd / 1_000_000).toFixed(2)} of $${(attended.open.budgetMicrousd / 1_000_000).toFixed(2)} · everything ends by ${
|
|
19090
|
+
`<p class="meta">${attended.open.running ? "the agent is running — your open console keeps it live" : "waiting to dispatch to this machine"} · ${attended.open.turnsUsed}/${attended.open.cap} messages · $${(attended.open.spentMicrousd / 1_000_000).toFixed(2)} of $${(attended.open.budgetMicrousd / 1_000_000).toFixed(2)} · everything ends by ${whenTime(attended.open.expiresAt)}</p>`,
|
|
18843
19091
|
`<p class="meta">this page being open keeps it alive — close it and the session winds down within a minute</p>`,
|
|
18844
19092
|
`<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
|
|
18845
19093
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
@@ -18992,7 +19240,7 @@ function taskBodyParts(data) {
|
|
|
18992
19240
|
runOutcomeBadge(run, run.id === liveHistoryRunId) +
|
|
18993
19241
|
`${run.reason === null ? "" : ` <span class="meta">${escape(reasonWords(run.reason))}</span>`}` +
|
|
18994
19242
|
` <span class="meta mono">${escape(bits.join(" · "))}</span>` +
|
|
18995
|
-
`<span class="right meta mono">${
|
|
19243
|
+
`<span class="right meta mono">${whenTime(run.startedAt)}</span></p>`);
|
|
18996
19244
|
})
|
|
18997
19245
|
.join("\n");
|
|
18998
19246
|
// ---- the right rail (slice 1c) ------------------------------------------
|
|
@@ -19047,6 +19295,9 @@ function taskBodyParts(data) {
|
|
|
19047
19295
|
return "no project — no publication grant can apply";
|
|
19048
19296
|
if (grant === null)
|
|
19049
19297
|
return "branch only — publishing is not set up";
|
|
19298
|
+
if (grant.publishOn === "complete") {
|
|
19299
|
+
return `pull requests to ${grant.githubRepo} into ${grant.base} when completed · ${grant.mergeWhenGreen === true ? "merges when checks pass" : "a person merges"} (${grant.mergeMethod ?? "squash"})`;
|
|
19300
|
+
}
|
|
19050
19301
|
return [
|
|
19051
19302
|
grant.capabilities.includes("push-branch") ? `may push ${grant.headPrefix}* to ${grant.githubRepo}` : "cannot push",
|
|
19052
19303
|
grant.capabilities.includes("open-pr") ? `may open a PR against ${grant.base}${grant.draft ? " (draft)" : ""}` : null,
|
|
@@ -19068,7 +19319,7 @@ function taskBodyParts(data) {
|
|
|
19068
19319
|
const scopeRow = scope === null
|
|
19069
19320
|
? prop("scope", "none yet")
|
|
19070
19321
|
: approval.approved
|
|
19071
|
-
? prop("approved scope", `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${escape(qualityModeTitle(scope.qualityMode ?? "default"))} · approved by ${escape(approval.by)} · ${
|
|
19322
|
+
? prop("approved scope", `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${escape(qualityModeTitle(scope.qualityMode ?? "default"))} · approved by ${escape(approval.by)} · ${whenTime(approval.at)}`)
|
|
19072
19323
|
: prop("scope", approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved");
|
|
19073
19324
|
const strikesRow = data.strikes > 0 ? prop("strikes", `${data.strikes} failed attempt(s)`) : "";
|
|
19074
19325
|
const propsCard = `<div class="card props">${workerRow}${queueRow}${scopeRow}${publishesRow}${economics}${strikesRow}</div>`;
|
|
@@ -19102,7 +19353,8 @@ function taskBodyParts(data) {
|
|
|
19102
19353
|
facts.push({ label: "Budget", parts: [`${data.budgetHold} · `, { label: "Spend", href: "/spend" }] });
|
|
19103
19354
|
if (data.policyHold != null)
|
|
19104
19355
|
facts.push({ label: "Policy", parts: [`${data.policyHold} · `, { label: "Policy", href: "/settings/policy" }] });
|
|
19105
|
-
|
|
19356
|
+
// The pull-request card already says this once, with its action.
|
|
19357
|
+
if (data.publication !== null && data.publication !== undefined && data.pullRequest?.view == null) {
|
|
19106
19358
|
const prHref = safePrUrl(data.publication.prUrl), pr = `PR #${data.publication.prNumber ?? "?"}`;
|
|
19107
19359
|
facts.push({ label: "Published", parts: [prHref === null ? pr : { label: pr, href: prHref },
|
|
19108
19360
|
` · ${data.publication.state}${data.publication.remoteState !== null ? ` · ${data.publication.remoteState.toLowerCase()} on GitHub` : ""}${data.publication.lastCheckState !== null ? ` · CI ${data.publication.lastCheckState} at last observation` : " · no checks observed"}`] });
|
|
@@ -19283,7 +19535,9 @@ function taskBodyParts(data) {
|
|
|
19283
19535
|
: data.filedVia === null || data.filedVia === undefined
|
|
19284
19536
|
? ""
|
|
19285
19537
|
: ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">scout</span>` : ""}</p>`;
|
|
19286
|
-
|
|
19538
|
+
// A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
|
|
19539
|
+
const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
|
|
19540
|
+
const assignmentOptions = data.assignment == null ? null : { workStatus: status, hideAction: (approveForm !== "" && data.dispatch?.action === "approve-scope") || pullRequestActs, problem: status.tone === "problem", diagnostics: [...(status.diagnostics ?? []), ...(status.tone === "problem" && status.detail !== data.assignment.detail ? [status] : [])] };
|
|
19287
19541
|
// The result takes over from the task status as soon as it is ready.
|
|
19288
19542
|
const statusHtml = (data.assignment != null && assignmentOptions !== null ? assignmentSummaryHtml(data.assignment, assignmentOptions) : receiptLeads ? completionReceiptCard(data.completion.receipt, task.id, "task", status) : taskStatusCard(status, task.id, data.dispatch ?? null, liveRunId, approveForm !== "" && data.dispatch?.action === "approve-scope")) + checkProgressHtml(data.checkProgress ?? null);
|
|
19289
19543
|
// The exact-run control (v52), directly under the scheduler's answer:
|
|
@@ -19413,6 +19667,7 @@ function taskBodyParts(data) {
|
|
|
19413
19667
|
approval: approvalHtml,
|
|
19414
19668
|
lead: [
|
|
19415
19669
|
{ key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
|
|
19670
|
+
{ key: "pull-request", html: data.pullRequest == null ? "" : pullRequestCardHtml(data.pullRequest, data.csrf) },
|
|
19416
19671
|
{ key: "needs-scope", html: needsScopeCard }, { key: "progress", html: progressCard }, { key: "revisions", html: revisionLedgerCard },
|
|
19417
19672
|
{ key: "plan", html: planCard }, { key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
|
|
19418
19673
|
].filter(one => one.html !== ""),
|
|
@@ -19428,6 +19683,11 @@ function taskBodyParts(data) {
|
|
|
19428
19683
|
{ id: "task-diagnostics", title: "Task options", html: optionsHtml, open: optionsOpen, count: null },
|
|
19429
19684
|
],
|
|
19430
19685
|
cancel: cancelForm === "" ? null : { html: cancelForm, open: data.cancelDraft !== undefined },
|
|
19686
|
+
// "Do this every time…": the starter flow that does this kind of work on its own, one yes away.
|
|
19687
|
+
everyTime: data.repo === null || data.csrf === "" ? null : (() => {
|
|
19688
|
+
const starter = starterForWork(`${task.title}\n${scope?.goal ?? ""}`);
|
|
19689
|
+
return { href: `/settings/flows?repo=${encodeURIComponent(data.repo)}&starter=${starter.id}#starter-${starter.id}`, starter: starter.name };
|
|
19690
|
+
})(),
|
|
19431
19691
|
};
|
|
19432
19692
|
return { html, view };
|
|
19433
19693
|
}
|
|
@@ -19770,7 +20030,7 @@ function reviewCockpitPage(chrome, data) {
|
|
|
19770
20030
|
: `<span class="cockpit-why">${statusLineHtml(status)}</span>`;
|
|
19771
20031
|
return (`<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${escape(reviewHref(row.taskId, row.runId, row.repo))}"${current ? ` aria-current="page"` : ""}>` +
|
|
19772
20032
|
`<span class="cockpit-row-head"><strong>${escape(row.title)}</strong></span>` +
|
|
19773
|
-
`<span class="cockpit-row-meta">${
|
|
20033
|
+
`<span class="cockpit-row-meta">${whenTime(row.completedAt)}${row.runId === null ? " · no build" : row.outcome === "no-change" ? " · no change" : ""}${row.prNumber === null ? "" : ` · PR #${row.prNumber}`}</span>` +
|
|
19774
20034
|
(row.historyProblem ? `<span class="cockpit-why" data-history-problem>History unavailable</span>` : why) +
|
|
19775
20035
|
(status !== null && row.ciFailing ? `<span class="cockpit-why">CI is failing</span>` : "") +
|
|
19776
20036
|
`</a></li>`);
|
|
@@ -19858,7 +20118,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
19858
20118
|
? "the scope changed after approval — the words below are the current text, not the signed one"
|
|
19859
20119
|
: "never approved — the result was built without a signed scope";
|
|
19860
20120
|
const approval = intent.approval.approved
|
|
19861
|
-
? `approved by ${escape(intent.approvedBy ?? "an operator")} · ${
|
|
20121
|
+
? `approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
|
|
19862
20122
|
: approvalWords;
|
|
19863
20123
|
const body = `<p class="recap" style="margin-top:.25rem"><strong>goal</strong> ${escape(intent.goal)}</p>` +
|
|
19864
20124
|
(intent.outOfScope === null ? `<p class="meta">no boundary was stated</p>` : `<p class="recap"><strong>not this</strong> ${escape(intent.outOfScope)}</p>`) +
|
|
@@ -19917,10 +20177,10 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
19917
20177
|
const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
|
|
19918
20178
|
? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
|
|
19919
20179
|
if (complete !== null)
|
|
19920
|
-
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf));
|
|
20180
|
+
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
|
|
19921
20181
|
if (view.notes.length > 0) {
|
|
19922
20182
|
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
|
|
19923
|
-
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${
|
|
20183
|
+
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
|
|
19924
20184
|
`</section>`);
|
|
19925
20185
|
}
|
|
19926
20186
|
return {
|
|
@@ -19931,8 +20191,41 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
19931
20191
|
},
|
|
19932
20192
|
};
|
|
19933
20193
|
}
|
|
19934
|
-
function completionForm(taskId, runId, digest, csrf) {
|
|
19935
|
-
|
|
20194
|
+
function completionForm(taskId, runId, digest, csrf, pullRequestTo = null) {
|
|
20195
|
+
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="receipt" value="${escape(digest)}"><input type="hidden" name="run" value="${runId}">`;
|
|
20196
|
+
if (pullRequestTo !== null) {
|
|
20197
|
+
// With pull requests set up: the PR is the primary road, a bare completion the quiet one beside it.
|
|
20198
|
+
return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">After inspecting the work, mark this result complete. A pull request opens on ${escape(pullRequestTo)} from this exact commit.</p><div class="result-complete-actions"><button type="submit" name="publish" value="1" style="min-height:44px">Complete and open a pull request</button><button type="submit" class="secondary" style="min-height:44px">Complete only</button></div></form>`;
|
|
20199
|
+
}
|
|
20200
|
+
return `<form method="post" action="${taskHref(taskId)}/complete" class="card result-complete">${hidden}<p class="meta">After inspecting the work, mark this result complete. Checks stay unchanged; nothing is published or deployed.</p><button type="submit" style="min-height:44px">Mark complete</button></form>`;
|
|
20201
|
+
}
|
|
20202
|
+
/** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
|
|
20203
|
+
* behind the password when checks passed, or Open a pull request for a result completed without one. */
|
|
20204
|
+
function pullRequestCardHtml(pr, csrf) {
|
|
20205
|
+
if (pr.view === null) {
|
|
20206
|
+
if (pr.offer === null || csrf === "")
|
|
20207
|
+
return "";
|
|
20208
|
+
return `<section class="card pull-request" id="merge" data-pull-request="none"><div class="pull-request-head"><strong>No pull request</strong></div>` +
|
|
20209
|
+
`<p class="meta">This result was completed without one.</p>` +
|
|
20210
|
+
`<form method="post" action="${taskHref(pr.offer.taskId)}/complete" class="pull-request-act"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="receipt" value="${escape(pr.offer.digest)}"><input type="hidden" name="run" value="${pr.offer.runId}"><input type="hidden" name="publish" value="1">` +
|
|
20211
|
+
`<button type="submit" class="secondary">Open a pull request</button></form></section>`;
|
|
20212
|
+
}
|
|
20213
|
+
const view = pr.view;
|
|
20214
|
+
const url = safePrUrl(view.prUrl);
|
|
20215
|
+
const link = view.prNumber === null ? "" : url === null ? `<span class="mono">PR #${view.prNumber}</span>` : `<a href="${escape(url)}" class="mono" rel="noreferrer" target="_blank">PR #${view.prNumber}</a>`;
|
|
20216
|
+
const tone = view.state === "ready" || view.state === "merged" ? "ok" : view.state === "failing" || view.state === "failed" || view.state === "closed" ? "problem" : "waiting";
|
|
20217
|
+
const revision = view.revisionTask === null || view.state !== "failing" ? "" : ` <a href="${taskHref(view.revisionTask)}">Open revision</a>`;
|
|
20218
|
+
const merged = view.mergeCommit === null ? "" : `<p class="meta">Merge commit <span class="mono">${escape(view.mergeCommit.slice(0, 12))}</span></p>`;
|
|
20219
|
+
const target = pr.target === null ? "the base branch" : pr.target;
|
|
20220
|
+
const act = !view.canMerge || csrf === "" || view.prNumber === null ? "" :
|
|
20221
|
+
`<form method="post" action="${taskHref(pr.taskId)}/merge" class="pull-request-act" data-merge-form>` +
|
|
20222
|
+
`<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="run" value="${view.runId}">` +
|
|
20223
|
+
`<p class="meta">${escape(view.mergeMethod === "squash" ? "Squash-merges" : view.mergeMethod === "rebase" ? "Rebase-merges" : "Merges")} PR #${view.prNumber} into ${escape(target)} and deletes its branch.</p>` +
|
|
20224
|
+
`<label>Password<input type="password" name="token" autocomplete="current-password" required></label>` +
|
|
20225
|
+
`<button type="submit">Merge</button></form>`;
|
|
20226
|
+
return `<section class="card pull-request pull-request--${tone}" id="merge" data-pull-request="${escape(view.state)}">` +
|
|
20227
|
+
`<div class="pull-request-head"><strong>${escape(view.label)}</strong>${link}</div>` +
|
|
20228
|
+
`<p class="meta">${escape(view.detail)}${revision}</p>${merged}${act}</section>`;
|
|
19936
20229
|
}
|
|
19937
20230
|
/** Exactly one primary road per result, chosen from its state; the
|
|
19938
20231
|
* others stay reachable from their own sections. Every form posts to the
|
|
@@ -20083,7 +20376,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
20083
20376
|
needsVerification +
|
|
20084
20377
|
criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []) +
|
|
20085
20378
|
`${run.provider === "claude" ? "" : ` <span class="meta mono">${escape(run.provider)}</span>`}` +
|
|
20086
|
-
`<span class="right meta mono">${
|
|
20379
|
+
`<span class="right meta mono">${whenTime(run.startedAt)}` +
|
|
20087
20380
|
`${run.providerStartedAt === null && run.tokensIn === null && run.tokensOut === null && run.costUsd === null ? "" : ` \u00b7 ${escape(runCostWords(run, liveIds.has(run.id)))}`}</span></p>`);
|
|
20088
20381
|
})
|
|
20089
20382
|
.join("\n");
|
|
@@ -20580,7 +20873,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
20580
20873
|
parts.push(`<p class="row" data-proof-verdict="${escape(dispatchStatusToken(view.verdict))}">${statusLineHtml(status)} <span class="meta">${escape(verificationExplanation(view.verdict, view.reasons))}</span></p>`);
|
|
20581
20874
|
}
|
|
20582
20875
|
if (view.accepted !== null) {
|
|
20583
|
-
parts.push(`<p class="meta">Accepted with an exception by <span class="mono">${escape(view.accepted.by)}</span> · ${
|
|
20876
|
+
parts.push(`<p class="meta">Accepted with an exception by <span class="mono">${escape(view.accepted.by)}</span> · ${whenTime(view.accepted.at)}${view.accepted.note === null ? "" : ` — ${escape(view.accepted.note)}`}</p>`);
|
|
20584
20877
|
}
|
|
20585
20878
|
parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks }));
|
|
20586
20879
|
parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
|
|
@@ -20856,7 +21149,7 @@ function resultPanelParts(detail, o) {
|
|
|
20856
21149
|
(publication === null || publication.prNumber === null
|
|
20857
21150
|
? ""
|
|
20858
21151
|
: ` ${prUrl === null ? `<span class="mono">PR #${publication.prNumber}</span>` : `<a href="${escape(prUrl)}">PR #${publication.prNumber}</a>`}` +
|
|
20859
|
-
`<span class="meta" data-ci-observed="${escape(detail.ciFailing ? "failing" : (publication.lastCheckState ?? "none"))}"> · ${detail.ciFailing ? "CI failing at the last check" : publication.lastCheckState === "passing" ? `CI passing, observed ${
|
|
21152
|
+
`<span class="meta" data-ci-observed="${escape(detail.ciFailing ? "failing" : (publication.lastCheckState ?? "none"))}"> · ${detail.ciFailing ? "CI failing at the last check" : publication.lastCheckState === "passing" ? `CI passing, observed ${whenTime(publication.lastCheckAt)}` : publication.lastCheckState === "running" ? "CI still running at the last check" : "no CI checks found — verify on GitHub"}</span>`) +
|
|
20860
21153
|
`</p>`;
|
|
20861
21154
|
const agent = runAgentWords(detail.route);
|
|
20862
21155
|
const links = [];
|
|
@@ -20872,7 +21165,7 @@ function resultPanelParts(detail, o) {
|
|
|
20872
21165
|
summaryParts.push(`<details class="result-details" data-result-details><summary>Build details</summary>` +
|
|
20873
21166
|
(unavailableShots.length === 0 ? "" : `<ul class="result-unavailable meta" data-result-unavailable="${unavailableShots.length}">${unavailableShots.map(one => `<li>Screenshot unavailable — <span class="mono">${escape(one.path)}</span>: ${escape(one.problem ?? "")}</li>`).join("")}</ul>`) +
|
|
20874
21167
|
`<dl class="result-facts">` +
|
|
20875
|
-
`<div><dt>Build</dt><dd>#${runId} · ${escape(run.runner)}${agent === null ? ` · ${escape(run.provider)}` : ""}${run.finishedAt === null ? "" : ` · ${
|
|
21168
|
+
`<div><dt>Build</dt><dd>#${runId} · ${escape(run.runner)}${agent === null ? ` · ${escape(run.provider)}` : ""}${run.finishedAt === null ? "" : ` · ${whenTime(run.finishedAt)}`}</dd></div>` +
|
|
20876
21169
|
(agent === null ? "" : `<div><dt>Agent</dt><dd>${escape(agent)}</dd></div>`) +
|
|
20877
21170
|
`<div><dt>Commits</dt><dd>${facts.head === null
|
|
20878
21171
|
? facts.base === null ? "no commit recorded" : `<span class="mono">${escape(facts.base.slice(0, 12))}</span> <span class="meta">base · no head recorded</span>`
|
|
@@ -20951,7 +21244,7 @@ function resultPanelParts(detail, o) {
|
|
|
20951
21244
|
checkParts.push(`<p class="meta" data-proof-verdict="none">No verification result is available for this build.</p>`);
|
|
20952
21245
|
}
|
|
20953
21246
|
if (proof.accepted !== null)
|
|
20954
|
-
checkParts.push(`<p class="meta">${humanReview ? "Accepted after human review" : "Accepted with an exception"} by <span class="mono">${escape(proof.accepted.by)}</span> · ${
|
|
21247
|
+
checkParts.push(`<p class="meta">${humanReview ? "Accepted after human review" : "Accepted with an exception"} by <span class="mono">${escape(proof.accepted.by)}</span> · ${whenTime(proof.accepted.at)}${proof.accepted.note === null ? "" : ` — ${escape(proof.accepted.note)}`}</p>`);
|
|
20955
21248
|
if (proof.matrix.length === 0) {
|
|
20956
21249
|
checkParts.push(detail.signedCriteria > 0 ? `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : `<p class="meta">This scope signed no acceptance checks.</p>`);
|
|
20957
21250
|
}
|
|
@@ -21002,7 +21295,7 @@ function resultPanelParts(detail, o) {
|
|
|
21002
21295
|
};
|
|
21003
21296
|
if (detail.comments.length > 0) {
|
|
21004
21297
|
requestParts.push(`<p class="meta">Saved for later · ${detail.comments.length}</p><div class="diff-comments" data-result-notes="${detail.comments.length}">` +
|
|
21005
|
-
detail.comments.map(one => `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>${one.path === null ? "" : pathWords(one.path, one.line)}${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${
|
|
21298
|
+
detail.comments.map(one => `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>${one.path === null ? "" : pathWords(one.path, one.line)}${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span></div>`).join("") +
|
|
21006
21299
|
`</div>`);
|
|
21007
21300
|
if (o.csrf !== "" && !detail.canAnnotate) {
|
|
21008
21301
|
requestParts.push(`<form method="post" action="/r/${runId}/revise" class="card revision-from-comments"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="return" value="${escape(o.returnTo)}">${revisionSealFields(detail.comments, detail.sourceDigest)}` +
|
|
@@ -21011,7 +21304,7 @@ function resultPanelParts(detail, o) {
|
|
|
21011
21304
|
}
|
|
21012
21305
|
}
|
|
21013
21306
|
if ((detail.pastComments?.length ?? 0) > 0) {
|
|
21014
|
-
requestParts.push(`<details class="result-feedback-history"><summary>Earlier feedback</summary><div class="diff-comments" data-past-feedback>${detail.pastComments.map(one => `<div class="diff-comment"><p>${one.path === null ? "" : pathWords(one.path, one.line)}${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${
|
|
21307
|
+
requestParts.push(`<details class="result-feedback-history"><summary>Earlier feedback</summary><div class="diff-comments" data-past-feedback>${detail.pastComments.map(one => `<div class="diff-comment"><p>${one.path === null ? "" : pathWords(one.path, one.line)}${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span></div>`).join("")}</div></details>`);
|
|
21015
21308
|
}
|
|
21016
21309
|
for (const revision of detail.revisions) {
|
|
21017
21310
|
// The child's standing is the shared status projection's own words
|
|
@@ -21230,7 +21523,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
21230
21523
|
? "the brief"
|
|
21231
21524
|
: turn.sourceKind === "answer"
|
|
21232
21525
|
? "your answer"
|
|
21233
|
-
: "repair (machine)"} · ${
|
|
21526
|
+
: "repair (machine)"} · ${whenTime(turn.recordedAt)}${turnWords(turn)}</span> ${escape(oneLineOf(turn.text, 240))}</p>`)
|
|
21234
21527
|
.join("\n")) +
|
|
21235
21528
|
(heldTurns.open && csrf !== ""
|
|
21236
21529
|
? `<form method="post" action="/r/${run.id}/turn" class="row">` +
|
|
@@ -21305,7 +21598,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
21305
21598
|
const commentRows = comments
|
|
21306
21599
|
.map(one => `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>` +
|
|
21307
21600
|
`${one.path === null ? "" : commentPathWords(one.path, one.line)}` +
|
|
21308
|
-
`${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${
|
|
21601
|
+
`${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span></div>`)
|
|
21309
21602
|
.join("\n");
|
|
21310
21603
|
const commentForm = csrf === "" || !hasTerminalDiff
|
|
21311
21604
|
? ""
|
|
@@ -21349,7 +21642,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
21349
21642
|
const noteRows = notes.length === 0
|
|
21350
21643
|
? ""
|
|
21351
21644
|
: notes
|
|
21352
|
-
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${
|
|
21645
|
+
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ` +
|
|
21353
21646
|
`${escape(one.note)}</p>`)
|
|
21354
21647
|
.join("\n");
|
|
21355
21648
|
const noteForm = csrf === ""
|
|
@@ -21537,7 +21830,7 @@ function telegramDeliveryWords(store, bot) {
|
|
|
21537
21830
|
return `Telegram pushes new messages to ${state.url}${state.problem === null ? "." : `, but ${state.problem.charAt(0).toLowerCase()}${state.problem.slice(1)}.`}`;
|
|
21538
21831
|
return `Toolroll asks Telegram for new messages every few seconds${state?.problem ? ` (${state.problem})` : ""}.`;
|
|
21539
21832
|
}
|
|
21540
|
-
function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null) {
|
|
21833
|
+
function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null, push = null, providerKeys = null, digest = null, permissionDefault = null, qualityDefault = null, email = null, telegramDelivery = null, workers = null, updates = null, firstResult = null, chatNotices = null) {
|
|
21541
21834
|
const permissionCard = permissionDefault === null
|
|
21542
21835
|
? ""
|
|
21543
21836
|
: [
|
|
@@ -21550,7 +21843,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21550
21843
|
: `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">an approver can change this default</p></div>`,
|
|
21551
21844
|
permissionDefault.updatedAt === null
|
|
21552
21845
|
? ""
|
|
21553
|
-
: `<p class="meta settings-changed">Changed ${
|
|
21846
|
+
: `<p class="meta settings-changed">Changed ${whenTime(permissionDefault.updatedAt)}${permissionDefault.updatedBy === null ? "" : ` by ${escape(permissionDefault.updatedBy)}`}</p>`,
|
|
21554
21847
|
].join("\n");
|
|
21555
21848
|
const qualityCard = qualityDefault === null
|
|
21556
21849
|
? ""
|
|
@@ -21564,9 +21857,25 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21564
21857
|
: `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">an approver can change this default</p></div>`,
|
|
21565
21858
|
qualityDefault.updatedAt === null
|
|
21566
21859
|
? ""
|
|
21567
|
-
: `<p class="meta settings-changed">Changed ${
|
|
21860
|
+
: `<p class="meta settings-changed">Changed ${whenTime(qualityDefault.updatedAt)}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
|
|
21861
|
+
].join("\n");
|
|
21862
|
+
// Quiet chat: this person's own choice. The installation's Telegram cadence only bundles Every step updates.
|
|
21863
|
+
const chatCard = chatNotices === null || csrf === ""
|
|
21864
|
+
? ""
|
|
21865
|
+
: [
|
|
21866
|
+
"<h3>Chat messages</h3>",
|
|
21867
|
+
`<form method="post" action="/settings/notifications" class="card" data-autosave>`,
|
|
21868
|
+
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
21869
|
+
`<label><input type="radio" name="mode" value="quiet"${chatNotices.mode === "quiet" ? " checked" : ""}> Only when I'm needed <span class="meta">· one message per task, updated as it moves</span></label>`,
|
|
21870
|
+
`<label><input type="radio" name="mode" value="all"${chatNotices.mode === "all" ? " checked" : ""}> Every step <span class="meta">· a new message for each update</span></label>`,
|
|
21871
|
+
`<label>Evening digest<select name="digest">` +
|
|
21872
|
+
digestTimes(chatNotices.digestAt).map(([value, label]) => `<option value="${value}"${value === (chatNotices.digestAt ?? "off") ? " selected" : ""}>${label}</option>`).join("") +
|
|
21873
|
+
`</select></label>`,
|
|
21874
|
+
`<p class="meta">One message: what finished, what waits, what failed.</p>`,
|
|
21875
|
+
`<button type="submit">Save</button>`,
|
|
21876
|
+
`</form>`,
|
|
21568
21877
|
].join("\n");
|
|
21569
|
-
const digestCard = digest === null || csrf === ""
|
|
21878
|
+
const digestCard = digest === null || csrf === "" || chatNotices?.mode === "quiet"
|
|
21570
21879
|
? ""
|
|
21571
21880
|
: [
|
|
21572
21881
|
"<h3>Telegram digest</h3>",
|
|
@@ -21651,7 +21960,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21651
21960
|
: `<p class="meta">Alerts need a secure (https) address for this app.</p>`,
|
|
21652
21961
|
...push.devices
|
|
21653
21962
|
.filter(one => one.retiredAt === null || one.retiredReason === "gone")
|
|
21654
|
-
.map(one => `<p class="row">${escape(one.uaWords)} · since ${
|
|
21963
|
+
.map(one => `<p class="row">${escape(one.uaWords)} · since ${whenTime(one.createdAt)}` +
|
|
21655
21964
|
`${one.retiredAt !== null ? ` · <span class="meta">expired</span>` : one.consecutiveFailures >= 20 ? ` · <span class="meta">failing</span>` : ""}` +
|
|
21656
21965
|
(one.retiredAt === null
|
|
21657
21966
|
? ` <form method="post" action="/push/remove" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="id" value="${one.id}"><button type="submit">Remove</button></form>`
|
|
@@ -21729,7 +22038,8 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21729
22038
|
}),
|
|
21730
22039
|
services: messaging === null || messaging.configured.length === 0 ? null : { configured: messaging.configured, channel: messaging.channel, implicit: messaging.implicit },
|
|
21731
22040
|
push: push === null || csrf === "" ? null : { available: push.available, devices: push.devices.filter(one => one.retiredAt === null || one.retiredReason === "gone").map(one => ({ id: one.id, words: `${one.uaWords} · since ${when(one.createdAt)}`, state: one.retiredAt !== null ? "expired" : one.consecutiveFailures >= 20 ? "failing" : "ok", removable: one.retiredAt === null })) },
|
|
21732
|
-
|
|
22041
|
+
chat: chatNotices === null || csrf === "" ? null : chatNotices,
|
|
22042
|
+
digest: digest === null || csrf === "" || chatNotices?.mode === "quiet" ? null : { every: digest.everyMs === null ? "off" : String(Math.round(digest.everyMs / 60_000)), held: digest.everyMs === null ? null : `${digest.held} routine fact(s) held` },
|
|
21733
22043
|
telegram: { state: hasEnv ? "from the environment" : existing === null ? "not set" : "saved", current, delivery: telegramDelivery },
|
|
21734
22044
|
email,
|
|
21735
22045
|
workers,
|
|
@@ -21746,7 +22056,8 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21746
22056
|
workersCard(workers),
|
|
21747
22057
|
updatesCard(updates, csrf),
|
|
21748
22058
|
firstResult === null ? "" : `<p class="meta" data-first-result>${escape(firstResult)}</p>`,
|
|
21749
|
-
pushCard === "" && messagingCard === "" && digestCard === "" ? "" : `<h2>Notifications</h2>`,
|
|
22059
|
+
pushCard === "" && messagingCard === "" && digestCard === "" && chatCard === "" ? "" : `<h2>Notifications</h2>`,
|
|
22060
|
+
chatCard,
|
|
21750
22061
|
messagingCard,
|
|
21751
22062
|
pushCard,
|
|
21752
22063
|
digestCard,
|
|
@@ -21842,6 +22153,8 @@ function settingsTiles() {
|
|
|
21842
22153
|
return `<nav class="settings-tiles" aria-label="Settings sections">${tiles.map(([href, label, icon]) => `<a href="${href}">${strokeIcon(icon)}<span>${label}</span></a>`).join("")}</nav>`;
|
|
21843
22154
|
}
|
|
21844
22155
|
const SETTINGS_TILE_ICONS = [
|
|
22156
|
+
["/settings/flows", "Flows", `<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/><path d="M10 6.5h4a3 3 0 0 1 3 3V14"/>`],
|
|
22157
|
+
["/settings/integrations", "Integrations", `<path d="M9 2v6M15 2v6"/><path d="M6 8h12v4a6 6 0 0 1-12 0z"/><path d="M12 18v4"/>`],
|
|
21845
22158
|
["/settings/models", "Models", `<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"/>`],
|
|
21846
22159
|
["/settings/skills", "Skills", `<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"/>`],
|
|
21847
22160
|
["/settings/tools", "Tools", `<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"/>`],
|
|
@@ -21902,7 +22215,7 @@ function nextPage(chrome, data) {
|
|
|
21902
22215
|
if (item.kind === "decision") {
|
|
21903
22216
|
const { decision } = item;
|
|
21904
22217
|
card =
|
|
21905
|
-
`<h1>${escape(decision.taskId)} <span class="meta">asked ${
|
|
22218
|
+
`<h1>${escape(decision.taskId)} <span class="meta">asked ${whenTime(decision.createdAt)}</span></h1>` +
|
|
21906
22219
|
`<div class="recap">${escape(decision.recap)}</div>` +
|
|
21907
22220
|
`<div class="question">${escape(decision.question)}</div>` +
|
|
21908
22221
|
decisionOptionForms(decision, data.csrf, "next");
|