toolroll 0.8.2 → 0.9.0
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 +31 -0
- package/dist/agent-onboard.d.ts +3 -0
- package/dist/agent-onboard.js +3 -0
- package/dist/browser/workspace.js +21 -20
- package/dist/browser-workspace.d.ts +10 -0
- 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 +3 -0
- 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/integrations-ui.d.ts +13 -0
- package/dist/integrations-ui.js +65 -0
- package/dist/integrations.d.ts +113 -0
- package/dist/integrations.js +526 -0
- package/dist/operate.d.ts +5 -2
- package/dist/operate.js +151 -4
- 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 +140 -0
- package/dist/pull-request-flow.js +440 -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 +264 -16
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +61 -0
- package/dist/store.js +142 -3
- package/dist/surface.js +2 -0
- package/dist/telegram-progress.d.ts +1 -0
- package/dist/telegram-progress.js +1 -1
- package/dist/telegram.js +95 -15
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -56,6 +56,8 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
|
|
|
56
56
|
import { workIndexPage, workCountsByProject, WorkIndexCursorError } from "./work-index.js";
|
|
57
57
|
import { openWorkDecisionOf } from "./work-summary.js";
|
|
58
58
|
import { assignmentOf, checkAssignmentAsOperator } from './assignment.js';
|
|
59
|
+
import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
|
|
60
|
+
import { checkPublishing, completeAndOpenPullRequest, mergeAsPerson, pullRequestBlocker, pullRequestViewOf, publishingOf, saveMergeSettings, savePublishing, MERGE_METHODS } from './pull-request-flow.js';
|
|
59
61
|
import { leadBriefHtml, LEAD_CONTEXT_CSS } from './lead-context.js';
|
|
60
62
|
import { assignmentCatchUp } from './assignment-brief.js';
|
|
61
63
|
import { assignmentCardOf, assignmentStatusOf, assignmentSummaryHtml, assignmentWithEvidence, ASSIGNMENT_CSS } from './assignment-ui.js';
|
|
@@ -167,7 +169,8 @@ import { dirname } from "node:path";
|
|
|
167
169
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
168
170
|
import { loadOrCreateVapidKeys, validatePushEndpoint } from "./push.js";
|
|
169
171
|
import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, isLargeRepo } from "./onboard.js";
|
|
170
|
-
import { verifiedAuthor, LEAD_THREAD } from "./store.js";
|
|
172
|
+
import { verifiedAuthor, LEAD_THREAD, isDigestTime } from "./store.js";
|
|
173
|
+
import { digestTimes } from "./digest-times.js";
|
|
171
174
|
import { updateRepos, addRepos, removeRepos } from "./repos.js";
|
|
172
175
|
import { run as execRun } from "./exec.js";
|
|
173
176
|
import { findFirstTasks, firstResultWords, firstRunSteps, firstTaskSuggestions, SANDBOX_COMMAND } from "./first-run.js";
|
|
@@ -201,6 +204,8 @@ import { accessFromGroups, accountNameFor, discoverOidc, exchangeOidcCode, newOi
|
|
|
201
204
|
import { readSsoSettings, removeSsoSettings, saveSsoSettings, SSO_CALLBACK, ssoChangeWords } from "./sso-settings.js";
|
|
202
205
|
import { monitoringChange, readMonitoring, saveMonitoring } from "./monitoring-settings.js";
|
|
203
206
|
import { MONITORING_CSS, monitoringHtml, signingSecretHtml } from "./monitoring-ui.js";
|
|
207
|
+
import { INTEGRATIONS_CSS, integrationsHtml } from "./integrations-ui.js";
|
|
208
|
+
import { createIntegrationMonitor } from "./integrations.js";
|
|
204
209
|
import { BACKUP_CSS, backupHtml } from "./backup-ui.js";
|
|
205
210
|
import { BACKUP_EVERY_HOURS, MAX_KEEP, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder } from "./backup.js";
|
|
206
211
|
import { prometheusMetrics } from "./metrics.js";
|
|
@@ -438,6 +443,19 @@ export function createDecisionServer(options) {
|
|
|
438
443
|
catch { /* the pause keeps its own state */ }
|
|
439
444
|
return value;
|
|
440
445
|
};
|
|
446
|
+
// Settings → Integrations: checked in the background with a short cache, as the first-run suggestions are.
|
|
447
|
+
const integrations = createIntegrationMonitor(() => ({
|
|
448
|
+
store,
|
|
449
|
+
dir: options.configDir ?? null,
|
|
450
|
+
telegramTokenFile: options.telegramTokenFile ?? null,
|
|
451
|
+
env: process.env,
|
|
452
|
+
repos: managedRepos(),
|
|
453
|
+
gh: options.firstTaskRunner ?? execRun,
|
|
454
|
+
checkConnection: connectionCheck,
|
|
455
|
+
toolHome,
|
|
456
|
+
clock,
|
|
457
|
+
...options.integrationIo,
|
|
458
|
+
}));
|
|
441
459
|
const modelCatalog = openRouterModelsCache(options.modelCatalogFetcher);
|
|
442
460
|
const modelSeams = {
|
|
443
461
|
home: providerHome,
|
|
@@ -1566,7 +1584,7 @@ export function createDecisionServer(options) {
|
|
|
1566
1584
|
}
|
|
1567
1585
|
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
1586
|
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)$");
|
|
1587
|
+
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
1588
|
const resource = request.method === "GET"
|
|
1571
1589
|
? /^\/(?: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
1590
|
: /^\/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 +3323,7 @@ export function createDecisionServer(options) {
|
|
|
3305
3323
|
back: { href: roomId ? "/chat?conversation=" + encodeURIComponent(roomId) : taskChatHref(focusTask?.id ?? ""), label: "Back to chat" },
|
|
3306
3324
|
}) + (who.role === 'approver' && focusTask?.assignment?.state === 'ready-to-check'
|
|
3307
3325
|
&& focusTask.assignment.receipt?.runId === resultRun.id
|
|
3308
|
-
? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf) : '');
|
|
3326
|
+
? completionForm(focusTask.assignment.receipt.taskId, resultRun.id, focusTask.assignment.receipt.digest, who.session.csrf, pullRequestTargetOf(resultRun.id)) : '');
|
|
3309
3327
|
const focusProblem = requestedTask !== null && focusTask === null
|
|
3310
3328
|
? "That task is not available in this workspace."
|
|
3311
3329
|
: requestedResult !== null && focusTask !== null && resultRun === null
|
|
@@ -3621,6 +3639,13 @@ export function createDecisionServer(options) {
|
|
|
3621
3639
|
sessions: store.webSessions(everyone ? null : who.name).map(one => ({ ...one, here: one.idHash === here })), tokens: store.apiTokens(everyone ? null : who.name) };
|
|
3622
3640
|
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
3641
|
}
|
|
3642
|
+
// Settings → Integrations: which integrations work. The list is the last checks; a render never waits on one.
|
|
3643
|
+
if (url.pathname === "/settings/integrations") {
|
|
3644
|
+
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
3645
|
+
return refuse(response, who, 403, "An installation approver sees integrations.", "/settings");
|
|
3646
|
+
const list = integrations.list();
|
|
3647
|
+
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") }));
|
|
3648
|
+
}
|
|
3624
3649
|
// v104: Settings → Monitoring, where the audit stream and traces go. An instance operator's page.
|
|
3625
3650
|
if (url.pathname === "/settings/monitoring") {
|
|
3626
3651
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
@@ -3659,6 +3684,20 @@ export function createDecisionServer(options) {
|
|
|
3659
3684
|
const plan = await checkoutPlan(store, storagePool(databaseFile), now, { manual: true });
|
|
3660
3685
|
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
3686
|
}
|
|
3687
|
+
// Settings → Projects → Pull requests: one project's setup. Off, the checks run on open, so the page says
|
|
3688
|
+
// exactly what turning on would do, or what to fix first.
|
|
3689
|
+
if (url.pathname === "/settings/pull-requests") {
|
|
3690
|
+
const repo = url.searchParams.get("repo") ?? "";
|
|
3691
|
+
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
3692
|
+
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
3693
|
+
return refuse(response, who, 404, "No such project.", "/projects");
|
|
3694
|
+
const publishing = publishingOf(store, repo);
|
|
3695
|
+
const canChange = who.role === "approver" && !store.isDemo();
|
|
3696
|
+
const check = publishing.on || !canChange ? null : await checkPublishing(repo, options.publishExec === undefined ? {} : { exec: options.publishExec });
|
|
3697
|
+
const html = pullRequestSettingsHtml({ repo, name: projectName(repo), csrf: who.session.csrf, canChange, publishing, check,
|
|
3698
|
+
said: url.searchParams.get("said"), problem: url.searchParams.get("problem") });
|
|
3699
|
+
return sendScreen(response, 200, screen("Pull requests", `<p><a href="/projects">Projects</a></p><h1>Pull requests</h1>${html}`, { chrome: chromeFor(project, "projects") }));
|
|
3700
|
+
}
|
|
3662
3701
|
// Sprint 8: Settings → Backups, how the last backup went and the schedule. An instance operator's page.
|
|
3663
3702
|
if (url.pathname === "/settings/backups") {
|
|
3664
3703
|
const databaseFile = store.databaseFile();
|
|
@@ -3833,6 +3872,9 @@ export function createDecisionServer(options) {
|
|
|
3833
3872
|
// The installation fact: when the first Ready result arrived, measured from the first account.
|
|
3834
3873
|
const at = store.firstSuccessAt(now), since = store.installationStartedAt();
|
|
3835
3874
|
return at === null || since === null ? null : firstResultWords(since, at);
|
|
3875
|
+
})(), (() => {
|
|
3876
|
+
const chosen = store.notificationPreference(who.name);
|
|
3877
|
+
return { mode: chosen.mode, digestAt: chosen.digestAt };
|
|
3836
3878
|
})()));
|
|
3837
3879
|
}
|
|
3838
3880
|
if (url.pathname === "/push/key") {
|
|
@@ -4903,7 +4945,7 @@ export function createDecisionServer(options) {
|
|
|
4903
4945
|
peeks[one.path] = peekOf(one.path);
|
|
4904
4946
|
for (const path of candidates)
|
|
4905
4947
|
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));
|
|
4948
|
+
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
4949
|
}
|
|
4908
4950
|
/** The task screen, shared by the GET and by every refusal that re-renders it. */
|
|
4909
4951
|
/**
|
|
@@ -5361,6 +5403,41 @@ export function createDecisionServer(options) {
|
|
|
5361
5403
|
: completionReceiptView(store, latest, store.artifactsFor(latest.id), evidenceRoot, reviewFactsFor(latest.id)),
|
|
5362
5404
|
};
|
|
5363
5405
|
}
|
|
5406
|
+
/** The newest pull request any version of this task opened through Complete; else, for a result completed
|
|
5407
|
+
* without one, the offer to open it (an approver's browser session only). */
|
|
5408
|
+
/** Where "Complete and open a pull request" would open one for this result, or null when it isn't offered. */
|
|
5409
|
+
function pullRequestTargetOf(runId) {
|
|
5410
|
+
const run = store.getRun(runId);
|
|
5411
|
+
const publishing = publishingOf(store, run === null ? null : store.refById(run.taskRef)?.repo ?? null);
|
|
5412
|
+
if (!publishing.on || publishing.legacy || store.publicationForRun(runId) !== null || pullRequestBlocker(store, runId) !== null)
|
|
5413
|
+
return null;
|
|
5414
|
+
return `${publishing.githubRepo} into ${publishing.base}`;
|
|
5415
|
+
}
|
|
5416
|
+
function taskPullRequestOf(taskId, who) {
|
|
5417
|
+
const family = familyOf(taskId);
|
|
5418
|
+
const rootId = family?.root.id ?? taskId;
|
|
5419
|
+
const ids = family === null ? [taskId] : [...family.versions.map(one => one.id)].reverse();
|
|
5420
|
+
const repo = store.lookupRef(taskId)?.repo ?? null;
|
|
5421
|
+
const publishing = publishingOf(store, repo);
|
|
5422
|
+
const target = publishing.on ? publishing.base : null;
|
|
5423
|
+
for (const id of ids) {
|
|
5424
|
+
const ref = store.lookupRef(id);
|
|
5425
|
+
if (ref === null)
|
|
5426
|
+
continue;
|
|
5427
|
+
for (const run of store.runsFor(ref.id)) {
|
|
5428
|
+
const view = pullRequestViewOf(store, run.id);
|
|
5429
|
+
if (view !== null)
|
|
5430
|
+
return { taskId: rootId, view: { ...view, canMerge: view.canMerge && who.role === "approver" && who.via === "cookie" }, offer: null, target };
|
|
5431
|
+
}
|
|
5432
|
+
}
|
|
5433
|
+
if (!publishing.on || who.via !== "cookie" || who.role !== "approver")
|
|
5434
|
+
return null;
|
|
5435
|
+
const assignment = assignmentOf(store, taskId, clock(), { principal: "operator", repos: admissionList(), includeUnplaced: visible(null) }, evidenceRoot);
|
|
5436
|
+
const receipt = assignment?.receipt ?? null;
|
|
5437
|
+
if (assignment?.state !== "complete" || receipt === null || store.publicationForRun(receipt.runId) !== null || pullRequestBlocker(store, receipt.runId) !== null)
|
|
5438
|
+
return null;
|
|
5439
|
+
return { taskId: rootId, view: null, offer: { taskId: receipt.taskId, runId: receipt.runId, digest: receipt.digest }, target };
|
|
5440
|
+
}
|
|
5364
5441
|
function taskScreen(response, who, taskId, problem, status, scopeDraft, cancelDraft) {
|
|
5365
5442
|
const admittedFamily = familyOf(taskId);
|
|
5366
5443
|
if (admittedFamily?.problem != null)
|
|
@@ -5376,6 +5453,7 @@ export function createDecisionServer(options) {
|
|
|
5376
5453
|
presentedData.scopeDraft = scopeDraft;
|
|
5377
5454
|
if (cancelDraft !== undefined)
|
|
5378
5455
|
presentedData.cancelDraft = cancelDraft;
|
|
5456
|
+
presentedData.pullRequest = taskPullRequestOf(taskId, who);
|
|
5379
5457
|
const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
5380
5458
|
const page = taskPage(paneProject === null && !unscopedMode
|
|
5381
5459
|
? chromeFor(paneProject, "tasks")
|
|
@@ -6668,6 +6746,43 @@ export function createDecisionServer(options) {
|
|
|
6668
6746
|
const done = await discardCheckout(store, pool, body.get("path") ?? "", now, who.name);
|
|
6669
6747
|
return done.ok ? back("said", `Discarded, about ${bytesWords(done.bytes)}. Its branch stays.`) : back("problem", done.message);
|
|
6670
6748
|
}
|
|
6749
|
+
// Pull requests for one project: turn on (the checks run again and must match what the page showed), merge
|
|
6750
|
+
// settings, or turn off. Each takes the password and lands in the ledger.
|
|
6751
|
+
if (url.pathname === "/settings/pull-requests") {
|
|
6752
|
+
const repo = body.get("repo") ?? "";
|
|
6753
|
+
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
6754
|
+
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
6755
|
+
return refuse(response, who, 404, "No such project.", "/projects");
|
|
6756
|
+
if (who.role !== "approver" || store.isDemo())
|
|
6757
|
+
return refuse(response, who, 403, "An approver sets up pull requests.", "/projects");
|
|
6758
|
+
const back = (key, words) => redirect(response, `/settings/pull-requests?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
6759
|
+
const act = body.get("act");
|
|
6760
|
+
if (!authenticateApprover(store, who.name, body.get("password") ?? "", repo).ok)
|
|
6761
|
+
return back("problem", "Enter your Toolroll password to change pull requests.");
|
|
6762
|
+
if (act === "on") {
|
|
6763
|
+
const checked = await checkPublishing(repo, options.publishExec === undefined ? {} : { exec: options.publishExec });
|
|
6764
|
+
if (!checked.ok)
|
|
6765
|
+
return back("problem", checked.message);
|
|
6766
|
+
if (checked.plan.githubRepo !== body.get("github") || checked.plan.base !== body.get("base"))
|
|
6767
|
+
return back("problem", "GitHub answered differently from what you saw. Check the details again.");
|
|
6768
|
+
savePublishing(store, checked.plan, who.name, {}, now);
|
|
6769
|
+
return back("said", "Pull requests are on.");
|
|
6770
|
+
}
|
|
6771
|
+
if (act === "settings") {
|
|
6772
|
+
const method = body.get("method") ?? "";
|
|
6773
|
+
if (!MERGE_METHODS.includes(method))
|
|
6774
|
+
return back("problem", "Choose a merge method.");
|
|
6775
|
+
const saved = saveMergeSettings(store, repo, { mergeMethod: method, mergeWhenGreen: body.get("when-green") === "1" }, who.name, now);
|
|
6776
|
+
return saved.ok ? back("said", "Saved.") : back("problem", saved.message);
|
|
6777
|
+
}
|
|
6778
|
+
if (act === "off") {
|
|
6779
|
+
const revoked = store.revokePublicationGrant(repo, who.name, now);
|
|
6780
|
+
if (revoked)
|
|
6781
|
+
store.recordAction({ at: now.toISOString(), actor: who.name, repo, taskId: null, runId: null, action: "pull requests turned off", outcome: "off", source: "policy" });
|
|
6782
|
+
return back("said", revoked ? "Pull requests are off." : "Pull requests were already off.");
|
|
6783
|
+
}
|
|
6784
|
+
return back("problem", "That change isn't available.");
|
|
6785
|
+
}
|
|
6671
6786
|
// Sprint 8: back up now. An instance operator; the outcome is kept like a scheduled one's.
|
|
6672
6787
|
if (url.pathname === "/settings/backups/now") {
|
|
6673
6788
|
const databaseFile = store.databaseFile();
|
|
@@ -6716,6 +6831,18 @@ export function createDecisionServer(options) {
|
|
|
6716
6831
|
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
6717
6832
|
return void response.end(zip);
|
|
6718
6833
|
}
|
|
6834
|
+
// Settings → Integrations → Send test: one harmless read-only check of one integration, now.
|
|
6835
|
+
if (url.pathname === "/settings/integrations/test") {
|
|
6836
|
+
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
6837
|
+
return refuse(response, who, 403, "An installation approver tests integrations.", "/settings");
|
|
6838
|
+
const key = body.get("key") ?? "";
|
|
6839
|
+
const back = (name, words) => redirect(response, `/settings/integrations?${name}=${encodeURIComponent(words)}`);
|
|
6840
|
+
const checked = (await integrations.check([key])).find(one => one.key === key);
|
|
6841
|
+
if (checked === undefined)
|
|
6842
|
+
return back("problem", "That integration isn't here any more.");
|
|
6843
|
+
return checked.state === "connected" ? back("said", `${checked.name} works${checked.account === null ? "" : ` (${checked.account})`}.`)
|
|
6844
|
+
: back("problem", `${checked.name}: ${checked.action.kind === "fix" ? checked.action.words : "not set up."}`);
|
|
6845
|
+
}
|
|
6719
6846
|
// v104: change where monitoring goes. A step-up; the ledger keeps before → after (addresses, never secrets).
|
|
6720
6847
|
if (url.pathname === "/settings/monitoring") {
|
|
6721
6848
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
@@ -7275,6 +7402,20 @@ export function createDecisionServer(options) {
|
|
|
7275
7402
|
? "New tasks now use Strict / release. Approved tasks keep their setting."
|
|
7276
7403
|
: "New tasks now use Default quality. Approved tasks keep their setting.")}`);
|
|
7277
7404
|
}
|
|
7405
|
+
if (url.pathname === "/settings/notifications") {
|
|
7406
|
+
// Each person's own choice: a closed list of modes and a 24-hour HH:MM, or off.
|
|
7407
|
+
const mode = body.get("mode"), time = (body.get("digest") ?? "").trim();
|
|
7408
|
+
if (mode !== null && mode !== "quiet" && mode !== "all")
|
|
7409
|
+
return refuse(response, who, 400, "choose Only when I'm needed or Every step", "/settings");
|
|
7410
|
+
if (time !== "" && time !== "off" && !isDigestTime(time))
|
|
7411
|
+
return refuse(response, who, 400, "the evening digest time is HH:MM, or off", "/settings");
|
|
7412
|
+
const before = store.notificationPreference(who.name);
|
|
7413
|
+
const after = store.setNotificationPreference(who.name, { ...(mode === null ? {} : { mode }), ...(time === "" ? {} : { digestAt: time === "off" ? null : time }) }, who.name, now);
|
|
7414
|
+
const said = after.mode !== before.mode
|
|
7415
|
+
? after.mode === "quiet" ? "Chats now message you only when you're needed." : "Chats now message you at every step."
|
|
7416
|
+
: after.digestAt === null ? "Evening digest off." : `Evening digest at ${after.digestAt}.`;
|
|
7417
|
+
return redirect(response, `/settings?said=${encodeURIComponent(said)}`);
|
|
7418
|
+
}
|
|
7278
7419
|
if (url.pathname === "/settings/telegram-digest" && options.telegramTokenFile !== undefined) {
|
|
7279
7420
|
// The cadence is a closed list of minutes — never a free number from
|
|
7280
7421
|
// a form; "off" clears it. Any approver session may set it.
|
|
@@ -8006,7 +8147,28 @@ export function createDecisionServer(options) {
|
|
|
8006
8147
|
if (attendAct !== null) {
|
|
8007
8148
|
return attendMutation(response, who, attendAct.taskId, attendAct.verb, body, now);
|
|
8008
8149
|
}
|
|
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)$");
|
|
8150
|
+
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)$");
|
|
8151
|
+
if (act !== null && act.verb === "merge") {
|
|
8152
|
+
// Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
|
|
8153
|
+
// result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
|
|
8154
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
8155
|
+
return refuse(response, who, 403, "Only an approver can merge.");
|
|
8156
|
+
if (store.isDemo())
|
|
8157
|
+
return taskScreen(response, who, act.taskId, "The demo merges nothing.", 403);
|
|
8158
|
+
const ref = store.lookupRef(act.taskId);
|
|
8159
|
+
if (ref === null || !visible(ref.repo))
|
|
8160
|
+
return refuse(response, who, 404, "no such task", "/tasks");
|
|
8161
|
+
const named = (body.get("run") ?? "").trim();
|
|
8162
|
+
const run = /^[0-9]{1,15}$/.test(named) ? store.getRun(Number(named)) : null;
|
|
8163
|
+
const runTask = run === null ? null : store.externalIdFor(run.taskRef);
|
|
8164
|
+
if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
|
|
8165
|
+
return taskScreen(response, who, act.taskId, "That pull request isn't part of this task.", 409);
|
|
8166
|
+
}
|
|
8167
|
+
const merged = await mergeAsPerson(store, { runId: run.id, name: who.name, password: body.get("token") ?? "", ...(options.publishExec === undefined ? {} : { exec: options.publishExec }), clock });
|
|
8168
|
+
if (!merged.ok)
|
|
8169
|
+
return taskScreen(response, who, act.taskId, merged.message, merged.reason === "password" ? 403 : 409);
|
|
8170
|
+
return redirect(response, `${taskHref(act.taskId)}#merge`);
|
|
8171
|
+
}
|
|
8010
8172
|
if (act !== null) {
|
|
8011
8173
|
return taskMutation(response, who, act.taskId, act.verb, body, now);
|
|
8012
8174
|
}
|
|
@@ -10043,6 +10205,14 @@ export function createDecisionServer(options) {
|
|
|
10043
10205
|
if (current === null || current.receipt?.taskId !== taskId || !/^[0-9]{1,15}$/.test(namedRun) || current.receipt.runId !== Number(namedRun)) {
|
|
10044
10206
|
return taskScreen(response, who, taskId, "This result changed. Open the current result before marking it complete.", 409);
|
|
10045
10207
|
}
|
|
10208
|
+
if (body.get("publish") === "1") {
|
|
10209
|
+
// "Complete and open a pull request": the same exact-receipt completion, plus the owed PR for this
|
|
10210
|
+
// exact commit in one transaction. The watch process pushes and opens it; the task shows its progress.
|
|
10211
|
+
const opened = completeAndOpenPullRequest(store, { taskId, digest, runId: Number(namedRun), who: principal, root: evidenceRoot }, now);
|
|
10212
|
+
if (!opened.ok)
|
|
10213
|
+
return taskScreen(response, who, taskId, opened.message, 409);
|
|
10214
|
+
return redirect(response, `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge`);
|
|
10215
|
+
}
|
|
10046
10216
|
const completed = checkAssignmentAsOperator(store, taskId, digest, principal, now, evidenceRoot);
|
|
10047
10217
|
if (!completed.ok)
|
|
10048
10218
|
return taskScreen(response, who, taskId, completed.message, 409);
|
|
@@ -10567,6 +10737,7 @@ export function createDecisionServer(options) {
|
|
|
10567
10737
|
proof,
|
|
10568
10738
|
terminal,
|
|
10569
10739
|
publication,
|
|
10740
|
+
pullRequestTo: pullRequestTargetOf(run.id),
|
|
10570
10741
|
ciFailing: publication !== null && publication.prNumber !== null && store.hasOpenCiEpisode(publication.githubRepo, publication.prNumber),
|
|
10571
10742
|
files,
|
|
10572
10743
|
outsideTouches: files.filter(one => one.outsideTouches).map(one => one.path),
|
|
@@ -12946,6 +13117,23 @@ ${THEME_DARK}
|
|
|
12946
13117
|
.status-line[data-tone="ready"] .status-dot, .status-line[data-tone="done"] .status-dot { background: var(--success); }
|
|
12947
13118
|
.status-line[data-tone="muted"] { color: var(--muted-foreground); font-weight: 500; }
|
|
12948
13119
|
.status-line[data-tone="muted"] .status-dot { background: var(--border); }
|
|
13120
|
+
.pull-request { display: grid; gap: .35rem; }
|
|
13121
|
+
.pull-request p { margin: 0; overflow-wrap: anywhere; }
|
|
13122
|
+
.pull-request-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: .25rem .75rem; }
|
|
13123
|
+
.pull-request-head a, .pull-request .meta a { display: inline-flex; align-items: center; min-height: 44px; }
|
|
13124
|
+
.pull-request-head strong { font-size: 1.0625rem; font-weight: 600; color: var(--foreground); display: inline-flex; align-items: center; gap: .5rem; }
|
|
13125
|
+
.pull-request-head strong::before { content: ""; width: .5rem; height: .5rem; border-radius: 999px; background: var(--muted-foreground); }
|
|
13126
|
+
.pull-request--ok .pull-request-head strong::before { background: var(--success, #15803d); }
|
|
13127
|
+
.pull-request--problem .pull-request-head strong::before { background: var(--destructive, #b91c1c); }
|
|
13128
|
+
.pull-request-act { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .75rem; margin: .25rem 0 0; }
|
|
13129
|
+
.pull-request-act .meta { flex: 1 1 100%; }
|
|
13130
|
+
.pull-request-act label { display: grid; gap: .25rem; flex: 1 1 12rem; margin: 0; }
|
|
13131
|
+
.pull-request-act label input { margin: 0; min-height: 44px; box-sizing: border-box; width: 100%; }
|
|
13132
|
+
.pull-request-act button { min-height: 44px; white-space: nowrap; }
|
|
13133
|
+
.result-complete-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
|
|
13134
|
+
.result-complete-actions button { white-space: nowrap; }
|
|
13135
|
+
.result-complete-actions button[name=publish], .pull-request-act button { background: var(--primary); color: var(--primary-foreground); border-color: var(--primary); font-weight: 600; }
|
|
13136
|
+
@media (max-width: 480px) { .pull-request-act button, .result-complete-actions button { flex: 1 1 100%; } }
|
|
12949
13137
|
.receipt-publication { position: relative; z-index: 1; margin: .35rem 0 0; font-size: .8125rem; color: var(--muted-foreground); }
|
|
12950
13138
|
/* A raw repository path in ordinary copy wraps at any point rather than
|
|
12951
13139
|
widening the page (the phone task-list overflow, package 1). */
|
|
@@ -14114,7 +14302,7 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
|
|
|
14114
14302
|
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
14303
|
/** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
|
|
14116
14304
|
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}');
|
|
14305
|
+
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 + 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
14306
|
/**
|
|
14119
14307
|
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
14120
14308
|
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
@@ -17568,7 +17756,7 @@ function githubReposPage(chrome, data) {
|
|
|
17568
17756
|
`<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
|
|
17569
17757
|
].join("\n"), { chrome });
|
|
17570
17758
|
}
|
|
17571
|
-
function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/") {
|
|
17759
|
+
function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn) {
|
|
17572
17760
|
// The onboarding card (repo onboarding, findings 1-39): preview first,
|
|
17573
17761
|
// then a password-confirmed clone into a configured root. Disabled
|
|
17574
17762
|
// states explain themselves in words (finding 28/39).
|
|
@@ -17658,6 +17846,7 @@ function projectsPage(chrome, recent, candidates, open, csrf, problem, _unscoped
|
|
|
17658
17846
|
return {
|
|
17659
17847
|
name, path, shortPath: path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path, open: open !== null && open === path, openedAt,
|
|
17660
17848
|
knowledgeHref: `/settings/knowledge?repo=${encodeURIComponent(path)}`,
|
|
17849
|
+
...(pullRequestsOn === undefined ? {} : { pullRequests: { href: `/settings/pull-requests?repo=${encodeURIComponent(path)}`, on: pullRequestsOn(path) } }),
|
|
17661
17850
|
peek: peek === null ? null : [
|
|
17662
17851
|
...(peek.waiting > 0 ? [{ label: `${peek.waiting} waiting on you`, href: "/", tone: "attention" }] : []),
|
|
17663
17852
|
...(peek.running > 0 ? [{ label: `${peek.running} running`, href: "/runs", tone: "neutral" }] : []),
|
|
@@ -19047,6 +19236,9 @@ function taskBodyParts(data) {
|
|
|
19047
19236
|
return "no project — no publication grant can apply";
|
|
19048
19237
|
if (grant === null)
|
|
19049
19238
|
return "branch only — publishing is not set up";
|
|
19239
|
+
if (grant.publishOn === "complete") {
|
|
19240
|
+
return `pull requests to ${grant.githubRepo} into ${grant.base} when completed · ${grant.mergeWhenGreen === true ? "merges when checks pass" : "a person merges"} (${grant.mergeMethod ?? "squash"})`;
|
|
19241
|
+
}
|
|
19050
19242
|
return [
|
|
19051
19243
|
grant.capabilities.includes("push-branch") ? `may push ${grant.headPrefix}* to ${grant.githubRepo}` : "cannot push",
|
|
19052
19244
|
grant.capabilities.includes("open-pr") ? `may open a PR against ${grant.base}${grant.draft ? " (draft)" : ""}` : null,
|
|
@@ -19102,7 +19294,8 @@ function taskBodyParts(data) {
|
|
|
19102
19294
|
facts.push({ label: "Budget", parts: [`${data.budgetHold} · `, { label: "Spend", href: "/spend" }] });
|
|
19103
19295
|
if (data.policyHold != null)
|
|
19104
19296
|
facts.push({ label: "Policy", parts: [`${data.policyHold} · `, { label: "Policy", href: "/settings/policy" }] });
|
|
19105
|
-
|
|
19297
|
+
// The pull-request card already says this once, with its action.
|
|
19298
|
+
if (data.publication !== null && data.publication !== undefined && data.pullRequest?.view == null) {
|
|
19106
19299
|
const prHref = safePrUrl(data.publication.prUrl), pr = `PR #${data.publication.prNumber ?? "?"}`;
|
|
19107
19300
|
facts.push({ label: "Published", parts: [prHref === null ? pr : { label: pr, href: prHref },
|
|
19108
19301
|
` · ${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 +19476,9 @@ function taskBodyParts(data) {
|
|
|
19283
19476
|
: data.filedVia === null || data.filedVia === undefined
|
|
19284
19477
|
? ""
|
|
19285
19478
|
: ` · filed via ${escape(data.filedVia)}`}${data.deliverable === "report" ? ` · <span class="badge">scout</span>` : ""}</p>`;
|
|
19286
|
-
|
|
19479
|
+
// A pull request that can be merged (or opened) owns the one primary action; the result stays in its section.
|
|
19480
|
+
const pullRequestActs = data.csrf !== "" && (data.pullRequest?.view?.canMerge === true || (data.pullRequest?.view == null && data.pullRequest?.offer != null));
|
|
19481
|
+
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
19482
|
// The result takes over from the task status as soon as it is ready.
|
|
19288
19483
|
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
19484
|
// The exact-run control (v52), directly under the scheduler's answer:
|
|
@@ -19413,6 +19608,7 @@ function taskBodyParts(data) {
|
|
|
19413
19608
|
approval: approvalHtml,
|
|
19414
19609
|
lead: [
|
|
19415
19610
|
{ key: "history", html: data.history ?? "" }, { key: "control", html: controlHtml }, { key: "problem", html: problemHtml },
|
|
19611
|
+
{ key: "pull-request", html: data.pullRequest == null ? "" : pullRequestCardHtml(data.pullRequest, data.csrf) },
|
|
19416
19612
|
{ key: "needs-scope", html: needsScopeCard }, { key: "progress", html: progressCard }, { key: "revisions", html: revisionLedgerCard },
|
|
19417
19613
|
{ key: "plan", html: planCard }, { key: "mirror", html: mirrorCard }, { key: "contest", html: contestCard }, { key: "attempt", html: attemptPanel },
|
|
19418
19614
|
].filter(one => one.html !== ""),
|
|
@@ -19917,7 +20113,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
19917
20113
|
const complete = assignment?.state === "ready-to-check" && assignment.receipt !== null && canRetryReview && csrf !== ""
|
|
19918
20114
|
? { action: `${taskHref(view.taskId)}/complete`, receipt: assignment.receipt.digest, run: run.id } : null;
|
|
19919
20115
|
if (complete !== null)
|
|
19920
|
-
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf));
|
|
20116
|
+
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
|
|
19921
20117
|
if (view.notes.length > 0) {
|
|
19922
20118
|
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
|
|
19923
20119
|
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${escape(when(one.createdAt))}</span> ${escape(one.note)}</p>`).join("\n") +
|
|
@@ -19931,8 +20127,41 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
19931
20127
|
},
|
|
19932
20128
|
};
|
|
19933
20129
|
}
|
|
19934
|
-
function completionForm(taskId, runId, digest, csrf) {
|
|
19935
|
-
|
|
20130
|
+
function completionForm(taskId, runId, digest, csrf, pullRequestTo = null) {
|
|
20131
|
+
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}">`;
|
|
20132
|
+
if (pullRequestTo !== null) {
|
|
20133
|
+
// With pull requests set up: the PR is the primary road, a bare completion the quiet one beside it.
|
|
20134
|
+
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>`;
|
|
20135
|
+
}
|
|
20136
|
+
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>`;
|
|
20137
|
+
}
|
|
20138
|
+
/** The pull request, in one card: its state and link, one line of detail, and the one action it needs — Merge
|
|
20139
|
+
* behind the password when checks passed, or Open a pull request for a result completed without one. */
|
|
20140
|
+
function pullRequestCardHtml(pr, csrf) {
|
|
20141
|
+
if (pr.view === null) {
|
|
20142
|
+
if (pr.offer === null || csrf === "")
|
|
20143
|
+
return "";
|
|
20144
|
+
return `<section class="card pull-request" id="merge" data-pull-request="none"><div class="pull-request-head"><strong>No pull request</strong></div>` +
|
|
20145
|
+
`<p class="meta">This result was completed without one.</p>` +
|
|
20146
|
+
`<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">` +
|
|
20147
|
+
`<button type="submit" class="secondary">Open a pull request</button></form></section>`;
|
|
20148
|
+
}
|
|
20149
|
+
const view = pr.view;
|
|
20150
|
+
const url = safePrUrl(view.prUrl);
|
|
20151
|
+
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>`;
|
|
20152
|
+
const tone = view.state === "ready" || view.state === "merged" ? "ok" : view.state === "failing" || view.state === "failed" || view.state === "closed" ? "problem" : "waiting";
|
|
20153
|
+
const revision = view.revisionTask === null || view.state !== "failing" ? "" : ` <a href="${taskHref(view.revisionTask)}">Open revision</a>`;
|
|
20154
|
+
const merged = view.mergeCommit === null ? "" : `<p class="meta">Merge commit <span class="mono">${escape(view.mergeCommit.slice(0, 12))}</span></p>`;
|
|
20155
|
+
const target = pr.target === null ? "the base branch" : pr.target;
|
|
20156
|
+
const act = !view.canMerge || csrf === "" || view.prNumber === null ? "" :
|
|
20157
|
+
`<form method="post" action="${taskHref(pr.taskId)}/merge" class="pull-request-act" data-merge-form>` +
|
|
20158
|
+
`<input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="run" value="${view.runId}">` +
|
|
20159
|
+
`<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>` +
|
|
20160
|
+
`<label>Password<input type="password" name="token" autocomplete="current-password" required></label>` +
|
|
20161
|
+
`<button type="submit">Merge</button></form>`;
|
|
20162
|
+
return `<section class="card pull-request pull-request--${tone}" id="merge" data-pull-request="${escape(view.state)}">` +
|
|
20163
|
+
`<div class="pull-request-head"><strong>${escape(view.label)}</strong>${link}</div>` +
|
|
20164
|
+
`<p class="meta">${escape(view.detail)}${revision}</p>${merged}${act}</section>`;
|
|
19936
20165
|
}
|
|
19937
20166
|
/** Exactly one primary road per result, chosen from its state; the
|
|
19938
20167
|
* others stay reachable from their own sections. Every form posts to the
|
|
@@ -21537,7 +21766,7 @@ function telegramDeliveryWords(store, bot) {
|
|
|
21537
21766
|
return `Telegram pushes new messages to ${state.url}${state.problem === null ? "." : `, but ${state.problem.charAt(0).toLowerCase()}${state.problem.slice(1)}.`}`;
|
|
21538
21767
|
return `Toolroll asks Telegram for new messages every few seconds${state?.problem ? ` (${state.problem})` : ""}.`;
|
|
21539
21768
|
}
|
|
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) {
|
|
21769
|
+
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
21770
|
const permissionCard = permissionDefault === null
|
|
21542
21771
|
? ""
|
|
21543
21772
|
: [
|
|
@@ -21566,7 +21795,23 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21566
21795
|
? ""
|
|
21567
21796
|
: `<p class="meta settings-changed">Changed ${escape(when(qualityDefault.updatedAt))}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
|
|
21568
21797
|
].join("\n");
|
|
21569
|
-
|
|
21798
|
+
// Quiet chat: this person's own choice. The installation's Telegram cadence only bundles Every step updates.
|
|
21799
|
+
const chatCard = chatNotices === null || csrf === ""
|
|
21800
|
+
? ""
|
|
21801
|
+
: [
|
|
21802
|
+
"<h3>Chat messages</h3>",
|
|
21803
|
+
`<form method="post" action="/settings/notifications" class="card" data-autosave>`,
|
|
21804
|
+
`<input type="hidden" name="csrf" value="${escape(csrf)}">`,
|
|
21805
|
+
`<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>`,
|
|
21806
|
+
`<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>`,
|
|
21807
|
+
`<label>Evening digest<select name="digest">` +
|
|
21808
|
+
digestTimes(chatNotices.digestAt).map(([value, label]) => `<option value="${value}"${value === (chatNotices.digestAt ?? "off") ? " selected" : ""}>${label}</option>`).join("") +
|
|
21809
|
+
`</select></label>`,
|
|
21810
|
+
`<p class="meta">One message: what finished, what waits, what failed.</p>`,
|
|
21811
|
+
`<button type="submit">Save</button>`,
|
|
21812
|
+
`</form>`,
|
|
21813
|
+
].join("\n");
|
|
21814
|
+
const digestCard = digest === null || csrf === "" || chatNotices?.mode === "quiet"
|
|
21570
21815
|
? ""
|
|
21571
21816
|
: [
|
|
21572
21817
|
"<h3>Telegram digest</h3>",
|
|
@@ -21729,7 +21974,8 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21729
21974
|
}),
|
|
21730
21975
|
services: messaging === null || messaging.configured.length === 0 ? null : { configured: messaging.configured, channel: messaging.channel, implicit: messaging.implicit },
|
|
21731
21976
|
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
|
-
|
|
21977
|
+
chat: chatNotices === null || csrf === "" ? null : chatNotices,
|
|
21978
|
+
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
21979
|
telegram: { state: hasEnv ? "from the environment" : existing === null ? "not set" : "saved", current, delivery: telegramDelivery },
|
|
21734
21980
|
email,
|
|
21735
21981
|
workers,
|
|
@@ -21746,7 +21992,8 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21746
21992
|
workersCard(workers),
|
|
21747
21993
|
updatesCard(updates, csrf),
|
|
21748
21994
|
firstResult === null ? "" : `<p class="meta" data-first-result>${escape(firstResult)}</p>`,
|
|
21749
|
-
pushCard === "" && messagingCard === "" && digestCard === "" ? "" : `<h2>Notifications</h2>`,
|
|
21995
|
+
pushCard === "" && messagingCard === "" && digestCard === "" && chatCard === "" ? "" : `<h2>Notifications</h2>`,
|
|
21996
|
+
chatCard,
|
|
21750
21997
|
messagingCard,
|
|
21751
21998
|
pushCard,
|
|
21752
21999
|
digestCard,
|
|
@@ -21842,6 +22089,7 @@ function settingsTiles() {
|
|
|
21842
22089
|
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
22090
|
}
|
|
21844
22091
|
const SETTINGS_TILE_ICONS = [
|
|
22092
|
+
["/settings/integrations", "Integrations", `<path d="M9 2v6M15 2v6"/><path d="M6 8h12v4a6 6 0 0 1-12 0z"/><path d="M12 18v4"/>`],
|
|
21845
22093
|
["/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
22094
|
["/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
22095
|
["/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"/>`],
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
-
content?: never;
|
|
19
18
|
ok: false;
|
|
20
19
|
message: string;
|
|
21
20
|
plan?: never;
|
|
21
|
+
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
-
content?: never;
|
|
34
33
|
ok: false;
|
|
35
34
|
message: string;
|
|
36
35
|
plan?: never;
|
|
36
|
+
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|